88import math
99import random
1010import string
11+ import time
12+
1113from collections import defaultdict
1214from functools import lru_cache
1315
16+ from django .contrib .humanize .templatetags .humanize import intcomma
1417from django .core .management .base import BaseCommand , CommandError
1518from django .db .models import Q
1619
20+ import debug # pyflakes:ignore
21+
1722from ietf .person .models import Person
1823from ietf .meeting import models
1924
@@ -24,36 +29,56 @@ class Command(BaseCommand):
2429 help = 'Create a meeting schedule'
2530
2631 def add_arguments (self , parser ):
27- parser .add_argument ('--meeting' , default = None , dest = 'meeting' ,
28- help = 'Number of the meeting to generate schedule for' )
32+ parser .add_argument ('-m' , '--meeting' , default = None ,
33+ help = 'the number of the meeting to generate a schedule for' )
34+ parser .add_argument ('-n' , '--name' , default = None ,
35+ help = 'a name for the generated schedule' )
36+ parser .add_argument ('-r' , '--max-runs' , type = int , dest = 'max_cycles' ,
37+ default = OPTIMISER_MAX_CYCLES ,
38+ help = 'maximum optimiser runs' )
2939
30- def handle (self , meeting , verbosity , * args , ** kwargs ):
31- ScheduleHandler (self .stdout , meeting , verbosity ).run ()
40+ def handle (self , meeting , name , max_cycles , verbosity , * args , ** kwargs ):
41+ ScheduleHandler (self .stdout , meeting , name , max_cycles , verbosity ).run ()
3242
3343
3444class ScheduleHandler (object ):
35- def __init__ (self , stdout , meeting_number , verbosity ):
45+ def __init__ (self , stdout , meeting_number , name , max_cycles , verbosity ):
3646 self .stdout = stdout
3747 self .verbosity = verbosity
38- try :
39- self .meeting = models .Meeting .objects .get (number = meeting_number )
40- except models .Meeting .DoesNotExist :
41- raise CommandError ('Unknown meeting number {}' .format (meeting_number ))
48+ self .name = name
49+ self .max_cycles = max_cycles
50+ if meeting_number :
51+ try :
52+ self .meeting = models .Meeting .objects .get (type = "ietf" , number = meeting_number )
53+ except models .Meeting .DoesNotExist :
54+ raise CommandError ('Unknown meeting number {}' .format (meeting_number ))
55+ else :
56+ self .meeting = models .Meeting .get_current_meeting ()
57+ if self .verbosity >= 1 :
58+ self .stdout .write ("\n Running automatic schedule layout for meeting IETF %s\n \n " % self .meeting .number )
4259 self ._load_meeting ()
4360
4461 def run (self ):
4562 """Schedule all sessions"""
63+
64+ beg_time = time .time ()
4665 self .schedule .fill_initial_schedule ()
4766 violations , cost = self .schedule .total_schedule_cost ()
67+ end_time = time .time ()
68+ tot_time = end_time - beg_time
4869 if self .verbosity >= 1 :
49- self .stdout .write ('Initial schedule completed with {} violations, total cost {}'
50- .format (len (violations ), cost ))
51-
52- self .schedule .optimise_schedule ()
70+ self .stdout .write ('Initial schedule completed with %s violations, total cost %s, in %dm %.2fs'
71+ % (len (violations ), intcomma (cost ), tot_time // 60 , tot_time % 60 ))
72+
73+ beg_time = time .time ()
74+ runs = self .schedule .optimise_schedule ()
5375 violations , cost = self .schedule .total_schedule_cost ()
76+ end_time = time .time ()
77+ tot_time = end_time - beg_time
5478 if self .verbosity >= 1 :
55- self .stdout .write ('Optimisation completed with {} violations, total cost {}'
56- .format (len (violations ), cost ))
79+ vc = len (violations )
80+ self .stdout .write ('Optimisation completed with %s violation%s, cost %s, %s runs in %dm %.2fs'
81+ % (vc , '' if vc == 1 else 's' , intcomma (cost ), runs , tot_time // 60 , tot_time % 60 ))
5782 if self .verbosity >= 1 and violations :
5883 self .stdout .write ('Remaining violations:' )
5984 for v in violations :
@@ -65,17 +90,22 @@ def run(self):
6590 return violations , cost
6691
6792 def _save_schedule (self , cost ):
68- name = 'Auto-' + '' .join (random .choice (string .ascii_uppercase ) for i in range (10 ))
93+ if not self .name :
94+ count = models .Schedule .objects .filter (name__startswith = 'auto-%s-' % self .meeting .number ).count ()
95+ self .name = 'auto-%s-%02d' % (self .meeting .number , count )
96+ if models .Schedule .objects .filter (name = self .name ).exists ():
97+ self .stdout .write ("WARNING: A schedule with the name '%s' already exists. Picking another random one." % self .name )
98+ self .name = 'auto-%s-%s' % (self .meeting .number , '' .join (random .choice (string .ascii_lowercase ) for i in range (10 )))
6999 schedule_db = models .Schedule .objects .create (
70100 meeting = self .meeting ,
71- name = name ,
101+ name = self . name ,
72102 owner = Person .objects .get (name = '(System)' ),
73103 public = False ,
74104 visible = True ,
75105 badness = cost ,
76106 )
77107 self .schedule .save_assignments (schedule_db )
78- self .stdout .write ('Scheduled saved as {}' .format (name ))
108+ self .stdout .write ('Schedule saved as {}' .format (self . name ))
79109
80110 def _load_meeting (self ):
81111 """Load all timeslots and sessions into in-memory objects."""
@@ -108,7 +138,7 @@ def _load_meeting(self):
108138 session .update_complexity (sessions )
109139
110140 self .schedule = Schedule (
111- self .stdout , timeslots , sessions , business_constraint_costs , self .verbosity )
141+ self .stdout , timeslots , sessions , business_constraint_costs , self .max_cycles , self . verbosity )
112142 self .schedule .adjust_for_timeslot_availability ()
113143
114144
@@ -118,7 +148,7 @@ class Schedule(object):
118148 The schedule is internally represented as a dict, timeslots being keys, sessions being values.
119149 Note that "timeslot" means the combination of a timeframe and a location.
120150 """
121- def __init__ (self , stdout , timeslots , sessions , business_constraint_costs , verbosity ):
151+ def __init__ (self , stdout , timeslots , sessions , business_constraint_costs , max_cycles , verbosity ):
122152 self .stdout = stdout
123153 self .timeslots = timeslots
124154 self .sessions = sessions
@@ -129,6 +159,7 @@ def __init__(self, stdout, timeslots, sessions, business_constraint_costs, verbo
129159 self .best_schedule = None
130160 self .fixed_cost = 0
131161 self .fixed_violations = []
162+ self .max_cycles = max_cycles
132163
133164 def save_assignments (self , schedule_db ):
134165 for timeslot , session in self .schedule .items ():
@@ -271,13 +302,13 @@ def optimise_schedule(self):
271302 shuffle_next_run = False
272303 last_run_cost = None
273304
274- for run_count in range (OPTIMISER_MAX_CYCLES ):
305+ for run_count in range (1 , self . max_cycles + 1 ):
275306 items = list (self .schedule .items ())
276307 random .shuffle (items )
277308
278309 if self .verbosity >= 2 :
279310 self .stdout .write ('== Optimiser starting run {}, dynamic cost after last run {} =='
280- .format (run_count , last_run_cost ))
311+ .format (run_count , intcomma ( last_run_cost ) ))
281312 self .stdout .write ('Dynamic violations in last optimiser run: {}'
282313 .format (last_run_violations ))
283314 if shuffle_next_run :
@@ -290,7 +321,7 @@ def optimise_schedule(self):
290321 if best_cost == 0 :
291322 if self .verbosity >= 2 :
292323 self .stdout .write ('Optimiser found an optimal schedule' )
293- return
324+ return run_count
294325 best_timeslot = None
295326
296327 for possible_new_slot in self .timeslots :
@@ -309,13 +340,15 @@ def optimise_schedule(self):
309340 if last_run_cost == best_cost :
310341 shuffle_next_run = True
311342 last_run_violations , last_run_cost = self .calculate_dynamic_cost ()
312- self ._save_schedule ()
343+ self ._save_schedule (last_run_cost )
313344
314345 if self .verbosity >= 2 :
315346 self .stdout .write ('Optimiser did not find perfect schedule, using best schedule at dynamic cost {}'
316347 .format (self .best_cost ))
317348 self .schedule = self .best_schedule
318349
350+ return run_count
351+
319352 def _shuffle_conflicted_sessions (self , items ):
320353 """
321354 Shuffle sessions that currently have conflicts.
@@ -420,8 +453,7 @@ def _switch_sessions(self, timeslot1, timeslot2):
420453 del self .schedule [timeslot1 ]
421454 return session2
422455
423- def _save_schedule (self ):
424- violations , cost = self .calculate_dynamic_cost ()
456+ def _save_schedule (self , cost ):
425457 if cost < self .best_cost :
426458 self .best_cost = cost
427459 self .best_schedule = self .schedule .copy ()
0 commit comments