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
This commit is contained in:
Henrik Levkowetz 2020-06-30 14:27:07 +00:00
parent 62e1a17579
commit 0a7b3be977

View file

@ -8,12 +8,17 @@ import datetime
import math
import random
import string
import time
from collections import defaultdict
from functools import lru_cache
from django.contrib.humanize.templatetags.humanize import intcomma
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
import debug # pyflakes:ignore
from ietf.person.models import Person
from ietf.meeting import models
@ -24,36 +29,56 @@ class Command(BaseCommand):
help = 'Create a meeting schedule'
def add_arguments(self, parser):
parser.add_argument('--meeting', default=None, dest='meeting',
help='Number of the meeting to generate schedule for')
parser.add_argument('-m', '--meeting', default=None,
help='the number of the meeting to generate a schedule for')
parser.add_argument('-n', '--name', default=None,
help='a name for the generated schedule')
parser.add_argument('-r', '--max-runs', type=int, dest='max_cycles',
default=OPTIMISER_MAX_CYCLES,
help='maximum optimiser runs')
def handle(self, meeting, verbosity, *args, **kwargs):
ScheduleHandler(self.stdout, meeting, verbosity).run()
def handle(self, meeting, name, max_cycles, verbosity, *args, **kwargs):
ScheduleHandler(self.stdout, meeting, name, max_cycles, verbosity).run()
class ScheduleHandler(object):
def __init__(self, stdout, meeting_number, verbosity):
def __init__(self, stdout, meeting_number, name, max_cycles, verbosity):
self.stdout = stdout
self.verbosity = verbosity
try:
self.meeting = models.Meeting.objects.get(number=meeting_number)
except models.Meeting.DoesNotExist:
raise CommandError('Unknown meeting number {}'.format(meeting_number))
self.name = name
self.max_cycles = max_cycles
if meeting_number:
try:
self.meeting = models.Meeting.objects.get(type="ietf", number=meeting_number)
except models.Meeting.DoesNotExist:
raise CommandError('Unknown meeting number {}'.format(meeting_number))
else:
self.meeting = models.Meeting.get_current_meeting()
if self.verbosity >= 1:
self.stdout.write("\nRunning automatic schedule layout for meeting IETF %s\n\n" % self.meeting.number)
self._load_meeting()
def run(self):
"""Schedule all sessions"""
beg_time = time.time()
self.schedule.fill_initial_schedule()
violations, cost = self.schedule.total_schedule_cost()
end_time = time.time()
tot_time = end_time - beg_time
if self.verbosity >= 1:
self.stdout.write('Initial schedule completed with {} violations, total cost {}'
.format(len(violations), cost))
self.schedule.optimise_schedule()
self.stdout.write('Initial schedule completed with %s violations, total cost %s, in %dm %.2fs'
% (len(violations), intcomma(cost), tot_time//60, tot_time%60))
beg_time = time.time()
runs = self.schedule.optimise_schedule()
violations, cost = self.schedule.total_schedule_cost()
end_time = time.time()
tot_time = end_time - beg_time
if self.verbosity >= 1:
self.stdout.write('Optimisation completed with {} violations, total cost {}'
.format(len(violations), cost))
vc = len(violations)
self.stdout.write('Optimisation completed with %s violation%s, cost %s, %s runs in %dm %.2fs'
% (vc, '' if vc==1 else 's', intcomma(cost), runs, tot_time//60, tot_time%60))
if self.verbosity >= 1 and violations:
self.stdout.write('Remaining violations:')
for v in violations:
@ -65,17 +90,22 @@ class ScheduleHandler(object):
return violations, cost
def _save_schedule(self, cost):
name = 'Auto-' + ''.join(random.choice(string.ascii_uppercase) for i in range(10))
if not self.name:
count = models.Schedule.objects.filter(name__startswith='auto-%s-'%self.meeting.number).count()
self.name = 'auto-%s-%02d' % (self.meeting.number, count)
if models.Schedule.objects.filter(name=self.name).exists():
self.stdout.write("WARNING: A schedule with the name '%s' already exists. Picking another random one." % self.name)
self.name = 'auto-%s-%s' % (self.meeting.number, ''.join(random.choice(string.ascii_lowercase) for i in range(10)))
schedule_db = models.Schedule.objects.create(
meeting=self.meeting,
name=name,
name=self.name,
owner=Person.objects.get(name='(System)'),
public=False,
visible=True,
badness=cost,
)
self.schedule.save_assignments(schedule_db)
self.stdout.write('Scheduled saved as {}'.format(name))
self.stdout.write('Schedule saved as {}'.format(self.name))
def _load_meeting(self):
"""Load all timeslots and sessions into in-memory objects."""
@ -108,7 +138,7 @@ class ScheduleHandler(object):
session.update_complexity(sessions)
self.schedule = Schedule(
self.stdout, timeslots, sessions, business_constraint_costs, self.verbosity)
self.stdout, timeslots, sessions, business_constraint_costs, self.max_cycles, self.verbosity)
self.schedule.adjust_for_timeslot_availability()
@ -118,7 +148,7 @@ class Schedule(object):
The schedule is internally represented as a dict, timeslots being keys, sessions being values.
Note that "timeslot" means the combination of a timeframe and a location.
"""
def __init__(self, stdout, timeslots, sessions, business_constraint_costs, verbosity):
def __init__(self, stdout, timeslots, sessions, business_constraint_costs, max_cycles, verbosity):
self.stdout = stdout
self.timeslots = timeslots
self.sessions = sessions
@ -129,6 +159,7 @@ class Schedule(object):
self.best_schedule = None
self.fixed_cost = 0
self.fixed_violations = []
self.max_cycles = max_cycles
def save_assignments(self, schedule_db):
for timeslot, session in self.schedule.items():
@ -271,13 +302,13 @@ class Schedule(object):
shuffle_next_run = False
last_run_cost = None
for run_count in range(OPTIMISER_MAX_CYCLES):
for run_count in range(1, self.max_cycles+1):
items = list(self.schedule.items())
random.shuffle(items)
if self.verbosity >= 2:
self.stdout.write('== Optimiser starting run {}, dynamic cost after last run {} =='
.format(run_count, last_run_cost))
.format(run_count, intcomma(last_run_cost)))
self.stdout.write('Dynamic violations in last optimiser run: {}'
.format(last_run_violations))
if shuffle_next_run:
@ -290,7 +321,7 @@ class Schedule(object):
if best_cost == 0:
if self.verbosity >= 2:
self.stdout.write('Optimiser found an optimal schedule')
return
return run_count
best_timeslot = None
for possible_new_slot in self.timeslots:
@ -309,13 +340,15 @@ class Schedule(object):
if last_run_cost == best_cost:
shuffle_next_run = True
last_run_violations, last_run_cost = self.calculate_dynamic_cost()
self._save_schedule()
self._save_schedule(last_run_cost)
if self.verbosity >= 2:
self.stdout.write('Optimiser did not find perfect schedule, using best schedule at dynamic cost {}'
.format(self.best_cost))
self.schedule = self.best_schedule
return run_count
def _shuffle_conflicted_sessions(self, items):
"""
Shuffle sessions that currently have conflicts.
@ -420,8 +453,7 @@ class Schedule(object):
del self.schedule[timeslot1]
return session2
def _save_schedule(self):
violations, cost = self.calculate_dynamic_cost()
def _save_schedule(self, cost):
if cost < self.best_cost:
self.best_cost = cost
self.best_schedule = self.schedule.copy()