Skip to content

Commit 0a7b3be

Browse files
committed
Renamed the automatic schedule generator command from 'schedule_generator' to 'generate_schedule', to match our other 'generate_*' management commands. Added some switches to the command, and added more information to the default output, providing information about number of optimisation runs and time consumed. Added a default value for the meeting option. Made it possible to provide a name to use for the generated schedule, and changed the autogenerated names to be easier to remember and type for later manual work based on an autogenerated schedule.
- Legacy-Id: 18106
1 parent 62e1a17 commit 0a7b3be

1 file changed

Lines changed: 58 additions & 26 deletions

File tree

ietf/meeting/management/commands/schedule_generator.py renamed to ietf/meeting/management/commands/generate_schedule.py

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@
88
import math
99
import random
1010
import string
11+
import time
12+
1113
from collections import defaultdict
1214
from functools import lru_cache
1315

16+
from django.contrib.humanize.templatetags.humanize import intcomma
1417
from django.core.management.base import BaseCommand, CommandError
1518
from django.db.models import Q
1619

20+
import debug # pyflakes:ignore
21+
1722
from ietf.person.models import Person
1823
from 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

3444
class 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("\nRunning 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

Comments
 (0)