From e7d726355d95cd2a0d7d6a7dc6eefe2310a55289 Mon Sep 17 00:00:00 2001 From: dave Date: Tue, 1 Aug 2006 19:43:15 +0000 Subject: [PATCH] The new judge scheduler. - Missing: Having a project judged more than once by different judging teams is untested, I don't htink it'll work properly, still working on that part. - Missing: Auto updating the configuration if any of the variables are missing. - Missing: A way to preserve judging questions (like willing_chair), so the user doesn't delete them and break the scheduler, OR, notice saying that the question has been deleted and the scheduler won't use the chair calculations, then we need a way to add them back in with the click of a button. --- admin/anneal.inc.php | 225 ++++++ admin/judges.inc.php | 12 + admin/judges_sa.php | 888 ++++++++++----------- admin/judges_schedulerconfig.php | 107 +-- admin/judges_schedulerconfig_check.inc.php | 262 +++--- config_editor.inc.php | 104 +++ db/db.update.20.sql | 12 + sfiab.css | 4 + 8 files changed, 922 insertions(+), 692 deletions(-) create mode 100644 admin/anneal.inc.php create mode 100644 config_editor.inc.php diff --git a/admin/anneal.inc.php b/admin/anneal.inc.php new file mode 100644 index 0000000..3bc606c --- /dev/null +++ b/admin/anneal.inc.php @@ -0,0 +1,225 @@ +num_buckets = $num_buckets; + $this->start_temp = $start_temp; + $this->start_moves = $start_moves; + $this->cost_function_callback = $cost_function_cb; + + $this->bucket_cost = array(); + $this->bucket = array(); + $this->iterations = 0; + $this->items_per_bucket = count($items) / $num_buckets; + $this->rate = $rate; + $ipb = ceil($this->items_per_bucket); + $i=0; + $this->cost = 0; + for($x=0; $x<$num_buckets; $x++) { + unset($b); + $b = array(); + for($y=0;$y<$ipb; $y++) { + if($i == count($items)) break; + $b[] = $items[$i]; + $i++; + } + $this->bucket[] = $b; + $c = $this->cost_function($x); + $this->bucket_cost[] = $c; + $this->cost += $c; + } + TRACE("Annealer setup: T={$this->start_temp}, ". + "M={$this->start_moves}, Bkts={$this->num_buckets}, ". + "Cost={$this->cost}\n"); + } + + + function pick_move() + { + /* Pick a bucket and item */ + while(1) { + $b1 = rand(0, $this->num_buckets - 1); + if(count($this->bucket[$b1]) > 0) break; + } + $i1 = rand(0, count($this->bucket[$b1]) -1); + + /* Pick a csecond bucket that is different thatn the first */ + $b2 = rand(0, $this->num_buckets - 2); + if($b2 >= $b1) $b2++; + + /* Picket an item, or a blank, in the second bucket */ + $i2 = rand(0, count($this->bucket[$b2])); + if($i2 == count($this->bucket[$b2])) $i2 = -1; +// TRACE("Move ($b1,$i1)<->($b2,$i2)\n"); + return array($b1, $i1, $b2, $i2); + } + + function cost_function($b) + { + $bkt = $this->bucket[$b]; + $cb = $this->cost_function_callback; + $c = $cb($this, $b, $bkt); +// $this->print_bucket($b); +// print("Computed cost to be: $c\n"); + return $c; + } + + function compute_delta_cost($move) + { + list($b1, $i1, $b2, $i2) = $move; + + $cost = 0; + + $b1_old = $this->bucket[$b1]; + $b2_old = $this->bucket[$b2]; + + $b1_new = array(); + $b2_new = array(); + /* Make 2 new bucket lists */ + for($x=0; $xbucket[$b1] = $b1_new; + $this->bucket[$b2] = $b2_new; + + /* Compute costs */ + $cost -= $this->bucket_cost[$b1]; + $cost -= $this->bucket_cost[$b2]; + + $c1 = $this->cost_function($b1); + $c2 = $this->cost_function($b2); + + $cost += $c1 + $c2; + + /* Return to the original bucket lists */ + $this->bucket[$b1] = $b1_old; + $this->bucket[$b2] = $b2_old; + + return array($cost, array($c1, $b1_new, $c2, $b2_new)); + } + + function accept_move($move, $movedata) + { + list($b1, $i1, $b2, $i2) = $move; + list($c1, $b1_new, $c2, $b2_new) = $movedata; +// TRACE("Old buckets:\n"); +// $this->print_bucket($b1); +// $this->print_bucket($b2); + $this->bucket[$b1] = $b1_new; + $this->bucket[$b2] = $b2_new; +// TRACE("New buckets:\n"); +// $this->print_bucket($b1); +// $this->print_bucket($b2); + $this->bucket_cost[$b1] = $c1; + $this->bucket_cost[$b2] = $c2; + } + + function anneal() + { + $temperature = $this->start_temp; + $current_cost = $this->cost; + $last_cost = 0; + $last_cost_count = 0; + + if($this->num_buckets <= 1) { + TRACE("Only one Bucket, nothing to anneal.\n"); + return; + } +// $this->print_buckets(); + while(1) { + $moves = $this->start_moves; + for($m = 0; $m<$moves; $m++) { +// $this->print_buckets(); + /* Pick 2 moves at random */ + $move = $this->pick_move(); + /* See what the new cost is compared to the old */ + list($delta_c, $movedata) = + $this->compute_delta_cost($move); + + + $r = floatval(rand()) / floatval(getrandmax()); + /* Decide if we want to keep it */ + $e = exp(-$delta_c / $temperature); +// TRACE("r=$r, exp=$e, delta=$delta_c\n"); + if($r < exp(-$delta_c / $temperature)) { + /* Yes, we do, record the move */ + $this->accept_move($move, $movedata); + $current_cost += $delta_c; + $n_accepted++; + if($current_cost < $this->cost) + $this->cost = $current_cost; +// TRACE("Move accepted, cost=$current_cost\n"); + } else { +// TRACE("Move rejected\n"); + } + $this->iterations++; + if($this->iterations % 50000 == 0) { + TRACE(" {$this->iterations} iterations, cost={$this->cost}, temperature=$temperature\n"); +// $this->print_buckets(); + } + } + if($this->cost == $last_cost) { + $last_cost_count ++; + } else { + $last_cost = $this->cost; + $last_cost_count=0; + } + + if($temperature < 0.1 && $last_cost_count > 10) + break; +// TRACE("Cost is {$this->cost}\n"); + $temperature *= $this->rate; + } + TRACE("Annealing complete. {$this->iterations} iterations. Final cost is {$this->cost}\n"); + } + + function print_bucket($x) + { + $b = $this->bucket[$x]; + print("Bucket $x: (cost: {$this->bucket_cost[$x]})\n"); + print(" "); + for($y=0;$ynum_buckets; $x++) { + $this->print_bucket($x); + } + } +} +?> diff --git a/admin/judges.inc.php b/admin/judges.inc.php index 3509def..08719e7 100644 --- a/admin/judges.inc.php +++ b/admin/judges.inc.php @@ -178,4 +178,16 @@ function getJudgingTeam($teamid) } +function judges_scheduler_load_config() +{ + global $config; + $configq=mysql_query("SELECT * FROM config WHERE year='".$config['FAIRYEAR']."' AND var LIKE 'JSCHEDULER_%'"); + $data=array(); + while($configr=mysql_fetch_object($configq)) { + $v = substr($configr->var, 11); + $data[$v]=$configr->val; + } + return $data; +} + ?> diff --git a/admin/judges_sa.php b/admin/judges_sa.php index 09eec68..9d44c4a 100644 --- a/admin/judges_sa.php +++ b/admin/judges_sa.php @@ -23,57 +23,57 @@ ?> "; +echo "
"; +echo i18n("When complete, a green bar will appear at the BOTTOM of this page +saying that everything has completed successfully. When complete, you can use +the following links to manage the Judging Teams and the Judges assigned to them +(clicking on these links now will stop the scheduler)."); +echo "
"; +echo "
"; +echo "".i18n("Manage Judge Teams").""; +echo "
"; +echo "".i18n("Manage Judge Members").""; +echo "
"; +echo "
"; +echo i18n("If you get an error like: \"Fatal error: Maximum execution time of +30 seconds exceeded...\" you will need to talk to your system admin and have +them adjust the \"max_execution_time\" variable in the \"php.ini\" file from +30(seconds) to something larger, like 600 (10 minutes). And then have them +restart the webserver for the change to take effect"); +echo "
"; +echo "
"; -/* Given the team data, pick a judge, then pick a new team - * for them */ -function pick_random_move(&$team) -{ -//TRACE_R($team); - /* Pick 2 random teams*/ - $tms = count($team); - while(1) { - $t1 = rand(0, $tms - 1); - if(count($team[$t1]['judges']) > 0) break; - } - $t2 = rand(0, $tms - 2); - if($t2 >= $t1) $t2++; +TRACE("
");
 
-	/* Pick a judge on team1 */
-	$j1 = rand(0, count($team[$t1]['judges']) - 1);
-	/* Pick a judge or the empty slot on team2 */
-	$j2 = rand(0, count($team[$t2]['judges']));
 
-	if($j2 == count($team[$t2]['judges'])) {
-		$j2 = -1;
-	}
+//function TRACE() { }
+//function TRACE_R() { }
+function TRACE($str) { print($str); }
+function TRACE_R($array) { print_r($array); }
 
-	
-	TRACE("Random move: ($t1,$j1) ($t2,$j2)\n");
-	TRACE_R($team[$t1]['judges']);
-	TRACE("T2:\n");
-	TRACE_R($team[$t2]['judges']);
-	TRACE("\n");
-	/* The move is team1,judge1 <==> team2,judge2 */
-	return array($t1, $j1, $t2, $j2);
-}
 
 /* The cost function is:
 	+ 50 * each judge below the min for each team
@@ -95,406 +95,265 @@ function pick_random_move(&$team)
 
 /* Compute the cost of adding a judge to a team */
 
-function compute_team_cost(&$teams, &$judges, $team_id)
+function judges_cost_function($annealer, $bucket_id, $ids)
 {
+	global $jteam;
+	global $judges;
+	/* Bucket ID is the team number */
+	/* ids are the judge ids currently in the bucket */
+
+//	TRACE("Bucket id=$bucket_id, ids=");
+//	TRACE_R($ids);
+
 	$cost = 0;
 	$have_chair = 0;
+
+	if($bucket_id == 0) {
+		/* This is the placeholder */
+		$cost = count($ids) * 5;
+//		TRACE("Extra judge team cost=$cost\n");
+		return $cost;
+	}
 	
-	$t =& $teams[$team_id];
+	
+	$t =& $jteam[$bucket_id];
 	
 	/* Compute the over max / under min costs */
-	$c = count($t['judges']);
-	$min = ($c < $t['min']) ? $t['min'] - $c : 0;
-	$max = ($c > $t['max']) ? $c - $t['max'] : 0;
+	$c = count($ids);
+	$min = ($c < $t['min_judges']) ? $t['min_judges'] - $c : 0;
+	$max = ($c > $t['max_judges']) ? $c - $t['max_judges'] : 0;
 	$cost += $min * 50;
 	$cost += $max * 10;
 
 //	TRACE("Under min=$min, over max=$max\n");
 
 	/* For each judge on the team, score their preferences */
-	reset($t['judges']);
-	while( list($key, $judge_id) = each($t['judges']) ) {
-		$j =& $judges[$judge_id];
-
+	for($x=0; $xitems_per_bucket)) * 100;
+	/* Score 100 pts for multiple languages */
+	$cost += (count($t_lang) - 1) * 75;
+	/* Score 25pts for multiple divs/cats */
+	$cost += (count($t_div) - 1) * 25;
+	$cost += (count($t_cat) - 1) * 25;
 	
-	TRACE("Team $tid1 cost {$t1['cost']} -> $c1\n");
-	TRACE("Team $tid2 cost {$t2['cost']} -> $c2\n");
-	TRACE("Delta = $cost\n");
-
-	$t1['judges'] = $ja1;
-	$t2['judges'] = $ja2;
-
-	return array($cost, array($nj1, $c1, $nj2, $c2)) ;
-}
-
-function record_move(&$teams, $move, $movedata)
-{
-	list($tid1, $jidx1, $tid2, $jidx2) = $move;
-	list($judges1, $c1, $judges2, $c2) = $movedata;
-
-	$t1 =& $teams[$tid1];
-	$t2 =& $teams[$tid2];
-
-//	TRACE_R($t1);
-
-	$t1['judges'] = $judges1;
-	$t1['cost'] = $c1;
-	$t2['judges'] = $judges2;
-	$t2['cost'] = $c2;
-	
-	TRACE("T1:");
-	TRACE_R($t1['judges']);
-	TRACE("\nT2:");
-	TRACE_R($t2['judges']);
-	TRACE("\n");
-
-//	TRACE_R($t1);
-//	TRACE_R($t2);
-
-}
-
-
-/* Inputs to annealer:
- *  - data['min_per_team']
- *  -      'max_per_team'
- *  -      'teams' [division][category] = number of teams
- *
- *  - judges 
- */
-function judges_assign_anneal($divisions, $categories, $languages, $judges, &$team, $data)
-{
-	$num_teams = count($team);
-	$x=0;
-
-	TRACE("Input: $num_teams juding teams \n");
-
-	if($num_teams  <= 0) return;
-
-	/* Inital assignment of judges to teams */
-	reset($judges);
-	while( list($j, $ji) = each($judges)) {
-		$team[$x % $num_teams]['judges'][] = $j;
-		$x++;
-	}
-
-	/* Compute inital costs */
-	$current_cost = 0;
-//	reset($team);
-	for($x=0; $x<$num_teams; $x++) {
-		$t =& $team[$x];
-		$t['cost'] = compute_team_cost($team, $judges, $x);
-		$current_cost += $t['cost'];
-	}
-	
-	
-
-	/* Anneal */
-	$temperature = 25.0;
-	while(1) {
-		$moves = 1000;
-		for($m = 0; $m<$moves; $m++) {
-			/* Pick 2 moves at random */
-			$move = pick_random_move($team);
-			/* See what the new cost is compared to the old */
-			list($delta_c, $movedata) = 
-				compute_delta_cost($team, $judges, $move);
-			
-			
-			$r = floatval(rand()) / floatval(getrandmax());
-			/* Decide if we want to keep it */
-			$e = exp(-$delta_c / $temperature);
-			TRACE("r=$r, exp=$e\n");
-			if($r < exp(-$delta_c / $temperature)) {
-				/* Yes, we do, record the move */
-				record_move($team, $move, $movedata);
-				$current_cost += $delta_c;
-				$n_accepted++;
-				if($current_cost < $best_cost)
-					$best_cost = $current_cost;
-				TRACE("Move accepted, cost=$current_cost\n");
-			} else {
-				TRACE("Move rejected\n");
-			}
-
-		}
-		TRACE("Cost is $current_cost\n");
-		$temperature *= 0.9;
-
-		if($temperature <= 0.05) break;
-	}
-
+	return $cost;
 }
 
 
+TRACE("\n\n");
+$div = array();
+TRACE("Loading Project Divisions...\n");
 $q=mysql_query("SELECT * FROM projectdivisions WHERE year='".$config['FAIRYEAR']."' ORDER BY id");
 while($r=mysql_fetch_object($q))
 {
 	$divshort[$r->id]=$r->division_shortform;
 	$div[$r->id]=$r->division;
+	TRACE("   {$r->id} - {$div[$r->id]}\n");
 }
 
+TRACE("Loading Project Age Categories...\n");
+$cat = array();
 $q=mysql_query("SELECT * FROM projectcategories WHERE year='".$config['FAIRYEAR']."' ORDER BY id");
-while($r=mysql_fetch_object($q))
+while($r=mysql_fetch_object($q)) {
 	$cat[$r->id]=$r->category;
-
-
-if($config['filterdivisionbycategory']=="yes")
-{
-	$q=mysql_query("SELECT * FROM projectcategoriesdivisions_link WHERE year='".$config['FAIRYEAR']."' ORDER BY id");
-	while($r=mysql_fetch_object($q))
-		$catdiv[$r->projectcategories_id][]=$r->projectdivisions_id;
+	TRACE("   {$r->id} - {$r->category}\n");
 }
 
-
+TRACE("Loading Languages...\n");
 $langr = array();
 $q=mysql_query("SELECT * FROM languages WHERE active='Y'");
-while($r=mysql_fetch_object($q)) 
-	$langr[] = $r->lang;
-
-
-$configq=mysql_query("SELECT * FROM judges_schedulerconfig WHERE year='".$config['FAIRYEAR']."'");
-$data=array();
-while($configr=mysql_fetch_object($configq))
-	$data[$configr->var]=$configr->val;
-
-$data['teams'] = array();
-$data['projects'] = array();
-$jdivisions = array();
-
-/* Load a list of all projects, and build an array of projects in each
- * category */
-// print_r($langr);
-$q=mysql_query("SELECT * FROM projects WHERE year='".$config['FAIRYEAR']."'");
 while($r=mysql_fetch_object($q)) {
-	$d_id = $r->projectdivisions_id;
-	$c_id = $r->projectcategories_id;
-	$l_id = $r->language;
-
-	$data['projects'][$r->id]['timetable'] = array();
-	
-
-	if(!in_array($l_id, $langr)) $l_id = 'en';
-
-	$jdivisions[$d_id][$c_id][$l_id][] = $r->id;
-
-	TRACE("Found project id {$r->id} in $d_id, $c_id, $l_id\n\n");
+	$langr[$r->lang] = $r->langname;
+	TRACE("   {$r->lang} - {$r->langname}\n");
 }
 
+$jdiv = array();
+TRACE("Loading Judging Division Configuration and Projects...\n");
+$q=mysql_query("SELECT * FROM judges_jdiv");
+while($r=mysql_fetch_object($q)) {
+	$jdiv[$r->jdiv_id]['config'][] = array('div' => $r->projectdivisions_id,
+					'cat' => $r->projectcategories_id,
+					'lang' => $r->lang);
+}
 
-$t=0;
-$max_ts = 0;
-foreach($div AS $d_id=>$d_val)
-{
-	foreach($cat AS $c_id=>$c_val)
-	{
-		//only make the teams for the ones that are needed
-		if($config['filterdivisionbycategory']=="yes")
-		{
-			if(!in_array($d_id,$catdiv[$c_id]))
-				continue;
+$keys = array_keys($jdiv);
+for($k=0; $k 0) TRACE("\t- ");
+		TRACE($cat[$d['cat']]." ".$div[$d['div']]." - ".$langr[$d['lang']]);
+		$qp = mysql_query("SELECT * FROM projects WHERE ".
+					" year='".$config['FAIRYEAR']."' AND ".
+					" projectdivisions_id='{$d['div']}' AND ".
+					" projectcategories_id='{$d['cat']}' AND ".
+					" language='{$d['lang']}' "
+				);
+		$count = 0;
+		while($rp = mysql_fetch_object($qp)) {
+			$jdiv[$jdiv_id]['projects'][$rp->id] = array( 
+					'div' => $d['div'],
+					'cat' => $d['cat'],
+					'lang' => $d['lang']);
+			$count++;
 		}
-		foreach($langr AS $l_id) {
-			$num = count($jdivisions[$d_id][$c_id][$l_id]);
-
-			if($num <= 0) continue;
-			$numteams=ceil($num/$data['max_projects_per_team']*$data['num_times_judged']);
-
-			if($numteams < $data['num_times_judged']) 
-				$numteams = $data['num_times_judged'];
-			TRACE("Judging teams for $d_id, $c_id, $l_id is $numteams\n\n");
-
-			$start_t = $t;
-			for($x=0; $x<$numteams; $x++) {
-				$team[$t]['division'] = $d_id;
-				$team[$t]['category'] = $c_id;
-				$team[$t]['language'] = $l_id;
-				$team[$t]['min'] = $data['min_judges_per_team'];
-				$team[$t]['max'] = $data['max_judges_per_team'];
-				$team[$t]['judges'] = array();
-				$t++;
-			}
-
-			TRACE("Created judging teams $start_t -> ".($t - 1)."\n\n");
-			
-			TRACE("Need to assign these teams to $num projects: ");
-//			print_r($jdivisions[$d_id][$c_id][$l_id]);
-			TRACE("\n\n");
-			/* We just created teams $start_t -> $t-1, now we can assign which projects
-			 * they judge in different timeslots */
-			/* Each project must be judged $data['num_times_judged'], and each team is 
-			 * allowed to judge $data['num_times_judged'] projects */
-			$x=0; /* Cycles over 0 -> $num */
-			$ts=1; /* Current timeslot , increment when all judging teams are assinged */
-			$j=0; /* Cycles over o0 -> $numteams */
-			$teams_at_this_ts = array();
-			$num_done = 0;
-			while(1) {
-
-				TRACE("x=$x  ");
-				if($x == $num) $x=0;
-				
-				/* Get the project id we want to look at */
-				$p = $jdivisions[$d_id][$c_id][$l_id][$x];
-				TRACE("   project=$p  \n\n");
-
-				TRACE("This project has ".(count($data['projects'][$p]['timetable']))." judging teams\n");
-
-				/* See if this project needs more judges */
-				if(count($data['projects'][$p]['timetable']) == $data['num_times_judged']) {
-					/* No, this project doesn't need any more judging teams */
-					TRACE("  This project doesn't need more teams, skipping\n\n");
-//					print_r($data['projects'][$p]['timetable']);
-					TRACE("\n\n");
-					if($data['projects'][$p]['timetable_done'] != 1) {
-						$data['projects'][$p]['timetable_done'] = 1;
-						$num_done++;
-
-						if($num_done == $num) break;
-					}
-					$x++;
-					continue;
-				}
-				
-				/* Find a judging team to assign */
-			
-				TRACE("Starting at team=$j\n\n");
-				while(1) {
-					TRACE("j=$j");
-					if($j == $numteams) $j = 0;
-					$jteam = $j + $start_t;
-
-					TRACE("   team=$jteam\n\n");
-					if(in_array($jteam, $teams_at_this_ts)) {
-						$j++;
-						continue;
-					}
-				
-					if(!in_array($jteam, $data['projects'][$p]['timetable'])) {
-						/* Add this juding team to the timetable */
-						TRACE("Project $p, timeslot $ts = judging team $jteam\n\n");
-						$data['projects'][$p]['timetable'][$ts] = $jteam;
-						$teams_at_this_ts[] = $jteam;
-						$j++;
-						break;
-					} 
-					$j++;
-					if($j==$numteams) $j=0;
-				}
-				$x++;
-				/* If we've used all the judging teams, of we've assigned judges equal
-				 * to the number of projects, it's time for the next timeslot */
-				if(count($teams_at_this_ts) == $numteams || count($teams_at_this_ts) == $num) {
-					$ts++;
-					$teams_at_this_ts  = array();
-					if($ts > $max_ts) $max_ts = $ts;
-				}
-				
-			}
-		}
-
+		TRACE(" ($count projects)\n");
 	}
 }
 
-//print_r($data['projects']);
-TRACE("Teams: ".count($team)."\n");
-//print_r($team);
+TRACE("Loading Scheduler Configuration Data...\n");
+$data = judges_scheduler_load_config();
 
-TRACE("\n");
+		
+TRACE("Computing required judging teams...\n");
+TRACE("   Each judging team may judge {$data['max_projects_per_team']} projects\n");
+TRACE("   Each project must be judged {$data['times_judged']} times\n");
+
+$keys = array_keys($jdiv);
+for($k=0; $kanneal();
+
+	for($x=0;$x<$a->num_buckets; $x++) {
+		$bkt = $a->bucket[$x];
+		TRACE("   SubTeam $x:\n");
+		$jteam[$jteam_id]['id'] = $jteam_id;
+		$jteam[$jteam_id]['projects'] = $a->bucket[$x];
+		$jteam[$jteam_id]['sub'] = $x;
+		$jteam[$jteam_id]['jdiv_id'] = $jdiv_id;
+		$jteam[$jteam_id]['divs'] = array();
+		$jteam[$jteam_id]['cats'] = array();
+		$jteam[$jteam_id]['langs'] = array();
+		$jteam[$jteam_id]['min_judges'] = $data['min_judges_per_team'];
+		$jteam[$jteam_id]['max_judges'] = $data['max_judges_per_team'];
+		
+		for($y=0;$ylanguages_lang;
 
+	$q2 = mysql_query("SELECT answer FROM question_answers WHERE ".
+				" registrations_id='{$r->id}' AND ".
+				" questions_id='$willing_chair_question_id' AND ".
+				" year='{$config['FAIRYEAR']}' ");
+ 	mysql_error();
+	$willing_chair = 'no';
+	if(mysql_num_rows($q2) == 1) {
+		$r2 = mysql_fetch_object($q2);
+		if($r2->answer == 'yes') $willing_chair = 'yes';
+	}
 
-	$judges[]=array(
+	$judges[$r->id]=array(
 	"judges_id"=>"$r->id",
 	"name"=>"$r->firstname $r->lastname",
 	"years_school"=>$r->years_school,
 	"years_regional"=>$r->years_regional,
 	"years_national"=>$r->years_national,
-	"willing_chair"=>$r->willing_chair,
+	"willing_chair"=>$willing_chair,
 	"divprefs"=>$divprefs,
 	"catprefs"=>$catprefs,
 	"languages"=>$langprefs
 	);
 }
+TRACE("Loaded ".count($judges)." judges.\n");
+$jteam[0]['max_judges'] = count($judges);
 
-//print_r($judges);
-//echo nl2br(TRACE_R($judges, true));
+$judge_ids = array_keys($judges);
+$e = $data['effort'];
+$a = new annealer(count($jteam), 25, $e, 0.98, judges_cost_function, $judge_ids);
+$a->anneal();
 
-judges_assign_anneal($div,$cat, $langr, $judges, $team, $data);
 
-//print_r( $team);
-$teamnums=array();
+/* Clean out the judging teams that were autocreated */
+TRACE("Deleting existing judging teams:");
+$q = mysql_query("SELECT * FROM judges_teams WHERE autocreate_type_id=1 AND year={$config['FAIRYEAR']}");
+while($r = mysql_fetch_object($q)) {
+	$jteam_id = $r->id;
+	print(" $jteam_id");
+	/* Clean out the judges_teams_link */
+	mysql_query("DELETE FROM judges_teams_link WHERE judges_teams_id='$jteam_id' AND year={$config['FAIRYEAR']}");
+	print mysql_error();
+	/* Awards */
+	mysql_query("DELETE FROM judges_teams_awards_link WHERE judges_teams_id='$jteam_id' AND year={$config['FAIRYEAR']}");
+	print mysql_error();
+	/* Timeslots */
+	mysql_query("DELETE FROM judges_teams_timeslots_link WHERE judges_teams_id='$jteam_id' AND year={$config['FAIRYEAR']}");
+	print mysql_error();
+	/* Timeslots projects */
+	mysql_query("DELETE FROM judges_teams_timeslots_projects_link WHERE judges_teams_id='$jteam_id' AND year={$config['FAIRYEAR']}");
+	print mysql_error();
+}
+/* Findally, delete all the autocreated judges teams */
+mysql_query("DELETE FROM judges_teams WHERE autocreate_type_id=1 AND year={$config['FAIRYEAR']}");
+print mysql_error();
+TRACE("Done.\n");
 
-echo i18n("Judging teams successfully created.  You can review the teams and modify as desired using the following links");
-echo "
"; -echo "
"; -echo "".i18n("Manage Judge Teams").""; -echo "
"; -echo "".i18n("Manage Judge Members").""; -echo "
"; -echo "
"; - -$totalcost=0; -while(list($tn, $t) = each($team)) { - //team numbers start with 0 in the annealer, but we want them to start at 1, so just tn++ here. - $tn++; - - print("Team $tn: ({$div[$t['division']]}({$t['division']}),{$cat[$t['category']]}({$t['category']})) ". - "(lang:{$t['language']} ". - "(cost:{$t['cost']} )
\n"); - $totalcost+=$t['cost']; - - if(!$teamnums[$t['division']][$t['category']]) $teamnums[$t['division']][$t['category']]=1; - else $teamnums[$t['division']][$t['category']]++; +function pr_judge(&$jt, $jid) +{ + global $judges; + $j =& $judges[$jid]; + print(" - {$j['name']} ("); + for($x=0; $x\n"); - mysql_query("INSERT INTO judges_teams_link (judges_id,judges_teams_id,captain,year) VALUES ('{$judge['judges_id']}','$team_id','{$judge['willing_chair']}','{$config['FAIRYEAR']}')"); +for($x=1;$xbucket_cost[$x]} "); + print("langs=("); + $langstr=""; + for($y=0; $ybucket[$x]; + for($y=0; $yid','$team_id','{$config['FAIRYEAR']}')"); } -} - - -$available_timeslots=array(); -$q=mysql_query("SELECT * FROM judges_timeslots WHERE year='".$config['FAIRYEAR']."' AND allowdivisional='yes' ORDER BY date,starttime"); -while($r=mysql_Fetch_object($q)) -{ - $available_timeslots[]=array("id"=>$r->id,"date"=>$r->date,"starttime"=>substr($r->starttime,0,-3),"endtime"=>substr($r->endtime,0,-3)); -} - -print("Project Timeslots:
\n"); -print(""); -//FIXME:! max_ts is sometimes bigger than it should be (by 1 or 2)! whats going on?? -for($x=0;$x<=$max_ts;$x++) { - print(""); -} -print(""); -while(list($proj_id, $projinfo) = each( $data['projects'] )) { - print(""); - $last_slot = 1; - while(list($slot,$jteam) = each ($projinfo['timetable']) ) { - while($last_slot != $slot) { - print(""); - $last_slot++; - } - print(""; - $last_slot++; - - //add the link of the judge team to the timeslot - mysql_query("INSERT INTO judges_teams_timeslots_link (judges_teams_id,judges_timeslots_id,year) VALUES ( - '".$team[$jteam]['team_id']."', - '".$available_timeslots[$slot-1]['id']."', - '".$config['FAIRYEAR']."')"); - - //now add the link of hte project to this judge team & timeslot - mysql_query("INSERT INTO judges_teams_timeslots_projects_link (judges_teams_id,judges_timeslots_id,projects_id,year) VALUES ( - '".$team[$jteam]['team_id']."', - '".$available_timeslots[$slot-1]['id']."', - '".$proj_id."', - '".$config['FAIRYEAR']."')"); } - print(""); + } -print("
Project ID".$available_timeslots[$x]['starttime']."-".$available_timeslots[$x]['endtime']."
$proj_id #".($jteam+1)); - echo "
"; - echo "team_id=".$team[$jteam]['team_id']; - echo "
"; - echo "proj_id=".$proj_id; - echo "
"; - echo "ts_id=".$available_timeslots[$slot-1]['id']; - echo "
"; - echo "
"); +print("Unused Judges:\n"); +$ids = $a->bucket[0]; +for($y=0; $y$r->id, + "date"=>$r->date, + "starttime"=>substr($r->starttime,0,-3), + "endtime"=>substr($r->endtime,0,-3)); + print(" -".$available_timeslots[$x]['starttime']." -> ". + $available_timeslots[$x]['endtime']."\n"); + $x++; +} + +TRACE("Assigning Judging Teams and Projects to Timeslots...\n"); +for($x=1;$x"; -echo "
"; -echo "
"; -echo "Total 'cost' for all teams: $totalcost"; +echo happy("Scheduler complete successfully"); send_footer(); ?> diff --git a/admin/judges_schedulerconfig.php b/admin/judges_schedulerconfig.php index dee0d83..a53d147 100644 --- a/admin/judges_schedulerconfig.php +++ b/admin/judges_schedulerconfig.php @@ -16,13 +16,15 @@ General Public License for more details. You should have received a copy of the GNU General Public License - along with this program; see the file COPYING. If not, write to + along with this pr\n"; +ogram; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ ?> << ".i18n("Back to Administration")."\n"; echo "<< ".i18n("Back to Judges")."\n"; - if($_GET['edit']) $edit=$_GET['edit']; - if($_POST['edit']) $edit=$_POST['edit']; - if($_GET['action']) $action=$_GET['action']; - if($_POST['action']) $action=$_POST['action']; - - $q=mysql_query("SELECT * FROM judges_schedulerconfig WHERE year='-1'"); - while($r=mysql_fetch_object($q)) - { - mysql_query("INSERT INTO judges_schedulerconfig (var,val,description,year) VALUES ( - '".mysql_escape_string($r->var)."', - '".mysql_escape_string($r->val)."', - '".mysql_escape_string($r->description)."', - '".$config['FAIRYEAR']."')"); - } - - if($_POST['action']=="save") - { - if($_POST['saveconfig']) - { - foreach($_POST['saveconfig'] as $key=>$val) - { - mysql_query("UPDATE judges_schedulerconfig SET val='".mysql_escape_string(stripslashes($val))."' WHERE year='".$config['FAIRYEAR']."' AND var='$key'"); - } - } - echo happy(i18n("Judges Scheduler Configuration successfully saved")); - - - } - $q=mysql_query("SELECT * FROM config WHERE year=0 ORDER BY var"); - echo "
"; - echo "\n"; - echo ""; - - echo ""; - - $q=mysql_query("SELECT * FROM judges_schedulerconfig WHERE year='".$config['FAIRYEAR']."' ORDER BY var"); - $schedulerconfig=array(); - while($r=mysql_fetch_object($q)) - { - $schedulerconfig[$r->var]=$r->val; - echo ""; - - - } - echo "


".i18n("Scheduler configuration settings for fair year %1",array($config['FAIRYEAR']))."

$r->var".i18n($r->description)."var]\" value=\"$r->val\" />
"; - echo "\n"; - echo "
"; + config_editor("JSCHEDULER_", $config['FAIRYEAR'], "var", $_SERVER['PHP_SELF']); echo "
"; + $ok = 1; - $ok=checkPrerequisites(); + echo ""; + echo "

Timeslots

- ".i18n("Timeslot Manager").""; + echo "
"; + + $timeslots = judges_scheduler_check_timeslots(); + if($timeslots > 0) { + echo happy(i18n("There are %1 timeslot(s) defined for divisional judging, good", array($timeslots))); + } else { + echo error(i18n("There are no timeslots defined for divisional judging")); + $ok = 0; + } + + echo ""; + echo "

Awards

- ".i18n("Awards Manager").""; + echo "
"; + + $missing_awards = judges_scheduler_check_awards(); + if(count($missing_awards) == 0) { + echo happy(i18n("There is a divisional award for each division, good")); + } else { + echo "
The following awards are missing:
    "; + for($x=0; $x"); + } + echo "
"; + echo error(i18n("There are missing divisional awards")); + $ok = 0; + } + + echo ""; + echo "

Divisional Judging Groupings

- ".i18n("Divisional Judging Groupings Manager").""; + echo "
"; + + $jdivs = judges_scheduler_check_jdivs(); + if($jdivs > 0) { + echo happy(i18n("There are %1 divisional groups defined for divisional judging, good", array($jdivs))); + } else { + echo error(i18n("There are no divisional groups defined for divisional judging")); + $ok = 0; + } + + echo "

Projects and Judges


"; + + $k=judges_scheduler_check_judges(); + + if(!$k) $ok=0; if($ok) { - echo i18n("Everything looks in order, we're ready to create the divisional awards judging teams. Click 'Create Divisional Awards Judging Teams' below to start the scheduler. Please be patient as it may take 20-30 seconds to find an optimal solution to the judging team assignments"); + echo i18n("Everything looks in order, we're ready to create the + divisional awards judging teams. Click 'Create Divisional Awards + Judging Teams' below to start the scheduler. Please be patient as it + may take several minutes find an good solution to the judging team + assignments."); + echo "
"; echo "
"; diff --git a/admin/judges_schedulerconfig_check.inc.php b/admin/judges_schedulerconfig_check.inc.php index a448bde..9757128 100644 --- a/admin/judges_schedulerconfig_check.inc.php +++ b/admin/judges_schedulerconfig_check.inc.php @@ -1,59 +1,119 @@ var]=$r->val; - } + return $rows; +} - echo i18n("Based on your configuration data above, here are some calculations"); - echo "
"; +function judges_scheduler_check_awards() +{ + global $config; - $totalteams=0; - $catq=mysql_query("SELECT * FROM projectcategories WHERE year='{$config['FAIRYEAR']}' ORDER BY mingrade"); - while($catr=mysql_fetch_object($catq)) - { - echo "

$catr->category

"; - echo ""; - echo ""; - if($config['filterdivisionbycategory']=="yes") - $divq=mysql_query("SELECT projectdivisions.* FROM projectdivisions,projectcategoriesdivisions_link WHERE projectdivisions.year='{$config['FAIRYEAR']}' AND projectcategoriesdivisions_link.projectcategories_id='$catr->id' AND projectcategoriesdivisions_link.projectdivisions_id=projectdivisions.id AND projectcategoriesdivisions_link.year='".$config['FAIRYEAR']."' ORDER BY id"); - else - $divq=mysql_query("SELECT * FROM projectdivisions WHERE year='{$config['FAIRYEAR']}' ORDER BY id"); - echo mysql_error(); - while($divr=mysql_fetch_object($divq)) - { - $langq=mysql_query("SELECT * FROM languages WHERE active='Y'"); - while($langr=mysql_fetch_object($langq)) - { - $numq=mysql_query("SELECT COUNT(id) AS num FROM projects WHERE projectcategories_id='$catr->id' AND projectdivisions_id='$divr->id' AND year='{$config['FAIRYEAR']}' AND language='$langr->lang'"); - $numr=mysql_fetch_object($numq); - echo ""; - echo ""; - $numteams=ceil($numr->num/$schedulerconfig['max_projects_per_team']*$schedulerconfig['num_times_judged']); - if($numteams<$schedulerconfig['num_times_judged']) $numteams=$schedulerconfig['num_times_judged']; - if($numr->num==0) $numteams=0; + $q=mysql_query("SELECT * FROM projectdivisions WHERE year='".$config['FAIRYEAR']."' ORDER BY id"); + while($r=mysql_fetch_object($q)) + $div[$r->id]=$r->division; - echo ""; - $totalteams+=$numteams; - echo ""; + $q=mysql_query("SELECT * FROM projectcategories WHERE year='".$config['FAIRYEAR']."' ORDER BY id"); + while($r=mysql_fetch_object($q)) + $cat[$r->id]=$r->category; + + $dkeys = array_keys($div); + $ckeys = array_keys($cat); + + $missing_awards = array(); + foreach($dkeys as $d) { + reset($ckeys); + foreach ($ckeys as $c) { + $q=mysql_query("SELECT award_awards.id FROM + award_awards, + award_awards_projectcategories, + award_awards_projectdivisions + WHERE + award_awards.year='{$config['FAIRYEAR']}' + AND award_awards_projectcategories.year='{$config['FAIRYEAR']}' + AND award_awards_projectdivisions.year='{$config['FAIRYEAR']}' + AND award_awards.id=award_awards_projectcategories.award_awards_id + AND award_awards.id=award_awards_projectdivisions.award_awards_id + AND award_awards_projectcategories.projectcategories_id='$c' + AND award_awards_projectdivisions.projectdivisions_id='$d' + AND award_awards.award_types_id='1' + "); + echo mysql_error(); + if(mysql_num_rows($q)!=1) { + $missing_awards[] = "{$cat[$c]} - {$div[$d]}"; } } - echo "
DivisionProjectsTeams
".i18n($divr->division)." ($langr->lang)$numr->num$numteams
"; - echo "
"; - echo "
"; } + return $missing_awards; + +} + + +function judges_scheduler_check_jdivs() +{ + global $config; + + $q=mysql_query("SELECT DISTINCT jdiv_id FROM judges_jdiv "); + $rows = mysql_num_rows($q); + + return $rows; +} + + +function judges_scheduler_check_judges() +{ + global $config; + $ok = 1; + + $data = judges_scheduler_load_config(); + + $jdiv = array(); + $q=mysql_query("SELECT * FROM judges_jdiv"); + while($r=mysql_fetch_object($q)) { + $d = $r->projectdivisions_id; + $c = $r->projectcategories_id; + $l = $r->lang; + + $qp = mysql_query("SELECT COUNT(id) as cnt FROM projects WHERE ". + " year='".$config['FAIRYEAR']."' AND ". + " projectdivisions_id='$d' AND ". + " projectcategories_id='$c' AND ". + " language='$l' " + ); + $qr = mysql_fetch_object($qp); + + $jdiv[$r->jdiv_id]['num_projects'] += $qr->cnt; + + } + + $totalteams = 0; + print("". + "". + ""); + $keys = array_keys($jdiv); + for($k=0; $k"); + } + print("
".i18n("Projects")."".i18n("Required Teams")."
Judging Division Group $jdiv_id$c$t
"); + echo ""; echo "Total judging teams required: $totalteams"; echo "
"; - $minjudges=($totalteams*$schedulerconfig['min_judges_per_team']); - $maxjudges=($totalteams*$schedulerconfig['max_judges_per_team']); + $minjudges=($totalteams*$data['min_judges_per_team']); + $maxjudges=($totalteams*$data['max_judges_per_team']); echo "Minimum number of judges required: $minjudges"; echo "
"; echo "Maximum number of judges acceptable: $maxjudges"; @@ -65,131 +125,17 @@ function checkPrerequisites() echo "Current number of registered judges: $currentjudges"; echo "
"; echo "
"; - $ok=true; if($currentjudges<$minjudges) { echo error(i18n("You do not have sufficient number of judges based on your parameters")); echo "  "; - echo "".i18n("Update Scheduler Configuration")." (".i18n("or get more judges!").")"; - $ok=false; + echo "".i18n("Update Scheduler Configuration")." (".i18n("or get more judges!").")"; + $ok=0; } else echo happy(i18n("You have a sufficient number of judges based on your parameters")); //now check if we can find a divisional award for each division and category - - - $q=mysql_query("SELECT * FROM projectdivisions WHERE year='".$config['FAIRYEAR']."' ORDER BY id"); - while($r=mysql_fetch_object($q)) - $div[$r->id]=$r->division; - - $q=mysql_query("SELECT * FROM projectcategories WHERE year='".$config['FAIRYEAR']."' ORDER BY id"); - while($r=mysql_fetch_object($q)) - $cat[$r->id]=$r->category; - - if($config['filterdivisionbycategory']=="yes") - { - $q=mysql_query("SELECT * FROM projectcategoriesdivisions_link WHERE year='".$config['FAIRYEAR']."' ORDER BY id"); - while($r=mysql_fetch_object($q)) - $catdiv[$r->projectcategories_id][]=$r->projectdivisions_id; - } - $foundawards=""; - $notfoundawards=""; - $foundteams=""; - foreach($div AS $d_id=>$d_division) - { - foreach($cat AS $c_id=>$c_category) - { - if($config['filterdivisionbycategory']=="yes") - { - if(!in_array($d_id,$catdiv[$c_id])) - continue; - } - $q=mysql_query("SELECT award_awards.id FROM - award_awards, - award_awards_projectcategories, - award_awards_projectdivisions - WHERE - award_awards.year='{$config['FAIRYEAR']}' - AND award_awards_projectcategories.year='{$config['FAIRYEAR']}' - AND award_awards_projectdivisions.year='{$config['FAIRYEAR']}' - AND award_awards.id=award_awards_projectcategories.award_awards_id - AND award_awards.id=award_awards_projectdivisions.award_awards_id - AND award_awards_projectcategories.projectcategories_id='$c_id' - AND award_awards_projectdivisions.projectdivisions_id='$d_id' - AND award_awards.award_types_id='1' - "); - echo mysql_error(); - if(mysql_num_rows($q)!=1) - { - while($r=mysql_fetch_object($q)) - print_r($r); - $notfoundawards.="$c_category - $d_division, "; - $ok=false; - } - else - { - $r=mysql_fetch_object($q); - $foundawards.="$c_category - $d_division, "; - //if we found an award thats good, but it would be bad if there's already a judges team - //that is assigned to the award, so now lets check the judges teams to see if ones assigned - //to this award. - - $q=mysql_query("SELECT judges_teams.num,judges_teams.name - FROM judges_teams, - judges_teams_awards_link - WHERE - judges_teams_awards_link.award_awards_id='$r->id' - AND judges_teams.year='".$config['FAIRYEAR']."' - AND judges_teams_awards_link.judges_teams_id=judges_teams.id"); - if(mysql_num_rows($q)) - { - $r=mysql_fetch_object($q); - $ok=false; - $foundteams.=i18n("(%1) %2, ",array($r->num,$r->name)); - } - } - } - } - $notfoundawards=substr($notfoundawards,0,-2); - $foundawards=substr($foundawards,0,-2); - $foundteams=substr($foundteams,0,-2); - - if($notfoundawards) - { - echo error(i18n("Cannot find awards for: %1. These awards must be created first",array($notfoundawards))); - echo "  "; - echo "".i18n("Awards Manager").""; - } - if($foundawards) - echo happy(i18n("Found awards for: %1",array($foundawards))); - if($foundteams) - { - echo error(i18n("Found judging teams that are currently assigned to divisional awards: %1. These teams must be removed first",array($foundteams))); - echo "  ".i18n("Judging Teams Manager").""; - } - else - echo happy(i18n("No judging teams are assigned to divisional awards (good!)")); - - //now check if we have enough timeslots available... - - $q=mysql_query("SELECT COUNT(id) AS num FROM judges_timeslots WHERE year='".$config['FAIRYEAR']."' AND allowdivisional='yes'"); - $r=mysql_fetch_object($q); - $numslots=$r->num; - - if($numslots<$schedulerconfig['max_projects_per_team']) - { - echo error(i18n("You do not have enough judging timeslots available for divisional judging. Please create more timeslots first. Available: %1, Required: %2",array($numslots,$schedulerconfig['max_projects_per_team']))); - echo "  ".i18n("Timeslot Manager").""; - $ok=false; - } - else - { - echo happy(i18n("You have sufficient timeslots available. Available: %1, Required: %2",array($numslots,$schedulerconfig['max_projects_per_team']))); - } - //asdf - - return $ok; } diff --git a/config_editor.inc.php b/config_editor.inc.php new file mode 100644 index 0000000..0e32e81 --- /dev/null +++ b/config_editor.inc.php @@ -0,0 +1,104 @@ + + Copyright (C) 2005 James Grant + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation, version 2. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; see the file COPYING. If not, write to + the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + Boston, MA 02111-1307, USA. +*/ +?> +var]['val'] = $r->val; + $var[$r->var]['desc'] = $r->description; + } + return $var; +} + +function config_editor_parse_from_http_headers($array_name) +{ + $ans = array(); + if(!is_array($_POST[$array_name])) return $ans; + + $keys = array_keys($_POST[$array_name]); + foreach($keys as $id) { + $ans[$id] = stripslashes($_POST[$array_name][$id]); + } + return $ans; +} + +/* A complete question editor. Just call it with the + * section you want to edit, a year, the array_name to use for + * POSTing and GETting the questions (so you can put more than one + * edtior on a single page), and give it $_SERVER['PHP_SELF'], because + * php_self inside this function is this file. + * FUTURE WORK: it would be nice to hide the order, and just implement + * a bunch of up/down arrows, and dynamically compute the order for + * all elements */ +function config_editor($append, $year, $array_name, $self) +{ + global $config; + + if($_POST['action']=="update") { + + $var = config_editor_parse_from_http_headers($array_name); + $varkeys = array_keys($var); + foreach($varkeys as $k) { + $val = mysql_escape_string(stripslashes($var[$k])); + $v = mysql_escape_string(stripslashes($k)); + mysql_query("UPDATE config SET val=\"$val\" WHERE var=\"$v\""); + print mysql_error(); +// echo "Saving {$v} = $val
"; + } + echo happy(i18n("Configuration Updated")); + } + + /* Load questions, then handle up and down, because with up and down we + * have to modify 2 questions to maintain the order */ + $var = config_editor_load($append, $year); + + echo "
"; + + echo ""; + + $varkeys = array_keys($var); + foreach($varkeys as $k) { + print(""); + print(""); + print(""; + } + print("
{$var[$k]['desc']}"); + print("\n"); + echo "
"); + print("\n"); + print("\n"); + + echo "
"; +} + +?> diff --git a/db/db.update.20.sql b/db/db.update.20.sql index f963795..07af0fd 100644 --- a/db/db.update.20.sql +++ b/db/db.update.20.sql @@ -13,3 +13,15 @@ CREATE TABLE `judges_jdiv` ( PRIMARY KEY (`id`) ) TYPE=MyISAM; + +-- +-- Dumping data for table `config` +-- + +INSERT INTO `config` (`var`, `val`, `description`, `year`) VALUES +('JSCHEDULER_max_projects_per_team', '7', 'The maximum number of projects that a judging team can judge.', -1), +('JSCHEDULER_times_judged', '1', 'The number of times each project must be judged by different judging teams.', -1), +('JSCHEDULER_min_judges_per_team', '3', 'The minimum number of judges that can be on a judging team.', -1), +('JSCHEDULER_max_judges_per_team', '3', 'The maximum number of judges that can be on a judging team.', -1), +('JSCHEDULER_effort', '10000', 'This number controls how long and hard the judge scheduler will look for a scheduling solution. Smaller numbers are lower effort. 100 is practically no effort, 1000 is moderate effort, 10000 is high effort. It can take several tens of minutes to run the scheduler with high effort, but it gives a very good solution.', -1); + diff --git a/sfiab.css b/sfiab.css index cc8f772..5839bc4 100644 --- a/sfiab.css +++ b/sfiab.css @@ -256,3 +256,7 @@ a.caution:hover { } +.headertable td { + vertical-align: baseline; +} +