forked from science-ation/science-ation
1641 lines
46 KiB
PHP
1641 lines
46 KiB
PHP
<?
|
|
|
|
/*
|
|
* This file is part of the 'Science Fair In A Box' project
|
|
* SFIAB Website: http://www.sfiab.ca
|
|
*
|
|
* Copyright (C) 2005-2008 Sci-Tech Ontario Inc <info@scitechontario.org>
|
|
* Copyright (C) 2008-2012 Youth Science Ontario <info@youthscienceontario.ca>
|
|
* Copyright (C) 2005-2012 James Grant <james@lightbox.org>
|
|
*
|
|
* 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.
|
|
*/
|
|
?>
|
|
<?
|
|
require_once ('../common.inc.php');
|
|
require_once ('../user.inc.php');
|
|
require_once ('../questions.inc.php');
|
|
require_once ('../projects.inc.php');
|
|
require_once ('judges.inc.php');
|
|
require_once ('anneal.inc.php');
|
|
|
|
// INFO ONLY: Re Windows OS. I have not found a test that works for both methods of starting this
|
|
// SERVER_ADDR is Always null in Windows OS IIS server
|
|
// when I launch using judges_sa_launcher_apache.php I could test using SERVER_NAME
|
|
// However when I Launch using $WshShell->run($bat_filename,0,false ); for Windows IIS it seems:
|
|
// All the $_SERVER variables are set as if were a website page so any variable I have tried will cause a bailout
|
|
// THUS.. There is no test I have found to verify this was run from the command line (or in background) for Windows
|
|
if (get_value_from_array($_SERVER, 'SERVER_ADDR')) {
|
|
echo 'This script must be run from the command line';
|
|
exit;
|
|
}
|
|
|
|
// function TRACE() { }
|
|
// function TRACE_R() { }
|
|
function TRACE($str)
|
|
{
|
|
print ($str);
|
|
}
|
|
|
|
function TRACE_R($array)
|
|
{
|
|
print_r($array);
|
|
}
|
|
|
|
TRACE('<pre>');
|
|
|
|
$round_divisional1 = NULL;
|
|
$round_divisional2 = NULL;
|
|
|
|
function set_status($txt)
|
|
{
|
|
global $pdo;
|
|
TRACE("Status: $txt\n");
|
|
$stmt = $pdo->prepare("UPDATE config SET val=? WHERE
|
|
var='judge_scheduler_activity' AND year=0");
|
|
$stmt->execute([$txt]);
|
|
}
|
|
|
|
$set_percent_last_percent = -1;
|
|
|
|
function set_percent($n)
|
|
{
|
|
global $pdo;
|
|
global $set_percent_last_percent;
|
|
$p = floor($n);
|
|
if ($p == $set_percent_last_percent)
|
|
return;
|
|
TRACE("Progress: $p\%\n");
|
|
$stmt = $pdo->prepare("UPDATE config SET val=? WHERE
|
|
var='judge_scheduler_percent' AND year=0");
|
|
$stmt->execute([$p]);
|
|
$set_percent_last_percent = $p;
|
|
}
|
|
|
|
set_status('Initializing...');
|
|
set_percent(0);
|
|
|
|
/*
|
|
* The cost function is:
|
|
* + 50 * each judge below the min for each team
|
|
* + 10 * each judge above the max for each team
|
|
* + 2 * each level of preference away from the
|
|
* max level for each judge
|
|
* + 40 if the team doesn't have a chair.
|
|
* + 25 for each memember on the team that can't speak the language
|
|
* of the judging team
|
|
*
|
|
* ( ex: if a judge has selected LS->2, PS->0, CS->-1
|
|
* then matching that judge with a:
|
|
* LS = -4,
|
|
* PS = 0,
|
|
* CS = -2,
|
|
* else = 0
|
|
* )
|
|
*/
|
|
|
|
/* Compute the cost of adding a judge to a team */
|
|
|
|
function judges_cost_function($annealer, $bucket_id, $ids)
|
|
{
|
|
global $config;
|
|
global $jteam;
|
|
global $judges, $round_divisional2;
|
|
|
|
/* 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 = false;
|
|
$have_div2 = false;
|
|
$years_experience = 0;
|
|
|
|
if ($bucket_id == 0) {
|
|
/*
|
|
* This is the placeholder for all judges, there's a slight
|
|
* cost for not using a judge
|
|
*/
|
|
$cost = count($ids) * 5;
|
|
// TRACE("Extra judge team cost=$cost\n");
|
|
return $cost;
|
|
}
|
|
|
|
$t = &$jteam[$bucket_id];
|
|
|
|
/* Compute the over max / under min costs */
|
|
$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;
|
|
|
|
// add an additional large cost above the minimum requirement cost if the team is completely empty
|
|
if ($c == 0)
|
|
$cost += 50;
|
|
|
|
// TRACE("Under min=$min, over max=$max\n");
|
|
|
|
/* For each judge on the team, score their preferences */
|
|
for ($x = 0; $x < count($ids); $x++) {
|
|
$j = &$judges[$ids[$x]];
|
|
|
|
/*
|
|
* Get the division, and see where it fits with this
|
|
* judges preferences
|
|
*/
|
|
$cpref = 0;
|
|
for ($y = 0; $y < count($t['cats']); $y++) {
|
|
$l = $t['cats'][$y];
|
|
/* Lookup the judge cat pref for this category */
|
|
$pref = -$j['cat_prefs'][$l] + 2;
|
|
/* $pref = 0 (best match) --- 4 (worst match) */
|
|
// but wait, if they're "indifferent" then we really dont care, so the cost for it shoudl be 0.
|
|
if ($pref == 2)
|
|
$pref = 0;
|
|
|
|
$cpref += $pref;
|
|
}
|
|
$dpref = 0;
|
|
for ($y = 0; $y < count($t['divs']); $y++) {
|
|
$l = $t['divs'][$y];
|
|
/* Lookup the judge cat pref for this category */
|
|
$pref = -$j['div_prefs'][$l] + 5;
|
|
/* $pref = 0 (best match) --- 4 (worst match) */
|
|
$dpref += $pref;
|
|
}
|
|
|
|
// TRACE("Judge {$ids[$x]}({$j['name']}) cp=$cpref, dp=$dpref\n");
|
|
|
|
$cost += 2 * $cpref;
|
|
// division matching is more important than category matching
|
|
$cost += 3 * $dpref;
|
|
|
|
/* See if the judge is willing to chair a team */
|
|
if ($j['willing_chair'] == 'yes')
|
|
$have_chair = true;
|
|
|
|
/*
|
|
* For each lang the team needs that the judge doesn't have,
|
|
* increase the cost
|
|
*/
|
|
for ($y = 0; $y < count($t['langs']); $y++) {
|
|
$l = $t['langs'][$y];
|
|
if (!in_array($l, $j['languages']))
|
|
$cost += 45;
|
|
}
|
|
|
|
/*
|
|
* For each additional language that the judge knows that they dont need
|
|
* increase the cost, this should hopefully stop the condition where
|
|
* it uses up all the bilingual judges for english only teams
|
|
* leaving no french/bilingual judges for the french teams
|
|
*/
|
|
$tlangs_count = count($t['langs']);
|
|
$jlangs_count = count($j['languages']);
|
|
if ($jlangs_count > $tlangs_count)
|
|
$cost += ($jlangs_count - $tlangs_count) * 15;
|
|
|
|
/*
|
|
* If divisional round2 is enabled, make sure there is a judge
|
|
* on the team for round2
|
|
*/
|
|
if ($j['available_for_divisional2'] == true)
|
|
$have_div2 = true;
|
|
|
|
/* Add up the years experience */
|
|
$years_experience += $j['years_school'] + $j['years_regional'] + $j['years_national'];
|
|
$years_experience_weighted += $j['years_school'] + $j['years_regional'] * 2 + $j['years_national'] * 4;
|
|
}
|
|
/* Huge penalty for a team without a willing chair, but only if the min judges per team >1 */
|
|
if (!$have_chair && $config['min_judges_per_team'] > 1)
|
|
$cost += 40;
|
|
|
|
/* Huge penalty for not having a round2 person on the team */
|
|
if ($round_divisional2 != NULL) {
|
|
if ($have_div2 == false)
|
|
$cost += 40;
|
|
}
|
|
|
|
/*
|
|
* Small penalty for a jteam with very little experience,
|
|
* but only if there's more than 1 person on the team
|
|
*/
|
|
if ($years_experience_weighted < 5 && count($ids) > 1) {
|
|
$cost += (5 - $years_experience_weighted) * 2;
|
|
}
|
|
|
|
// TRACE("Team $bucket_id, cost is $cost\n");
|
|
|
|
return $cost;
|
|
}
|
|
|
|
$current_jdiv = array();
|
|
|
|
function jdiv_compute_cost($annealer, $bucket_id, $ids)
|
|
{
|
|
/* IDS is a list of project ids for a judging team */
|
|
global $current_jdiv;
|
|
|
|
$cost = 0;
|
|
$t_div = array();
|
|
$t_cat = array();
|
|
$t_lang = array();
|
|
|
|
/*
|
|
* Foreach project this jteam is judging, record the
|
|
* div/cat/lang
|
|
*/
|
|
for ($x = 0; $x < count($ids); $x++) {
|
|
$proj = &$current_jdiv['projects'][$ids[$x]];
|
|
if (!in_array($proj['div'], $t_div))
|
|
$t_div[] = $proj['div'];
|
|
if (!in_array($proj['cat'], $t_cat))
|
|
$t_cat[] = $proj['cat'];
|
|
if (!in_array($proj['lang'], $t_lang))
|
|
$t_lang[] = $proj['lang'];
|
|
}
|
|
/* Square the project count for highter penalties for more projects */
|
|
$cost += floor(abs(count($ids) - $annealer->items_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;
|
|
|
|
/*
|
|
* Score +200 pts for each duplicate project this team is judging, we
|
|
* really don't want a jteam judging the same project twice
|
|
*/
|
|
for ($x = 0; $x < count($ids) - 1; $x++) {
|
|
for ($y = $x + 1; $y < count($ids); $y++) {
|
|
if ($ids[$x] == $ids[$y])
|
|
$cost += 200;
|
|
}
|
|
}
|
|
return $cost;
|
|
}
|
|
|
|
/*
|
|
* Returns true if a judge time preference indicates they are available for the
|
|
* specified round. Always returns true if judge time availablility selection
|
|
* is off
|
|
*/
|
|
function judge_available_for_round($j, $r)
|
|
{
|
|
global $config;
|
|
if ($config['judges_availability_enable'] == 'no')
|
|
return true;
|
|
|
|
foreach ($j['availability'] as $a) {
|
|
if ($a['start'] <= $r['starttime'] &&
|
|
$a['end'] >= $r['endtime'] &&
|
|
$a['date'] == $r['date']) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function judge_mark_for_round($j, $r)
|
|
{
|
|
/*
|
|
* The judge has been assigned to round $r, modify their available to
|
|
* exclude any time that falls within this time
|
|
* TODO: modify the DB to store date/times in timestamps, so we don't
|
|
* have to deal with dates separately.
|
|
*/
|
|
global $config;
|
|
global $judges;
|
|
if ($config['judges_availability_enable'] == 'no')
|
|
return true;
|
|
|
|
/*
|
|
* Grab a pointer to the real judge, because we want to
|
|
* modify it, not a copy of it
|
|
*/
|
|
$ju = &$judges[$j['id']];
|
|
|
|
$availability = &$ju['availability'];
|
|
foreach ($availability as $key => &$a) {
|
|
if ($r['starttime'] >= $a['start'] && $r['starttime'] <= $a['end']) {
|
|
/*
|
|
* Round starts in the middle of this availablity slot
|
|
* modify this availabilty so it doesn't overlap
|
|
*/
|
|
/* This may cause $a['start'] == $a['end'], that's ok */
|
|
$a['end'] = $r['starttime'];
|
|
// TRACE("adjust starttime\n");
|
|
}
|
|
|
|
if ($r['endtime'] >= $a['start'] && $r['endtime'] <= $a['end']) {
|
|
/*
|
|
* Round ends in the middle of this availablity slot
|
|
* modify this availabilty so it doesn't overlap
|
|
*/
|
|
/* This may cause $a['start'] == $a['end'], that's ok */
|
|
$a['start'] = $r['endtime'];
|
|
// TRACE("adjust endtime\n");
|
|
}
|
|
|
|
if ($a['start'] >= $a['end']) {
|
|
/* Delete the whole round */
|
|
unset($ju['availability'][$key]);
|
|
}
|
|
}
|
|
|
|
// print_r($ju['availability']);
|
|
}
|
|
|
|
/*
|
|
* UNUSED: should be moved to the timeslot manager to ensure rounds
|
|
* don't overlap.
|
|
*/
|
|
function rounds_overlap($r1, $r2)
|
|
{
|
|
$s1 = strtotime("{$r1['date']} {$r1['starttime']}");
|
|
$e1 = strtotime("{$r1['date']} {$r1['endtime']}");
|
|
$s2 = strtotime("{$r1['date']} {$r2['starttime']}");
|
|
$e2 = strtotime("{$r1['date']} {$r2['endtime']}");
|
|
|
|
if ($s1 <= $s2 && $e1 > $s1)
|
|
return true;
|
|
if ($s1 > $s2 && $s1 < $e2)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/* Print a judge */
|
|
function pr_judge(&$jt, $jid)
|
|
{
|
|
global $judges;
|
|
$j = &$judges[$jid];
|
|
print (" - {$j['name']} (" . join(' ', $j['languages']) . ')');
|
|
print ('(');
|
|
foreach ($jt['cats'] as $c)
|
|
print ("c{$c}={$j['cat_prefs'][$c]} ");
|
|
echo ' / ';
|
|
foreach ($j['cat_prefs'] AS $k => $v) {
|
|
print ("c{$k}=$v ");
|
|
}
|
|
echo ') (';
|
|
|
|
foreach ($jt['divs'] as $d)
|
|
print ("d{$d}={$j['div_prefs'][$d]} ");
|
|
|
|
echo ' / ';
|
|
foreach ($j['div_prefs'] AS $k => $v) {
|
|
print ("d{$k}=$v ");
|
|
}
|
|
|
|
print (')');
|
|
if ($j['willing_chair'] == 'yes')
|
|
print (' chair ');
|
|
|
|
print ("\n");
|
|
}
|
|
|
|
set_status('Loading Data From Database...');
|
|
TRACE("\n\n");
|
|
$div = array();
|
|
TRACE("Loading Project Divisions...\n");
|
|
$q = $pdo->prepare("SELECT * FROM projectdivisions WHERE year=? ORDER BY id");
|
|
$q->execute([$config['FAIRYEAR']]);
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
$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 = $pdo->prepare("SELECT * FROM projectcategories WHERE year=? ORDER BY id");
|
|
$q->execute([$config['FAIRYEAR']]);
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
$cat[$r->id] = $r->category;
|
|
TRACE(" {$r->id} - {$r->category}\n");
|
|
}
|
|
|
|
TRACE("Loading Languages...\n");
|
|
$langr = array();
|
|
$q = $pdo->prepare("SELECT * FROM languages WHERE active='Y'");
|
|
$q->execute();
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
$langr[$r->lang] = $r->langname;
|
|
TRACE(" {$r->lang} - {$r->langname}\n");
|
|
}
|
|
|
|
TRACE("Loading Judging Round time data...\n");
|
|
$round_special_awards = array();
|
|
$round = array();
|
|
$q = $pdo->prepare("SELECT * FROM judges_timeslots WHERE round_id='0' AND `year`=?");
|
|
$q->execute([$config['FAIRYEAR']]);
|
|
/* Loads judges_timeslots.id, .starttime, .endtime, .date, .name */
|
|
while ($r = $q->fetch(PDO::FETCH_ASSOC)) {
|
|
TRACE(" id:{$r['id']} type:{$r['type']} name:{$r['name']}\n");
|
|
|
|
$qq = $pdo->prepare("SELECT * FROM judges_timeslots WHERE round_id=?");
|
|
$qq->execute([$r['id']]);
|
|
if ($qq->rowCount() == 0) {
|
|
echo "ERROR: Round type:{$r['type']} name:{$r['name']} has no judging timeslots! Abort.\n";
|
|
exit;
|
|
}
|
|
while ($rr = $qq->fetch(PDO::FETCH_ASSOC)) {
|
|
TRACE(" Timeslot: {$rr['starttime']}-{$rr['endtime']}\n");
|
|
$r['timeslots'][] = $rr;
|
|
}
|
|
$round[] = $r;
|
|
|
|
if ($r['type'] == 'divisional1')
|
|
$round_divisional1 = $r;
|
|
if ($r['type'] == 'divisional2')
|
|
$round_divisional2 = $r;
|
|
if ($r['type'] == 'special')
|
|
$round_special_awards[] = $r;
|
|
}
|
|
|
|
if ($round_divisional1 == NULL) {
|
|
echo "No divisional1 round defined! Aborting!\n";
|
|
exit;
|
|
}
|
|
|
|
$jdiv = array();
|
|
TRACE("Loading Judging Division Configuration and Projects...\n");
|
|
$q = $pdo->prepare('SELECT * FROM judges_jdiv');
|
|
$q->execute();
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
/* Ignore jdiv 0 (all unassigned div/cats) */
|
|
if ($r->jdiv_id == 0)
|
|
continue;
|
|
|
|
$jdiv[$r->jdiv_id]['config'][] = array('div' => $r->projectdivisions_id,
|
|
'cat' => $r->projectcategories_id,
|
|
'lang' => $r->lang);
|
|
}
|
|
|
|
$keys = array_keys($jdiv);
|
|
foreach ($keys as $jdiv_id) {
|
|
TRACE(" $jdiv_id - ");
|
|
$jdiv[$jdiv_id]['projects'] = array();
|
|
for ($x = 0; $x < count($jdiv[$jdiv_id]['config']); $x++) {
|
|
$d = $jdiv[$jdiv_id]['config'][$x];
|
|
if ($x > 0)
|
|
TRACE("\t- ");
|
|
TRACE($cat[$d['cat']] . ' ' . $div[$d['div']] . ' - ' . $langr[$d['lang']]);
|
|
$qp = $pdo->prepare('SELECT projects.* FROM projects, registrations WHERE '
|
|
. " projects.year=? AND "
|
|
. " projectdivisions_id=? AND "
|
|
. " projectcategories_id=? AND "
|
|
. " language=? AND "
|
|
. ' registrations.id = projects.registrations_id '
|
|
. getJudgingEligibilityCode());
|
|
$qp->execute([$config['FAIRYEAR'],$d['div'],$d['cat'],$d['lang']]);
|
|
$count = 0;
|
|
while ($rp = $qp->fetch(PDO::FETCH_OBJ)) {
|
|
$jdiv[$jdiv_id]['projects'][$rp->id] = array(
|
|
'div' => $d['div'],
|
|
'cat' => $d['cat'],
|
|
'lang' => $d['lang']
|
|
);
|
|
$jdiv[$jdiv_id]['award_ids'] = array();
|
|
$count++;
|
|
}
|
|
TRACE(" ($count projects)\n");
|
|
}
|
|
if (count($jdiv[$jdiv_id]['projects']) == 0) {
|
|
TRACE("\t- This div has no projects, removing.\n");
|
|
unset($jdiv[$jdiv_id]);
|
|
}
|
|
}
|
|
|
|
/* Clean out the judging teams that were autocreated in a previous run */
|
|
TRACE('Deleting autocreated divisional and special award judging teams:');
|
|
$q = $pdo->prepare("SELECT * FROM judges_teams WHERE autocreate_type_id=1 AND year=?");
|
|
$q->execute([$config['FAIRYEAR']]);
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
$id = $r->id;
|
|
print (" $id");
|
|
/* Clean out the judges_teams_link */
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM judges_teams_link WHERE judges_teams_id=? AND year=?");
|
|
$stmt->execute([$id,$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
/* Awards */
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM judges_teams_awards_link WHERE judges_teams_id=? AND year=?");
|
|
$stmt->execute([$id,$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
/* Timeslots */
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM judges_teams_timeslots_link WHERE judges_teams_id=? AND year=?");
|
|
$stmt->execute([$id,$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
/* Timeslots projects */
|
|
|
|
$stmt = $pdo->prepare("DELETE FROM judges_teams_timeslots_projects_link WHERE judges_teams_id=? AND year=?");
|
|
$stmt->execute([$id,$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
}
|
|
echo "\n";
|
|
|
|
/* Finally, delete all the autocreated judges teams */
|
|
$stmt = $pdo->prepare("DELETE FROM judges_teams WHERE autocreate_type_id=1 AND year=?");
|
|
$stmt->execute([$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
|
|
/*
|
|
* Also delete any judges_teams_link that link to teams that dont exist, just
|
|
* in case
|
|
*/
|
|
$q = $pdo->prepare("SELECT judges_teams_link.id, judges_teams.id AS judges_teams_id
|
|
FROM judges_teams_link
|
|
LEFT JOIN judges_teams ON judges_teams_link.judges_teams_id=judges_teams.id
|
|
WHERE judges_teams_link.year=?");
|
|
|
|
$q->execute([$config['FAIRYEAR']]);
|
|
$n = 0;
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
if (!$r->judges_teams_id) {
|
|
$stmt = $pdo->prepare("DELETE FROM judges_teams_link WHERE id=?");
|
|
$stmt->execute([$r->id]);
|
|
$n++;
|
|
}
|
|
}
|
|
print ("Deleted $n orphaned team linkings\n");
|
|
TRACE(" Done.\n");
|
|
|
|
set_status('Loading Judges');
|
|
|
|
$judges = judges_load_all();
|
|
|
|
foreach ($judges as &$j) {
|
|
if ($j['judge_active'] == 'no') {
|
|
TRACE(" {$j['name']} has their judge profile deactivated, skipping.\n");
|
|
unset($judges[$j['id']]);
|
|
continue;
|
|
}
|
|
if ($j['judge_complete'] == 'no') {
|
|
TRACE(" {$j['name']} hasn't completed their judge profile, skipping.\n");
|
|
unset($judges[$j['id']]);
|
|
continue;
|
|
}
|
|
|
|
$q = $pdo->prepare("SELECT users_id FROM judges_teams_link WHERE
|
|
users_id=?
|
|
AND year=?");
|
|
$q->execute([$j['id'],$config['FAIRYEAR']]);
|
|
if ($q->rowCount() != 0) {
|
|
TRACE(" {$j['name']} is already on a judging team, skipping.\n");
|
|
unset($judges[$j['id']]);
|
|
continue;
|
|
}
|
|
if ($config['judges_availability_enable'] == 'yes') {
|
|
/* Load the judge time availability */
|
|
$q = $pdo->prepare("SELECT * FROM judges_availability WHERE users_id=? ORDER BY `start`");
|
|
$q->execute([$j['id']]);
|
|
if ($q->rowCount() == 0) {
|
|
TRACE(" {$j['name']} hasn't selected any time availability, POTENTIAL BUG (they shouldn't be marked as complete).\n");
|
|
TRACE(" Ignoring this judge.\n");
|
|
unset($judges[$j['id']]);
|
|
continue;
|
|
}
|
|
|
|
while ($r = $q->fetch(PDO::FETCH_ASSOC)) {
|
|
$j['availability'][] = $r;
|
|
}
|
|
}
|
|
|
|
/* Load special award preferences */
|
|
$q = $pdo->prepare("SELECT award_awards.id,award_awards.name FROM
|
|
judges_specialaward_sel,award_awards
|
|
WHERE
|
|
award_awards.id=judges_specialaward_sel.award_awards_id
|
|
AND judges_specialaward_sel.users_id=?
|
|
AND award_awards.year=?");
|
|
$q->execute([$j['id'],$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
|
|
if ($j['special_award_only'] == 'yes') {
|
|
TRACE(" {$j['name']} is a special awards only.\n");
|
|
/* Find their special award id */
|
|
if ($q->rowCount() == 0) {
|
|
TRACE(" NO special award selected! (removing special award only request)\n");
|
|
$j['special_award_only'] = 'no';
|
|
// } else if($q->rowCount()> 1) {
|
|
// TRACE(" More than ONE special award selected (removing special award only request):\n");
|
|
// $j['special_award_only'] = 'no';
|
|
}
|
|
}
|
|
|
|
$j['special_awards'] = array();
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
if ($j['special_award_only'] == 'yes') {
|
|
TRACE(" {$r->name}\n");
|
|
}
|
|
|
|
/*
|
|
* Add them to the SA judge list (modify the actual list, not
|
|
* $j, which is a copy
|
|
*/
|
|
$j['special_awards'][] = $r->id;
|
|
}
|
|
|
|
/*
|
|
* optimization, so the div1 cost function can try to find one
|
|
* round2 judge per team
|
|
*/
|
|
$j['available_for_divisional2'] = judge_available_for_round($j, $round_divisional2);
|
|
}
|
|
unset($j);
|
|
|
|
TRACE('Loaded ' . count($judges) . " judges\n");
|
|
$jteam[0]['max_judges'] = count($judges);
|
|
|
|
if (count($judges) == 0) {
|
|
echo "No judges available. Aborting!\n";
|
|
set_status('Error - no judges available...');
|
|
set_percent(0);
|
|
exit;
|
|
}
|
|
|
|
/*
|
|
* Load the numbers for any user-defined judge teams that already exist,
|
|
* these numbers will be off-limits for auto-assigning numbers
|
|
*/
|
|
$q = $pdo->prepare("SELECT * FROM judges_teams WHERE year=?");
|
|
$q->execute([$config['FAIRYEAR']]);
|
|
$used_judges_teams_numbers = array();
|
|
while ($i = $q->fetch(PDO::FETCH_ASSOC)) {
|
|
$used_judges_teams_numbers[] = $i['num'];
|
|
}
|
|
echo "The following judge team numbers are already used: \n";
|
|
print_r($used_judges_teams_numbers);
|
|
|
|
$next_judges_teams_number_try = 1;
|
|
|
|
/* A function to get the next available number */
|
|
function next_judges_teams_number()
|
|
{
|
|
global $used_judges_teams_numbers;
|
|
global $next_judges_teams_number_try;
|
|
|
|
while (1) {
|
|
if (!in_array($next_judges_teams_number_try, $used_judges_teams_numbers))
|
|
break;
|
|
|
|
$next_judges_teams_number_try++;
|
|
}
|
|
$r = $next_judges_teams_number_try;
|
|
$next_judges_teams_number_try++;
|
|
return $r;
|
|
}
|
|
|
|
function judge_team_create($num, $name)
|
|
{
|
|
global $config, $pdo;
|
|
$name = $pdo->quote($name);
|
|
$stmt = $pdo->prepare('
|
|
INSERT INTO judges_teams (num, name, autocreate_type_id, year)
|
|
VALUES (:num, :name, :autocreate_type_id, :year)
|
|
');
|
|
|
|
$stmt->bindValue(':num', $num, PDO::PARAM_INT);
|
|
$stmt->bindValue(':name', $name, PDO::PARAM_STR);
|
|
$stmt->bindValue(':autocreate_type_id', 1, PDO::PARAM_INT);
|
|
$stmt->bindValue(':year', $config['FAIRYEAR'], PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
$id = $pdo->lastInsertId();
|
|
return $id;
|
|
}
|
|
|
|
function judge_team_add_judge($team_id, $users_id)
|
|
{
|
|
global $config, $judges;
|
|
$stmt = $pdo->prepare("INSERT INTO judges_teams_link (users_id, judges_teams_id, captain, year) VALUES (?, ?, ?, ?)");
|
|
$stmt->execute([$users_id, $team_id, $judges[$users_id]['willing_chair'], $config['FAIRYEAR']]);
|
|
|
|
show_pdo_errors_if_any($pdo);
|
|
}
|
|
|
|
/*
|
|
* Round 1 Divisional Scheduling
|
|
* - Compute required divisional judge teams
|
|
* - Delete existing ones
|
|
* - Anneal Projects to Teams
|
|
* - Anneal Judtes to Projects
|
|
*/
|
|
|
|
set_status('Computing required judging teams');
|
|
TRACE(" Each judging team may judge {$config['max_projects_per_team']} projects\n");
|
|
TRACE(" Each project must be judged {$config['times_judged']} times\n");
|
|
|
|
$keys = array_keys($jdiv);
|
|
foreach ($keys as $jdiv_id) {
|
|
$c = count($jdiv[$jdiv_id]['projects']);
|
|
$t = ceil($c / $config['max_projects_per_team'] * $config['times_judged']);
|
|
if ($t < $config['times_judged'])
|
|
$t = $config['times_judged'];
|
|
TRACE(" $jdiv_id has $c projects, requires $t judging teams\n");
|
|
$jdiv[$jdiv_id]['num_jteams'] = $t;
|
|
}
|
|
|
|
$jteam = array();
|
|
$jteam_id = 0;
|
|
/* Create one more jteam, for anyone the annealer doesn't want to place */
|
|
$jteam[$jteam_id]['id'] = $jteam_id;
|
|
$jteam[$jteam_id]['projects'] = array();
|
|
$jteam[$jteam_id]['divs'] = array();
|
|
$jteam[$jteam_id]['cats'] = array();
|
|
$jteam[$jteam_id]['langs'] = array();
|
|
$jteam[$jteam_id]['min_judges'] = 0;
|
|
$jteam[$jteam_id]['max_judges'] = 0;
|
|
$jteam_id++;
|
|
|
|
set_status('Assigning projects to judging teams');
|
|
$keys = array_keys($jdiv);
|
|
for ($k = 0; $k < count($keys); $k++) {
|
|
$jdiv_id = $keys[$k];
|
|
TRACE("Judging Division $jdiv_id ({$jdiv[$jdiv_id]['num_jteams']} teams): \n");
|
|
$project_ids = array();
|
|
for ($x = 0; $x < $config['times_judged']; $x++) {
|
|
$project_ids = array_merge($project_ids, array_keys($jdiv[$jdiv_id]['projects']));
|
|
}
|
|
$current_jdiv = $jdiv[$jdiv_id];
|
|
|
|
$e = 100 + 10 * ($config['effort'] / 1000);
|
|
$a = new annealer($jdiv[$jdiv_id]['num_jteams'], 125, $e, 0.9,
|
|
'jdiv_compute_cost', $project_ids);
|
|
$a->anneal();
|
|
|
|
$jdiv[$jdiv_id]['jteams'] = array();
|
|
for ($x = 0; $x < $a->num_buckets; $x++) {
|
|
$bkt = $a->bucket[$x];
|
|
TRACE(" SubTeam $x: (jteam $jteam_id)\n");
|
|
$jdiv[$jdiv_id]['jteams'][] = $jteam_id;
|
|
|
|
$jteam[$jteam_id]['id'] = $jteam_id;
|
|
$jteam[$jteam_id]['num'] = next_judges_teams_number();
|
|
$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'] = $config['min_judges_per_team'];
|
|
$jteam[$jteam_id]['max_judges'] = $config['max_judges_per_team'];
|
|
|
|
foreach ($bkt as $projid) {
|
|
$p = $jdiv[$jdiv_id]['projects'][$projid];
|
|
TRACE(" $projid - " . $cat[$p['cat']] . ' ' . $div[$p['div']] . ' - ' . $langr[$p['lang']] . "\n");
|
|
if (!in_array($p['cat'], $jteam[$jteam_id]['cats'])) {
|
|
$jteam[$jteam_id]['cats'][] = $p['cat'];
|
|
}
|
|
if (!in_array($p['div'], $jteam[$jteam_id]['divs'])) {
|
|
$jteam[$jteam_id]['divs'][] = $p['div'];
|
|
}
|
|
if (!in_array($p['lang'], $jteam[$jteam_id]['langs'])) {
|
|
$jteam[$jteam_id]['langs'][] = $p['lang'];
|
|
}
|
|
}
|
|
$jteam_id++;
|
|
}
|
|
}
|
|
|
|
TRACE('There are ' . (count($jteam) - 1) . " judging teams\n");
|
|
|
|
TRACE("Finding judges available for round1 divisional\n");
|
|
$div1_judge_ids = array();
|
|
foreach ($judges as $j) {
|
|
if (judge_available_for_round($j, $round_divisional1) == false)
|
|
continue;
|
|
if ($j['special_award_only'] == 'yes')
|
|
continue;
|
|
|
|
/* If we get here, the judge is ok for div1 */
|
|
$div1_judge_ids[] = $j['id'];
|
|
}
|
|
|
|
TRACE(count($div1_judge_ids) . " judges available for round1 divisional\n");
|
|
|
|
function judges_to_teams_update($progress, $total)
|
|
{
|
|
set_percent(($progress * 50) / $total);
|
|
}
|
|
|
|
set_status('Assigning Judges to Teams');
|
|
|
|
$e = $config['effort'];
|
|
$a = new annealer(count($jteam), 25, $e, 0.98, 'judges_cost_function', $div1_judge_ids);
|
|
$a->set_update_callback('judges_to_teams_update');
|
|
$a->anneal();
|
|
|
|
for ($x = 1; $x < count($jteam); $x++) {
|
|
$t = &$jteam[$x];
|
|
print ("Judging Team {$t['num']}: cost={$a->bucket_cost[$x]} ");
|
|
$lang_array = $t['langs'];
|
|
asort($lang_array);
|
|
$langstr = implode(' ', $lang_array);
|
|
|
|
// sort the cats and divs too, so we dont end up with "int/sen" <--> "sen/int"
|
|
asort($t['cats']);
|
|
asort($t['divs']);
|
|
|
|
print ("langs=($langstr) ");
|
|
print ('cats=(');
|
|
$catstr = '';
|
|
|
|
if (count($t['cats'])) {
|
|
$first = true;
|
|
foreach ($t['cats'] AS $cid) {
|
|
print ('c' . $cid . ' ');
|
|
if (!$first)
|
|
$catstr .= '+';
|
|
$catstr .= $cat[$cid];
|
|
$first = false;
|
|
}
|
|
}
|
|
print (') divs=(');
|
|
$divstr = '';
|
|
if (count($t['divs'])) {
|
|
$first = true;
|
|
foreach ($t['divs'] AS $did) {
|
|
print ('d' . $did . ' ');
|
|
if (!$first)
|
|
$divstr .= '/';
|
|
$divstr .= $div[$did];
|
|
$first = false;
|
|
}
|
|
}
|
|
print (")\n");
|
|
|
|
/* Add this judging team to the database */
|
|
$tn = "$catstr $divstr ($langstr) " . ($t['sub'] + 1);
|
|
|
|
$team_id = judge_team_create($t['num'], $tn);
|
|
|
|
$t['team_id'] = $team_id;
|
|
|
|
$ids = $a->bucket[$x];
|
|
for ($y = 0; $y < count($ids); $y++) {
|
|
pr_judge($t, $ids[$y]);
|
|
$j = &$judges[$ids[$y]];
|
|
|
|
/* Mark this judge as used in round1 divisional */
|
|
judge_mark_for_round($j, $round_divisional1);
|
|
|
|
/* Add the judge to our internal team list */
|
|
$t['judge_ids'][] = $j['id'];
|
|
|
|
/* Write the SQL to do it */
|
|
judge_team_add_judge($team_id, $j['id']);
|
|
}
|
|
|
|
/*
|
|
* Get the original jdiv that this team was created from. The exact
|
|
* breakdown of each and every div/cat/lang that this team is judging
|
|
* is in the jdiv['config'] array
|
|
*/
|
|
$jd = $jdiv[$t['jdiv_id']];
|
|
for ($y = 0; $y < count($jd['config']); $y++) {
|
|
$cfg = $jd['config'][$y];
|
|
$q = $pdo->prepare("SELECT award_awards.id FROM
|
|
award_awards,
|
|
award_awards_projectcategories,
|
|
award_awards_projectdivisions
|
|
WHERE
|
|
award_awards.year=?
|
|
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=?
|
|
AND award_awards_projectdivisions.projectdivisions_id=?
|
|
AND award_awards.award_types_id='1'
|
|
");
|
|
$q->execute([$config['FAIRYEAR'],$cfg['cat'],$cfg['div']]);
|
|
if ($q->rowCount() != 1) {
|
|
echo error(i18n('Cannot find award for %1 - %2', array($cat[$cfg['cat']], $div[$cfg['div']])));
|
|
} else {
|
|
$r = $q->fetch(PDO::FETCH_OBJ);
|
|
$stmt = $pdo->prepare("INSERT INTO judges_teams_awards_link (award_awards_id,judges_teams_id,year) VALUES (?,?,?)");
|
|
$stmt->execute([$r->id,$team_id,$config['FAIRYEAR']]);
|
|
/* Add the award ID to the jdiv, if it's not already there */
|
|
if (!in_array($r->id, $jdiv[$t['jdiv_id']]['award_ids'])) {
|
|
$jdiv[$t['jdiv_id']]['award_ids'][] = $r->id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
print ("Unused Judges:\n");
|
|
$ids = $a->bucket[0];
|
|
|
|
for ($y = 0; $y < count(get_value_or_default($ids, [])); $y++) {
|
|
pr_judge($jteam[0], $ids[$y]);
|
|
}
|
|
|
|
/*
|
|
* Round 2 Divisional Scheduling
|
|
* - Find a judge on each team that is available for both rounds
|
|
* - Mark them as used
|
|
* - No annealing required
|
|
*/
|
|
|
|
if ($round_divisional2 == NULL) {
|
|
echo "No Round 2 Divisional defined, skipping.\n";
|
|
} else {
|
|
echo "Finding round2 carry-over judges:\n";
|
|
|
|
foreach ($jdiv as $jdiv_id => $jd) {
|
|
$num = next_judges_teams_number();
|
|
$team_id = judge_team_create($num, 'Round 2 Divisional ' . $jdiv_id);
|
|
|
|
TRACE("Created Round2 team id $team_id\n");
|
|
|
|
/* Find all the jteams in this jdiv */
|
|
for ($x = 1; $x < count($jteam); $x++) {
|
|
$t = &$jteam[$x];
|
|
|
|
if ($t['jdiv_id'] != $jdiv_id)
|
|
continue;
|
|
|
|
TRACE(" Round1 team #{$t['num']} ({$t['id']})\n");
|
|
|
|
$rep_id = NULL;
|
|
$chair_rep = false;
|
|
|
|
/*
|
|
* We would like the willing_chair to be the person that sticks around
|
|
* for round2, but if that's not possible, prefer anyone on the jteam be
|
|
* around for round2
|
|
*/
|
|
foreach ($t['judge_ids'] as $judge_id) {
|
|
$j = &$judges[$judge_id];
|
|
if (judge_available_for_round($j, $round_divisional2)) {
|
|
if ($j['willing_chair'] == true) {
|
|
$rep_id = $judge_id;
|
|
$chair_rep = true;
|
|
break;
|
|
} else if ($chair_rep == false) {
|
|
$rep_id = $judge_id;
|
|
}
|
|
}
|
|
}
|
|
if ($rep_id != NULL) {
|
|
pr_judge($t, $rep_id);
|
|
/* Mark this judge as used in this round */
|
|
judge_mark_for_round($judges[$rep_id], $round_divisional2);
|
|
/* Write it to the DB */
|
|
judge_team_add_judge($team_id, $rep_id);
|
|
} else {
|
|
echo "WARNING: Team $x has no carryover judge.\n";
|
|
}
|
|
}
|
|
|
|
/* Assign all the awards in this jdiv */
|
|
foreach ($jd['award_ids'] as $aid) {
|
|
$stmt = $pdo->prepare("INSERT INTO judges_teams_awards_link (award_awards_id,judges_teams_id,year) VALUES (?,?,?)");
|
|
$stmt->execute([$aid,$team_id,$config['FAIRYEAR']]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Special Awards
|
|
* - Find ALL special award rounds
|
|
* - special case: for awards with judges who are special awards
|
|
* only, that award should match their timeslot
|
|
* - Assign special awards to rounds based on needed judges
|
|
* - Assign judges to teams
|
|
*/
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* Two functions for the Special Award Annealer, if special award
|
|
* scheduling is disabled, these will never get called
|
|
*/
|
|
$current_jteam_ids = array();
|
|
|
|
function judges_sa_cost_function($annealer, $bucket_id, $ids)
|
|
{
|
|
global $sa_jteam;
|
|
global $judges, $current_jteam_ids;
|
|
|
|
/* Bucket ID is the team number */
|
|
/* ids are the judge ids currently in the bucket */
|
|
|
|
$cost = 0;
|
|
if ($bucket_id == 0) {
|
|
/* This is the placeholder */
|
|
$cost = count($ids) * 50;
|
|
return $cost;
|
|
}
|
|
|
|
$t = &$sa_jteam[$current_jteam_ids[$bucket_id]];
|
|
|
|
/* Compute the over max / under min costs */
|
|
$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;
|
|
|
|
/* For each judge on the team, score their preferences */
|
|
for ($x = 0; $x < count($ids); $x++) {
|
|
$j = &$judges[$ids[$x]];
|
|
$apref = 0;
|
|
|
|
/*
|
|
* Make sure this judge isn't on the team more than
|
|
* once. For S-A only judges, we duplicate the judge IDs
|
|
* so the judge can be scheduled on multiple teams
|
|
*/
|
|
if ($j['special_award_only'] == 'yes') {
|
|
for ($i = 0; $i < count($ids); $i++) {
|
|
if ($i == $x)
|
|
continue;
|
|
if ($ids[$i] == $ids[$x])
|
|
$cost += 1000;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* See if the sa_jteam award id (what the team is judging)
|
|
* is in the judges selection list
|
|
*/
|
|
/* Run through all awards this team is judging */
|
|
// TRACE(" - {$j['name']}\n");
|
|
foreach ($t['award_ids'] as $aid) {
|
|
if (in_array($aid, $j['special_awards'])) {
|
|
// / TRACE(" - award match\n");
|
|
/* This judge wants to judge this award */
|
|
/* No cost */
|
|
} else {
|
|
if ($j['special_award_only'] == 'yes') {
|
|
// TRACE(" - sa only mismatch\n");
|
|
|
|
/*
|
|
* This judge is for an award, but
|
|
* NOT assigned to the proper one,
|
|
* HUGE cost
|
|
*/
|
|
$cost += 500;
|
|
}
|
|
$apref++;
|
|
}
|
|
}
|
|
$cost += 5 * $apref;
|
|
}
|
|
|
|
// TRACE("Team $bucket_id, cost is $cost\n");
|
|
|
|
return $cost;
|
|
}
|
|
|
|
if ($config['scheduler_enable_sa_scheduling'] == 'yes') {
|
|
TRACE("Finding judges for special award round(s)\n");
|
|
foreach ($round_special_awards as &$r) {
|
|
$r['available_judge_ids'] = array();
|
|
}
|
|
|
|
$total_judges = 0;
|
|
foreach ($judges as &$j) {
|
|
TRACE(" {$j['firstname']} {$j['lastname']}\n");
|
|
foreach ($round_special_awards as &$r) {
|
|
if (judge_available_for_round($j, $r) == true) {
|
|
TRACE(" {$r['name']} yes\n");
|
|
$r['available_judge_ids'][] = $j['id'];
|
|
$total_judges++;
|
|
} else {
|
|
TRACE(" {$r['name']} no\n");
|
|
}
|
|
}
|
|
}
|
|
unset($j);
|
|
unset($r);
|
|
|
|
set_status('Creating Special Award Judging Teams (one team per award)');
|
|
|
|
/* Load special awards */
|
|
$q = "SELECT award_awards.name,award_awards.id FROM award_awards,award_types
|
|
WHERE
|
|
award_awards.year=?
|
|
AND award_types.id=award_awards.award_types_id
|
|
AND award_awards.schedule_judges='yes'
|
|
AND award_types.year=?
|
|
AND award_types.type='Special'
|
|
";
|
|
$r = $pdo->prepare($q);
|
|
$r->execute([$config['FAIRYEAR'],$config['FAIRYEAR']]);
|
|
print ($pdo->errorInfo());
|
|
/* sa_jteam for leftover judges, if any */
|
|
$sa_jteam = array();
|
|
$sa_jteam[0]['id'] = 0;
|
|
$sa_jteam[0]['projects'] = array();
|
|
$sa_jteam[0]['langs'] = array();
|
|
$sa_jteam[0]['min_judges'] = 0;
|
|
$sa_jteam[0]['max_judges'] = 0;
|
|
$sa_jteam[0]['award_ids'] = array();
|
|
|
|
$x = 1;
|
|
$required_judges = 0;
|
|
while ($i = $r->fetch(PDO::FETCH_OBJ)) {
|
|
$projects = getProjectsNominatedForSpecialAward($i->id);
|
|
$languages = getLanguagesOfProjectsNominatedForSpecialAward($i->id);
|
|
|
|
/*
|
|
* Construct an internal team for annealing, and create
|
|
* a DB team too
|
|
*/
|
|
$sa_jteam[$x]['num'] = next_judges_teams_number();
|
|
$sa_jteam[$x]['id'] = judge_team_create($sa_jteam[$x]['num'], $i->name . ' (' . implode(' ', $languages) . ')');
|
|
/* Note, we use $x instead of the ID, because the DB id could be zero. */
|
|
$sa_jteam[$x]['projects'] = $projects;
|
|
$sa_jteam[$x]['round'] = NULL;
|
|
$sa_jteam[$x]['sub'] = 0;
|
|
$sa_jteam[$x]['langs'] = array();
|
|
$min = floor(count($projects) / $config['projects_per_special_award_judge']) + 1;
|
|
$sa_jteam[$x]['min_judges'] = $min;
|
|
$sa_jteam[$x]['max_judges'] = $min;
|
|
$sa_jteam[$x]['award_ids'] = array($i->id);
|
|
$sa_jteam[$x]['name'] = $i->name;
|
|
|
|
$required_judges += $min;
|
|
|
|
/* Link the award to this team */
|
|
$stmt = $pdo->prepare("INSERT INTO judges_teams_awards_link (award_awards_id,judges_teams_id,year)
|
|
VALUES (?,?,?)");
|
|
$stmt->execute([$i->id,$sa_jteam[$x]['id'],$config['FAIRYEAR']]);
|
|
|
|
TRACE("Created Team: {$i->name}, " . count($projects) . " projects => $min judges needed (db id:{$sa_jteam[$x]['id']}) \n");
|
|
$x++;
|
|
}
|
|
TRACE("Total Judges: $total_judges, Required: $required_judges\n");
|
|
|
|
/* ==================================================================== */
|
|
set_status("Assigning Special Award Teams to Special Award Round(s)\n");
|
|
|
|
/*
|
|
* Compute how many judges each round needs based on the total number
|
|
* of needed judges, e.g. if SAround1 has 10 judges available and SAround2
|
|
* has 20 judges available, and we total need 90 judges, then we
|
|
* want to assign jteams so that SAround1 has 30 slots, and SAround2 has
|
|
* 60 to balance the deficit
|
|
*/
|
|
foreach ($round_special_awards as &$r) {
|
|
$x = count($r['available_judge_ids']);
|
|
$target = ($x * $required_judges) / $total_judges;
|
|
$r['target_judges'] = $target;
|
|
TRACE("Round {$r['name']} should be assigned $target judge timeslots\n");
|
|
|
|
/*
|
|
* Setup for the next step, always add special award
|
|
* judge team 0 to ALL rounds
|
|
*/
|
|
$r['jteam_ids'] = array(0);
|
|
$r['assigned_judges'] = 0;
|
|
}
|
|
unset($r);
|
|
|
|
/* ==================================================================== */
|
|
|
|
/*
|
|
* Scan the list of special awards, check each special award to see if
|
|
* it has special award only judges, we want those special awards pre-assigned
|
|
* to rounds where ALL SA-only judges are available, or, as best we can.
|
|
*/
|
|
foreach ($sa_jteam as $x => &$jt) {
|
|
if ($x == 0)
|
|
continue;
|
|
|
|
$sa_judges = array();
|
|
foreach ($round_special_awards as $i => $r) {
|
|
$sa_round_count[$i] = 0;
|
|
}
|
|
|
|
foreach ($jt['award_ids'] as $aid) {
|
|
foreach ($judges as $jid => $j) {
|
|
if ($j['special_award_only'] == 'no')
|
|
continue;
|
|
if (in_array($aid, $j['special_awards'])) {
|
|
$sa_judges[] = $jid;
|
|
foreach ($round_special_awards as $i => $r) {
|
|
// TRACE("Checking {$j['name']} in round {$r['name']}\n");
|
|
if (judge_available_for_round($j, $r)) {
|
|
// TRACE(" yes, round $i ++\n");
|
|
$sa_round_count[$i]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* If there are no SA-only judges, skip the pre-assignment */
|
|
if (count($sa_judges) == 0)
|
|
continue;
|
|
|
|
/*
|
|
* There are count($sa_judges), find the round
|
|
* with the highest count
|
|
*/
|
|
$highest_count = 0;
|
|
$highest_offset = -1;
|
|
foreach ($round_special_awards as $i => $r) {
|
|
if ($sa_round_count[$i] > $highest_count || $highest_offset == -1) {
|
|
$highest_count = $sa_round_count[$i];
|
|
$highest_offset = $i;
|
|
}
|
|
}
|
|
/* Assign this jteam to that round */
|
|
$round_special_awards[$highest_offset]['jteam_ids'][] = $x;
|
|
$round_special_awards[$highest_offset]['assigned_judges'] += $jt['min_judges'];
|
|
TRACE("Pre-assigning Team {$jt['name']} to Round {$round_special_awards[$highest_offset]['name']}\n");
|
|
$jt['assigned'] = true;
|
|
|
|
/*
|
|
* If the max judges for the jteam is less than the max, update the max,
|
|
* this prevents the scheduler from trying to remove sa-only judges
|
|
* from the jteam because of the over-max cost penalty
|
|
*/
|
|
if ($jt['max_judges'] < count($sa_judges)) {
|
|
TRACE(' Changing max_judges to ' . count($sa_judges) . " to accomodate all SA-only judge requests.\n");
|
|
$jt['max_judges'] = count($sa_judges);
|
|
}
|
|
}
|
|
unset($jt);
|
|
|
|
/*
|
|
* Use a greedy algorithm to assign the remaining jteams. First sort
|
|
* the teams by the number of judges needed so those can be assigned
|
|
* first
|
|
*/
|
|
function sa_cmp($a, $b)
|
|
{
|
|
return $b['min_judges'] - $a['min_judges'];
|
|
}
|
|
|
|
uasort($sa_jteam, 'sa_cmp');
|
|
|
|
foreach ($sa_jteam as $x => $jt) {
|
|
if ($x == 0)
|
|
continue;
|
|
if ($jt['assigned'] == true)
|
|
continue;
|
|
|
|
$highest = 0;
|
|
$highest_offset = -1;
|
|
|
|
/*
|
|
* Find the round with the highest missing judges, this works
|
|
* even if the $p computation is negative
|
|
*/
|
|
foreach ($round_special_awards as $o => $r) {
|
|
$p = $r['target_judges'] - $r['assigned_judges'];
|
|
// TRACE(" Round {$r['name']} p=$p\n");
|
|
if ($highest_offset == -1 || $p > $highest) {
|
|
$highest = $p;
|
|
$highest_offset = $o;
|
|
}
|
|
}
|
|
/* Assign this jteam id to the special award round */
|
|
$round_special_awards[$highest_offset]['jteam_ids'][] = $x;
|
|
$round_special_awards[$highest_offset]['assigned_judges'] += $jt['min_judges'];
|
|
TRACE("Assigned Team {$jt['name']} to Round {$round_special_awards[$highest_offset]['name']}\n");
|
|
}
|
|
unset($jt);
|
|
|
|
/*
|
|
* Now that teams have been assigned to rounds, search for all the
|
|
* SA only judges again, and duplicate the available judge id if they are signed
|
|
* up to judge more than one award in the round
|
|
*/
|
|
foreach ($judges as &$j) {
|
|
if ($j['special_award_only'] == 'no')
|
|
continue;
|
|
|
|
foreach ($round_special_awards as &$r) {
|
|
$count = 0;
|
|
if (judge_available_for_round($j, $r) == false)
|
|
continue;
|
|
|
|
/* Find out how many of their special awards are in this round. */
|
|
foreach ($sa_jteam as $jt_id => &$jt) {
|
|
/* Is the team in this round? */
|
|
if (!in_array($jt_id, $r['jteam_ids']))
|
|
continue;
|
|
|
|
/* Is this SA judge requsing an award judged by this team? */
|
|
foreach ($jt['award_ids'] as $aid) {
|
|
if (in_array($aid, $j['special_awards']))
|
|
$count++;
|
|
}
|
|
}
|
|
unset($jt);
|
|
while ($count > 1) {
|
|
$r['available_judge_ids'][] = $j['id'];
|
|
$count--;
|
|
TRACE(" Duplicate {$j['firstname']} {$j['lastname']} for multiple SA-only request in round {$r['name']}\n");
|
|
}
|
|
}
|
|
unset($r);
|
|
}
|
|
unset($j);
|
|
|
|
/* Now, anneal in each special award round */
|
|
foreach ($round_special_awards as $r) {
|
|
set_status("Assigning Judges in round {$r['name']}\n");
|
|
|
|
$current_jteam_ids = $r['jteam_ids'];
|
|
$judge_ids = $r['available_judge_ids'];
|
|
|
|
$e = $config['effort'];
|
|
$a = new annealer(count($r['jteam_ids']), 25, $e, 0.98,
|
|
'judges_sa_cost_function', $judge_ids);
|
|
// $a->set_update_callback(judges_to_teams_update);
|
|
// $a->set_pick_move(judges_sa_pick_move);
|
|
$a->anneal();
|
|
|
|
$x = 0;
|
|
|
|
unset($t);
|
|
unset($tid);
|
|
|
|
foreach ($r['jteam_ids'] as $tid) {
|
|
if ($tid == 0) {
|
|
$x++;
|
|
continue;
|
|
}
|
|
|
|
$t = &$sa_jteam[$tid];
|
|
|
|
print ("Judging Team {$t['id']} \"{$t['name']}\": cost={$a->bucket_cost[$x]} #=({$t['min_judges']},{$t['max_judges']}) ");
|
|
|
|
// print("langs=(");
|
|
/* $langstr="";
|
|
for($y=0; $y<count($t['langs']); $y++) {
|
|
if($y != 0) $langstr .= " ";
|
|
$langstr .= $t['langs'][$y];
|
|
}
|
|
print("$langstr)");*/
|
|
print ("\n");
|
|
|
|
/* Do timeslot and project timeslot assignment */
|
|
$stmt = $pdo->prepare("INSERT INTO judges_teams_timeslots_link
|
|
(judges_teams_id,judges_timeslots_id,year)
|
|
VALUES (?,?,?)");
|
|
$stmt->execute([$t['id'],$r['timeslots'][0]['id'],$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
|
|
foreach ($t['projects'] as $proj) {
|
|
$pid = $proj['id'];
|
|
$stmt = $pdo->prepare("INSERT INTO judges_teams_timeslots_projects_link
|
|
(judges_teams_id,judges_timeslots_id,projects_id,year)
|
|
VALUES (?,?,?,?)");
|
|
$stmt->execute([$t['id'],$r['timeslots'][0]['id'],$pid,$config['FAIRYEAR']]);
|
|
show_pdo_errors_if_any($pdo);
|
|
}
|
|
$ids = $a->bucket[$x];
|
|
foreach ($a->bucket[$x] as $jid) {
|
|
// pr_judge($t, $ids[$y]);
|
|
|
|
$j = &$judges[$jid];
|
|
print (" - {$j['name']}\n");
|
|
|
|
/* Link Judges to the judging team we just inserted */
|
|
judge_team_add_judge($t['id'], $jid);
|
|
}
|
|
$x++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Resume normal flow now */
|
|
|
|
/*
|
|
* Timeslot Scheduling
|
|
*/
|
|
|
|
/* ==================================================================== */
|
|
set_status('Assigning Judging Teams and Projects to Timeslots');
|
|
|
|
TRACE("Loading Divisional1 Timeslot Data\n");
|
|
$available_timeslots = array();
|
|
|
|
$q = $pdo->prepare("SELECT * FROM judges_timeslots WHERE
|
|
round_id=?
|
|
AND year=?
|
|
AND type='timeslot'
|
|
ORDER BY date,starttime");
|
|
$q->execute([$round_divisional1['id'],$config['FAIRYEAR']]);
|
|
$x = 0;
|
|
while ($r = $q->fetch(PDO::FETCH_OBJ)) {
|
|
$available_timeslots[] = array('id' => $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++;
|
|
}
|
|
|
|
$n_timeslots = count($available_timeslots);
|
|
|
|
/* First, check to see if the project is being judged 3 or
|
|
* more times in a row, OR, if it has large gaps that aren't
|
|
at the end of the judging */
|
|
|
|
/*
|
|
* I'm going to leave this here, for now, we shoudl do something like
|
|
* this at some point in evaluating projects, but right now
|
|
* the randomness is pretty good.
|
|
*/
|
|
/* for($x=0; $x<count($project_index); $x++) {
|
|
$i_start = $x * $n_timeslots;
|
|
$i_end = ($x+1) * $n_timeslots;
|
|
|
|
$z_count = 0;
|
|
$r_count = 0;
|
|
for($y=$i_start; $y<$i_end; $y++) {
|
|
$jteam_id = $ids[$y];
|
|
|
|
if($jteam_id == 0) {
|
|
$z_count++;
|
|
$r_count=0;
|
|
} else {*/
|
|
|
|
/*
|
|
* Do the z_count cost here so we don;t
|
|
* count any zcount cost for the end
|
|
* of the timetable
|
|
*/
|
|
|
|
/*
|
|
* if($z_count > 2) $cost += $z_count;
|
|
* $r_count++;
|
|
* $z_count=0;
|
|
* if($r_count > 2) $cost += $r_count;
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
|
|
function timeslot_pick_move($a)
|
|
{
|
|
/*
|
|
* Use the existing pick move, but we want the item numbers
|
|
* in each bucket to always be the same
|
|
*/
|
|
list($b1, $i1, $b2, $i2) = $a->pick_move();
|
|
$i2 = $i1;
|
|
return array($b1, $i1, $b2, $i2);
|
|
}
|
|
|
|
function timeslot_cost_function($annealer, $bucket_id, $ids)
|
|
{
|
|
$cost = 0;
|
|
|
|
/*
|
|
* Check to make sure a judge isn't judging two projects
|
|
* at the same time
|
|
*/
|
|
$n_pids = count($ids);
|
|
for ($x = 0; $x < $n_pids - 1; $x++) {
|
|
$jteam_id1 = $ids[$x];
|
|
if ($jteam_id1 == 0)
|
|
continue;
|
|
for ($y = $x + 1; $y < $n_pids; $y++) {
|
|
$jteam_id2 = $ids[$y];
|
|
if ($jteam_id1 == $jteam_id2)
|
|
$cost += 50;
|
|
}
|
|
}
|
|
return $cost;
|
|
}
|
|
|
|
$keys = array_keys($jdiv);
|
|
$keys_count = count($keys);
|
|
for ($k = 0; $k < $keys_count; $k++) {
|
|
$jdiv_id = $keys[$k];
|
|
|
|
$pids = array_keys($jdiv[$jdiv_id]['projects']);
|
|
$n_projects = count($pids);
|
|
|
|
if ($n_projects == 0)
|
|
continue;
|
|
|
|
unset($project_rlookup);
|
|
$project_rlookup = array();
|
|
|
|
for ($x = 0; $x < count($pids); $x++) {
|
|
$project_rlookup[$pids[$x]] = $x;
|
|
}
|
|
|
|
// $current_jdiv = $jdiv_id;
|
|
|
|
printf("jdiv $jdiv_id, $n_projects projects in this jdiv\n");
|
|
unset($jteams_ids);
|
|
$jteams_ids = array();
|
|
/* Pad to the correct length */
|
|
for ($x = 0; $x < ($n_timeslots * $n_projects); $x++)
|
|
$jteams_ids[] = 0;
|
|
|
|
printf('total of ' . count($jteams_ids) . " slots (should be $n_timeslots * $n_projects)\n");
|
|
|
|
/*
|
|
* Fill out the jteam array with a jteam_id for each time the
|
|
* jteam_id is supposed to judge a project
|
|
*/
|
|
$jteams = $jdiv[$jdiv_id]['jteams'];
|
|
|
|
foreach ($jteams as $jteam_id) {
|
|
for ($y = 0; $y < count($jteam[$jteam_id]['projects']); $y++) {
|
|
$pid = $jteam[$jteam_id]['projects'][$y];
|
|
$idx = $project_rlookup[$pid];
|
|
|
|
for ($o = $idx;; $o += $n_projects) {
|
|
if ($jteams_ids[$o] != 0)
|
|
continue;
|
|
|
|
$jteams_ids[$o] = $jteam_id;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* $y = 0;
|
|
* foreach($jteams as $jteam_id) {
|
|
* $o = 0;
|
|
* print("setting up jteam $jteam_id\n");
|
|
* print_r($jteam[$jteam_id]);
|
|
* foreach($jteam[$jteam_id]['projects'] as $pid) {
|
|
* $jteams_ids[$y * $n_timeslots + $o] = $jteam_id;
|
|
* $o++;
|
|
* }
|
|
* $y++;
|
|
* }
|
|
* printf("jteams_ids=\n");
|
|
* print_r($jteams_ids);
|
|
*/
|
|
print ('Jteams ids len=' . count($jteams_ids));
|
|
print ("n_timeslots=$n_timeslots\n");
|
|
|
|
set_percent(50 + ($k / $keys_count) * 50);
|
|
|
|
$e = 500 + 50 * ($config['effort'] / 1000);
|
|
$a = new annealer($n_timeslots, 100, $e, 0.98, 'timeslot_cost_function', $jteams_ids);
|
|
$a->set_pick_move('timeslot_pick_move');
|
|
$a->anneal();
|
|
|
|
printf(' ');
|
|
for ($y = 0; $y < $n_timeslots; $y++) {
|
|
printf('%4d ', $y + 1);
|
|
}
|
|
printf("\n");
|
|
|
|
for ($x = 0; $x < count($pids); $x++) {
|
|
$pid = $pids[$x];
|
|
printf('Project %4d: ', $pid);
|
|
|
|
for ($y = 0; $y < $n_timeslots; $y++) {
|
|
$jteam_id = $a->bucket[$y][$x];
|
|
printf('%4d ', $jteam[$jteam_id]['id']);
|
|
|
|
if ($jteam_id == 0)
|
|
continue;
|
|
|
|
/* if jteam_id isn't 0, instert it into the db */
|
|
$stmt = $pdo->prepare('INSERT INTO judges_teams_timeslots_link (judges_teams_id,judges_timeslots_id,year) VALUES (?,?,?)');
|
|
$stmt->execute([$jteam[$jteam_id]['team_id'],$available_timeslots[$y]['id'],$config['FAIRYEAR']]);
|
|
|
|
$stmt = $pdo->prepare('INSERT INTO judges_teams_timeslots_projects_link (judges_teams_id,judges_timeslots_id,projects_id,year) VALUES (?,?,?,?)');
|
|
$stmt->execute([$jteam[$jteam_id]['team_id'],$available_timeslots[$y]['id'],$pid,$config['FAIRYEAR']]);
|
|
}
|
|
printf("\n");
|
|
}
|
|
}
|
|
|
|
TRACE("All Done.\n");
|
|
echo '</pre>';
|
|
|
|
set_percent(-1);
|
|
set_status('Done');
|
|
|
|
// echo happy("Scheduler completed successfully");
|
|
|
|
// send_footer();
|
|
?>
|