Add new model for business logic meeting constraint costs, update existing constraint costs, and small improvements in tests
- Legacy-Id: 17894
This commit is contained in:
parent
cbcb5a2bd2
commit
c8e0a83b47
|
@ -75,6 +75,11 @@ class ScheduleHandler(object):
|
|||
|
||||
def _load_meeting(self):
|
||||
"""Load all timeslots and sessions into in-memory objects."""
|
||||
business_constraint_costs = {
|
||||
bc.slug: bc.penalty
|
||||
for bc in models.BusinessConstraint.objects.all()
|
||||
}
|
||||
|
||||
# TODO: ensure these filters are correct
|
||||
timeslots_db = models.TimeSlot.objects.filter(
|
||||
meeting=self.meeting,
|
||||
|
@ -91,13 +96,15 @@ class ScheduleHandler(object):
|
|||
schedulingevent__status_id='schedw',
|
||||
).select_related('group')
|
||||
|
||||
sessions = {Session(self.stdout, self.meeting, s, self.verbosity) for s in sessions_db}
|
||||
sessions = {Session(self.stdout, self.meeting, s, business_constraint_costs, self.verbosity)
|
||||
for s in sessions_db}
|
||||
for session in sessions:
|
||||
# The complexity of a session also depends on how many
|
||||
# sessions have declared a conflict towards this session.
|
||||
session.update_complexity(sessions)
|
||||
|
||||
self.schedule = Schedule(self.stdout, timeslots, sessions, self.verbosity)
|
||||
self.schedule = Schedule(
|
||||
self.stdout, timeslots, sessions, business_constraint_costs, self.verbosity)
|
||||
self.schedule.adjust_for_timeslot_availability()
|
||||
|
||||
|
||||
|
@ -107,10 +114,11 @@ 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, verbosity):
|
||||
def __init__(self, stdout, timeslots, sessions, business_constraint_costs, verbosity):
|
||||
self.stdout = stdout
|
||||
self.timeslots = timeslots
|
||||
self.sessions = sessions
|
||||
self.business_constraint_costs = business_constraint_costs
|
||||
self.verbosity = verbosity
|
||||
self.schedule = dict()
|
||||
self.best_cost = math.inf
|
||||
|
@ -154,7 +162,7 @@ class Schedule(object):
|
|||
msg = f.format(t_attr, session.group, getattr(session, s_attr), largest_available)
|
||||
setattr(session, s_attr, largest_available)
|
||||
availables.pop(-1)
|
||||
self.fixed_cost += 10000 # TODO
|
||||
self.fixed_cost += self.business_constraint_costs['session_requires_trim']
|
||||
self.fixed_violations.append(msg)
|
||||
|
||||
make_capacity_adjustments('duration', 'requested_duration')
|
||||
|
@ -437,7 +445,7 @@ class Session(object):
|
|||
i.e. it represents a single session to be scheduled. It also pulls
|
||||
in data about constraints, group parents, etc.
|
||||
"""
|
||||
def __init__(self, stdout, meeting, session_db, verbosity):
|
||||
def __init__(self, stdout, meeting, session_db, business_constraint_costs, verbosity):
|
||||
"""
|
||||
Initialise this object from a Session model instance.
|
||||
This includes collecting all constraints from the database,
|
||||
|
@ -445,6 +453,7 @@ class Session(object):
|
|||
"""
|
||||
self.stdout = stdout
|
||||
self.verbosity = verbosity
|
||||
self.business_constraint_costs = business_constraint_costs
|
||||
self.session_pk = session_db.pk
|
||||
self.group = session_db.group.acronym
|
||||
self.parent = session_db.group.parent.acronym if session_db.group.parent else None
|
||||
|
@ -604,36 +613,36 @@ class Session(object):
|
|||
if self.is_bof and other.is_prg:
|
||||
violations.append('{}: BoF overlaps with PRG: {}'
|
||||
.format(self.group, other.group))
|
||||
cost += 100000 # TODO
|
||||
cost += self.business_constraint_costs['bof_overlapping_prg']
|
||||
# BoFs cannot conflict with any other BoFs
|
||||
if self.is_bof and other.is_bof:
|
||||
violations.append('{}: BoF overlaps with other BoF: {}'
|
||||
.format(self.group, other.group))
|
||||
cost += 100000 # TODO
|
||||
cost += self.business_constraint_costs['bof_overlapping_bof']
|
||||
# BoFs cannot conflict with any other WGs in their area
|
||||
if self.is_bof and self.parent == other.parent:
|
||||
violations.append('{}: BoF overlaps with other session from same area: {}'
|
||||
.format(self.group, other.group))
|
||||
cost += 100000 # TODO
|
||||
cost += self.business_constraint_costs['bof_overlapping_area_wg']
|
||||
# BoFs cannot conflict with any area-wide meetings (of any area)
|
||||
if self.is_bof and other.is_area_meeting:
|
||||
violations.append('{}: BoF overlaps with area meeting {}'
|
||||
.format(self.group, other.group))
|
||||
cost += 100000 # TODO
|
||||
cost += self.business_constraint_costs['bof_overlapping_area_meeting']
|
||||
# Area meetings cannot conflict with anything else in their area
|
||||
if self.is_area_meeting and other.parent == self.group:
|
||||
violations.append('{}: area meeting overlaps with session from same area: {}'
|
||||
.format(self.group, other.group))
|
||||
cost += 100000 # TODO
|
||||
cost += self.business_constraint_costs['area_overlapping_in_area']
|
||||
# Area meetings cannot conflict with other area meetings
|
||||
if self.is_area_meeting and other.is_area_meeting:
|
||||
violations.append('{}: area meeting overlaps with other area meeting: {}'
|
||||
.format(self.group, other.group))
|
||||
cost += 10000 # TODO
|
||||
cost += self.business_constraint_costs['area_overlapping_other_area']
|
||||
# WGs overseen by the same Area Director should not conflict
|
||||
if self.ad and self.ad == other.ad:
|
||||
violations.append('{}: has same AD as {}'.format(self.group, other.group))
|
||||
cost += 1000 # TODO
|
||||
cost += self.business_constraint_costs['session_overlap_ad']
|
||||
return violations, cost
|
||||
|
||||
# TODO: Python 2 compatibility
|
||||
|
@ -645,7 +654,7 @@ class Session(object):
|
|||
if my_sessions != sorted(my_sessions, key=lambda i: i[1].session_pk):
|
||||
session_order = [s.session_pk for t, s in my_sessions]
|
||||
violations.append('{}: sessions out of order: {}'.format(self.group, session_order))
|
||||
cost += 10000 # TODO
|
||||
cost += self.business_constraint_costs['sessions_out_of_order']
|
||||
|
||||
if self.time_relation and len(my_sessions) >= 2:
|
||||
group_days = [t.start.date() for t, s in my_sessions]
|
||||
|
|
93
ietf/meeting/migrations/0028_businessconstraint.py
Normal file
93
ietf/meeting/migrations/0028_businessconstraint.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.27 on 2020-05-29 02:52
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
BusinessConstraint = apps.get_model("meeting", "BusinessConstraint")
|
||||
BusinessConstraint.objects.create(
|
||||
slug="bof_overlapping_prg",
|
||||
name="BoFs cannot conflict with PRGs",
|
||||
penalty=100000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="bof_overlapping_bof",
|
||||
name="BoFs cannot conflict with any other BoFs",
|
||||
penalty=100000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="bof_overlapping_area_wg",
|
||||
name="BoFs cannot conflict with any other WGs in their area",
|
||||
penalty=100000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="bof_overlapping_area_meeting",
|
||||
name="BoFs cannot conflict with any area-wide meetings (of any area)",
|
||||
penalty=10000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="area_overlapping_in_area",
|
||||
name="Area meetings cannot conflict with anything else in their area",
|
||||
penalty=10000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="area_overlapping_other_area",
|
||||
name="Area meetings cannot conflict with other area meetings",
|
||||
penalty=1000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="session_overlap_ad",
|
||||
name="WGs overseen by the same Area Director should not conflict",
|
||||
penalty=100,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="sessions_out_of_order",
|
||||
name="Sessions should be scheduled in requested order",
|
||||
penalty=100000,
|
||||
)
|
||||
BusinessConstraint.objects.create(
|
||||
slug="session_requires_trim",
|
||||
name="Sessions should be scheduled according to requested duration and attendees",
|
||||
penalty=100000,
|
||||
)
|
||||
|
||||
ConstraintName = apps.get_model("name", "ConstraintName")
|
||||
ConstraintName.objects.filter(slug='conflict').update(penalty=100000)
|
||||
ConstraintName.objects.filter(slug='conflic2').update(penalty=10000)
|
||||
ConstraintName.objects.filter(slug='conflic3').update(penalty=100000)
|
||||
ConstraintName.objects.filter(slug='bethere').update(penalty=10000)
|
||||
ConstraintName.objects.filter(slug='timerange').update(penalty=10000)
|
||||
ConstraintName.objects.filter(slug='time_relation').update(penalty=1000)
|
||||
ConstraintName.objects.filter(slug='wg_adjacent').update(penalty=1000)
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
ConstraintName = apps.get_model("name", "ConstraintName")
|
||||
ConstraintName.objects.filter(slug='conflict').update(penalty=100000)
|
||||
ConstraintName.objects.filter(slug='conflic2').update(penalty=10000)
|
||||
ConstraintName.objects.filter(slug='conflic3').update(penalty=1000)
|
||||
ConstraintName.objects.filter(slug='bethere').update(penalty=200000)
|
||||
ConstraintName.objects.filter(slug='timerange').update(penalty=100000)
|
||||
ConstraintName.objects.filter(slug='time_relation').update(penalty=100000)
|
||||
ConstraintName.objects.filter(slug='wg_adjacent').update(penalty=100000)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0027_add_constraint_options_and_joint_groups'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BusinessConstraint',
|
||||
fields=[
|
||||
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('penalty', models.IntegerField(default=0, help_text='The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(forward, reverse),
|
||||
]
|
|
@ -816,9 +816,23 @@ class SchedTimeSessAssignment(models.Model):
|
|||
|
||||
return "-".join(components).lower()
|
||||
|
||||
|
||||
class BusinessConstraint(models.Model):
|
||||
"""
|
||||
Constraints on the scheduling that apply across all qualifying
|
||||
sessions in all meetings. Used by the ScheduleGenerator, which
|
||||
expects a single BusinessConstraint to exist in the database.
|
||||
"""
|
||||
slug = models.CharField(max_length=32, primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")
|
||||
|
||||
|
||||
class Constraint(models.Model):
|
||||
"""
|
||||
Specifies a constraint on the scheduling.
|
||||
These constraints apply to a specific group during a specific meeting.
|
||||
|
||||
Available types are:
|
||||
- conflict/conflic2/conflic3: a conflict between source and target WG/session,
|
||||
with varying priority. The first is used for a chair conflict, the second for
|
||||
|
@ -828,6 +842,7 @@ class Constraint(models.Model):
|
|||
- time_relation: preference for a time difference between sessions
|
||||
- wg_adjacent: request for source WG to be adjacent (directly before or after,
|
||||
no breaks, same room) the target WG
|
||||
|
||||
"""
|
||||
TIME_RELATION_CHOICES = (
|
||||
('subsequent-days', 'Schedule the sessions on subsequent days'),
|
||||
|
|
|
@ -8,14 +8,14 @@ from django.core.management.base import CommandError
|
|||
from ietf.utils.test_utils import TestCase
|
||||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.person.factories import PersonFactory
|
||||
from ietf.meeting.models import Session, Constraint, TimerangeName
|
||||
from ietf.meeting.models import Session, Constraint, TimerangeName, BusinessConstraint
|
||||
from ietf.meeting.factories import MeetingFactory, RoomFactory, TimeSlotFactory, SessionFactory
|
||||
from ietf.meeting.management.commands.schedule_generator import ScheduleHandler
|
||||
|
||||
|
||||
class ScheduleGeneratorTest(TestCase):
|
||||
def setUp(self):
|
||||
# Create a meeting of 2 days, 4 sessions per day, in 2 rooms.
|
||||
# Create a meeting of 2 days, 5 sessions per day, in 2 rooms.
|
||||
# Two rooms is a fairly low level of simultaneous schedules, this is needed
|
||||
# because the schedule in these tests is much more complex than a real schedule.
|
||||
self.meeting = MeetingFactory(type_id='ietf', days=2)
|
||||
|
@ -27,7 +27,7 @@ class ScheduleGeneratorTest(TestCase):
|
|||
self.timeslots = []
|
||||
for room in self.rooms:
|
||||
for day in range(0, 2):
|
||||
for hour in range(12, 16):
|
||||
for hour in range(12, 17):
|
||||
t = TimeSlotFactory(
|
||||
meeting=self.meeting,
|
||||
location=room,
|
||||
|
@ -49,6 +49,8 @@ class ScheduleGeneratorTest(TestCase):
|
|||
self.bof1 = GroupFactory(acronym='bof1', parent=self.area1, state_id='bof')
|
||||
self.bof2 = GroupFactory(acronym='bof2', parent=self.area2, state_id='bof')
|
||||
self.prg1 = GroupFactory(acronym='prg1', parent=self.area2, type_id='rg', state_id='proposed')
|
||||
self.all_groups = [self.area1, self.area2, self.wg1, self.wg2, self.wg3, self.bof1,
|
||||
self.bof2, self.prg1]
|
||||
|
||||
self.ad_role = RoleFactory(group=self.wg1, name_id='ad')
|
||||
RoleFactory(group=self.bof1, name_id='ad', person=self.ad_role.person)
|
||||
|
@ -66,7 +68,7 @@ class ScheduleGeneratorTest(TestCase):
|
|||
stdout.seek(0)
|
||||
output = stdout.read()
|
||||
self.assertIn('WARNING: session wg2 (pk 13) has no attendees set', output)
|
||||
self.assertIn('scheduling 13 sessions in 16 timeslots', output)
|
||||
self.assertIn('scheduling 13 sessions in 20 timeslots', output)
|
||||
self.assertIn('Optimiser starting run 0', output)
|
||||
self.assertIn('Optimiser found an optimal schedule', output)
|
||||
|
||||
|
@ -76,10 +78,14 @@ class ScheduleGeneratorTest(TestCase):
|
|||
def test_unresolvable_schedule(self):
|
||||
stdout = StringIO()
|
||||
self._create_basic_sessions()
|
||||
self.wg2.parent = self.area1
|
||||
self.wg2.save()
|
||||
self.bof2.parent = self.area1
|
||||
self.bof2.save()
|
||||
for group in self.all_groups:
|
||||
group.parent = self.area1
|
||||
group.ad = self.ad_role
|
||||
group.save()
|
||||
c = Constraint.objects.create(meeting=self.meeting, source=group, name_id='timerange')
|
||||
c.timeranges.set(TimerangeName.objects.filter(slug__startswith=self.first_meeting_day))
|
||||
Constraint.objects.create(meeting=self.meeting, source=group,
|
||||
name_id='bethere', person=self.person1)
|
||||
|
||||
generator = ScheduleHandler(stdout, self.meeting.number, verbosity=2)
|
||||
violations, cost = generator.run()
|
||||
|
@ -105,8 +111,7 @@ class ScheduleGeneratorTest(TestCase):
|
|||
generator.run()
|
||||
|
||||
def _create_basic_sessions(self):
|
||||
for group in [self.area1, self.area2, self.wg1, self.wg2, self.wg3, self.bof1,
|
||||
self.bof2, self.prg1]:
|
||||
for group in self.all_groups:
|
||||
SessionFactory(meeting=self.meeting, group=group, add_to_schedule=False, attendees=5,
|
||||
requested_duration=datetime.timedelta(hours=1))
|
||||
for group in self.bof1, self.bof2, self.wg2:
|
||||
|
@ -139,4 +144,4 @@ class ScheduleGeneratorTest(TestCase):
|
|||
'requested 2:00:00, trimmed to 1:00:00',
|
||||
'No timeslot with sufficient capacity available for wg2, '
|
||||
'requested 500, trimmed to 100']
|
||||
self.fixed_cost = 10000 + 10000
|
||||
self.fixed_cost = BusinessConstraint.objects.get(slug='session_requires_trim').penalty * 2
|
||||
|
|
|
@ -5570,7 +5570,7 @@
|
|||
"desc": "",
|
||||
"name": "Person must be present",
|
||||
"order": 0,
|
||||
"penalty": 200000,
|
||||
"penalty": 10000,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.constraintname",
|
||||
|
@ -5592,7 +5592,7 @@
|
|||
"desc": "",
|
||||
"name": "Conflicts with (tertiary)",
|
||||
"order": 0,
|
||||
"penalty": 1000,
|
||||
"penalty": 100000,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.constraintname",
|
||||
|
@ -5614,7 +5614,7 @@
|
|||
"desc": "",
|
||||
"name": "Preference for time between sessions",
|
||||
"order": 0,
|
||||
"penalty": 100000,
|
||||
"penalty": 1000,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.constraintname",
|
||||
|
@ -5625,7 +5625,7 @@
|
|||
"desc": "",
|
||||
"name": "Can't meet within timerange",
|
||||
"order": 0,
|
||||
"penalty": 100000,
|
||||
"penalty": 10000,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.constraintname",
|
||||
|
@ -5636,7 +5636,7 @@
|
|||
"desc": "",
|
||||
"name": "Request for adjacent scheduling with another WG",
|
||||
"order": 0,
|
||||
"penalty": 100000,
|
||||
"penalty": 1000,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.constraintname",
|
||||
|
@ -14471,5 +14471,133 @@
|
|||
},
|
||||
"model": "utils.versioninfo",
|
||||
"pk": 4
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "area_overlapping_in_area",
|
||||
"fields": {
|
||||
"name": "Area meetings cannot conflict with anything else in their area",
|
||||
"penalty": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "area_overlapping_other_area",
|
||||
"fields": {
|
||||
"name": "Area meetings cannot conflict with other area meetings",
|
||||
"penalty": 500
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_area_meeting",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with any area-wide meetings (of any area)",
|
||||
"penalty": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_area_wg",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with any other WGs in their area",
|
||||
"penalty": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_bof",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with any other BoFs",
|
||||
"penalty": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_prg",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with PRGs",
|
||||
"penalty": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "session_overlap_ad",
|
||||
"fields": {
|
||||
"name": "WGs overseen by the same Area Director should not conflict",
|
||||
"penalty": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "area_overlapping_in_area",
|
||||
"fields": {
|
||||
"name": "Area meetings cannot conflict with anything else in their area",
|
||||
"penalty": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "area_overlapping_other_area",
|
||||
"fields": {
|
||||
"name": "Area meetings cannot conflict with other area meetings",
|
||||
"penalty": 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_area_meeting",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with any area-wide meetings (of any area)",
|
||||
"penalty": 10000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_area_wg",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with any other WGs in their area",
|
||||
"penalty": 100000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_bof",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with any other BoFs",
|
||||
"penalty": 100000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "bof_overlapping_prg",
|
||||
"fields": {
|
||||
"name": "BoFs cannot conflict with PRGs",
|
||||
"penalty": 100000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "sessions_out_of_order",
|
||||
"fields": {
|
||||
"name": "Sessions should be scheduled in requested order",
|
||||
"penalty": 100000
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "session_overlap_ad",
|
||||
"fields": {
|
||||
"name": "WGs overseen by the same Area Director should not conflict",
|
||||
"penalty": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "meeting.businessconstraint",
|
||||
"pk": "session_requires_trim",
|
||||
"fields": {
|
||||
"name": "Sessions should be scheduled according to requested duration and attendees",
|
||||
"penalty": 100000
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue