Skip to content

Commit ed5a9a7

Browse files
authored
Merge pull request #102 from drvinceknight/refactor-tag-constraint
Make tag an unavailability shortcut
2 parents 9742b95 + ec84e99 commit ed5a9a7

File tree

8 files changed

+92
-208
lines changed

8 files changed

+92
-208
lines changed

docs/background/mathematical_model.rst

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -120,45 +120,9 @@ Using this we have the following constraint:
120120
We see that if :math:`{C_{e}}_{ii'}=0` then at most one of the two events can be
121121
scheduled across the two slots :math:`j,j'`.
122122

123-
Talks in a given session have something in common
124-
-------------------------------------------------
125-
126-
It might be desirable to schedule collection of time slots in such a way that
127-
the events in that collection have something in common. Perhaps all talks in a
128-
morning session in a particular room should be welcoming to delegates of a given
129-
level of expertise.
130-
131-
To do this we first need to capture each collection of slots into sessions, and
132-
we define the following set for every slot :math:`j`:
133-
134-
.. math::
135-
:label: same_session_set
136-
137-
{K}_{j} = \{1\leq j' \leq N\,|\,\text{ if }j\text{ and }j'\text{ are in the same session}\}
138-
139-
We also assume that we have a number of collections of events. Note that these
140-
collections are non disjoint: any event can be in multiple collections. We refer
141-
to these collections as "tags": an event can for example be tagged as
142-
"beginner".
143-
144-
Using this we define the following set for every event :math:`i`
145-
146-
.. math::
147-
:label: same_tag_event_set
148-
149-
T_i = \{1\leq i'\leq M\,|\,\text{ if }i\text{ and }j\text{ do not share a tag}\}
150-
151-
This leads us to the following constraint:
152-
153-
.. math::
154-
:label: tag_constraint
155-
156-
X_{ij} + X_{i'j'} \leq 1 \text{ for all }j'\in K_j\text{ for all }1\leq j\leq N\text{ for all }i'\in T_i\text{ for all }1\leq i\leq M
157-
158-
159123
Expressions :eq:`all_events_scheduled_constraint`,
160124
:eq:`all_slots_at_most_1_event_constraint`,
161-
:eq:`slot_constraint`, :eq:`event_constraint` and :eq:`tag_constraint` define a
125+
:eq:`slot_constraint`, and :eq:`event_constraint` define a
162126
valid schedule and can be used by themselves.
163127

164128
However, it might be desirable to also optimise a given objective function.
@@ -186,15 +150,15 @@ solving the following problem:
186150

187151
Minimise :eq:`overflow_objective_function` subject to :eq:`all_events_scheduled_constraint`,
188152
:eq:`all_slots_at_most_1_event_constraint`,
189-
:eq:`slot_constraint`, :eq:`event_constraint` and :eq:`tag_constraint`.
153+
:eq:`slot_constraint` and :eq:`event_constraint`.
190154

191155
Minimise change from a previous schedule
192156
----------------------------------------
193157

194158
Once a schedule has been obtained and publicised to all delegates, a new
195159
constraint might arise (modifying :eq:`all_events_scheduled_constraint`,
196160
:eq:`all_slots_at_most_1_event_constraint`,
197-
:eq:`slot_constraint`, :eq:`event_constraint` and :eq:`tag_constraint`). At this
161+
:eq:`slot_constraint` and :eq:`event_constraint`. At this
198162
point the original optimisation problem can be solved again leading to a
199163
potentially completely different schedule. An alternative to this is to use
200164
distance from an original schedule :math:`X_o` as the objective function. Norms
@@ -225,4 +189,4 @@ problem:
225189
Minimise :eq:`number_of_changes_objective_function` subject to
226190
:eq:`all_events_scheduled_constraint`,
227191
:eq:`all_slots_at_most_1_event_constraint`, :eq:`slot_constraint`,
228-
:eq:`event_constraint` and :eq:`tag_constraint`.
192+
and :eq:`event_constraint`.

docs/tutorial/index.rst

Lines changed: 70 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ duration/location of the slots we know some of them are unavailable for a given
6565

6666
>>> events = [Event(name='Talk 1', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=50),
6767
... Event(name='Talk 2', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=130),
68-
... Event(name='Talk 3', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=500),
68+
... Event(name='Talk 3', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=3000),
6969
... Event(name='Talk 4', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=30),
7070
... Event(name='Talk 5', duration=30, tags=['intermediate'], unavailability=outside_slots[:], demand=60),
7171
... Event(name='Talk 6', duration=30, tags=['intermediate'], unavailability=outside_slots[:], demand=30),
@@ -107,20 +107,20 @@ event::
107107
>>> schedule.sort(key=lambda item: item.slot.starts_at)
108108
>>> for item in schedule:
109109
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
110-
Talk 3 at 15-Sep-2016 09:30 in Small
110+
Talk 5 at 15-Sep-2016 09:30 in Small
111111
Talk 11 at 15-Sep-2016 09:30 in Big
112112
Talk 4 at 15-Sep-2016 10:00 in Small
113-
Talk 8 at 15-Sep-2016 10:00 in Big
113+
Talk 10 at 15-Sep-2016 10:00 in Big
114114
Talk 1 at 15-Sep-2016 12:30 in Small
115-
Talk 5 at 15-Sep-2016 12:30 in Big
116-
Talk 2 at 15-Sep-2016 13:00 in Small
117-
Talk 6 at 15-Sep-2016 13:00 in Big
118-
Talk 9 at 16-Sep-2016 09:30 in Big
115+
Talk 6 at 15-Sep-2016 12:30 in Big
116+
Talk 3 at 15-Sep-2016 13:00 in Small
117+
Talk 8 at 15-Sep-2016 13:00 in Big
118+
Talk 2 at 16-Sep-2016 09:30 in Big
119119
Workshop 2 at 16-Sep-2016 09:30 in Small
120-
Talk 10 at 16-Sep-2016 10:00 in Big
121-
Talk 7 at 16-Sep-2016 12:30 in Big
120+
Talk 9 at 16-Sep-2016 10:00 in Big
121+
Talk 12 at 16-Sep-2016 12:30 in Big
122122
Boardgames at 16-Sep-2016 12:30 in Outside
123-
Talk 12 at 16-Sep-2016 13:00 in Big
123+
Talk 7 at 16-Sep-2016 13:00 in Big
124124
Workshop 1 at 16-Sep-2016 13:00 in Small
125125
City tour at 16-Sep-2016 13:00 in Outside
126126

@@ -129,10 +129,8 @@ the unavailability attribute for the events). Also we have that :code:`Talk 1`
129129
doesn't clash with :code:`Workshop 1`.
130130
Similarly, the :code:`Boardgame` does not clash with :code:`Workshop 2`.
131131

132-
You will also note that in any given session, talks share at least one tag. This
133-
is another constraint of the model; if you find that your schedule has no
134-
solutions you can adjust it by re-categorising your talks (or giving them all a
135-
single category).
132+
You will also note that no two events with the same tags are on at the same time.
133+
Tags allow for a quick way to batch define unavailability.
136134

137135
Avoiding room overcrowding
138136
--------------------------
@@ -150,18 +148,18 @@ our scheduler to minimise the difference between room capacity and demand::
150148
>>> schedule.sort(key=lambda item: item.slot.starts_at)
151149
>>> for item in schedule:
152150
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
153-
Talk 4 at 15-Sep-2016 09:30 in Big
154-
Talk 7 at 15-Sep-2016 09:30 in Small
155-
Talk 1 at 15-Sep-2016 10:00 in Big
151+
Talk 1 at 15-Sep-2016 09:30 in Big
152+
Talk 12 at 15-Sep-2016 09:30 in Small
153+
Talk 2 at 15-Sep-2016 10:00 in Big
156154
Talk 6 at 15-Sep-2016 10:00 in Small
157-
Talk 8 at 15-Sep-2016 12:30 in Big
158-
Talk 12 at 15-Sep-2016 12:30 in Small
159-
Talk 5 at 15-Sep-2016 13:00 in Big
160-
Talk 10 at 15-Sep-2016 13:00 in Small
161-
Talk 3 at 16-Sep-2016 09:30 in Big
155+
Talk 4 at 15-Sep-2016 12:30 in Small
156+
Talk 11 at 15-Sep-2016 12:30 in Big
157+
Talk 5 at 15-Sep-2016 13:00 in Small
158+
Talk 10 at 15-Sep-2016 13:00 in Big
159+
Talk 7 at 16-Sep-2016 09:30 in Big
162160
Workshop 2 at 16-Sep-2016 09:30 in Small
163-
Talk 2 at 16-Sep-2016 10:00 in Big
164-
Talk 11 at 16-Sep-2016 12:30 in Big
161+
Talk 8 at 16-Sep-2016 10:00 in Big
162+
Talk 3 at 16-Sep-2016 12:30 in Big
165163
Boardgames at 16-Sep-2016 12:30 in Outside
166164
Talk 9 at 16-Sep-2016 13:00 in Big
167165
Workshop 1 at 16-Sep-2016 13:00 in Small
@@ -181,7 +179,7 @@ informs us of a particular new constraints. For example, the speaker for
181179

182180
We can enter this new constraint::
183181

184-
>>> events[10].add_unavailability(*slots[9:])
182+
>>> events[10].add_unavailability(*slots[:9])
185183

186184
We can now solve the problem one more time from scratch just as before::
187185

@@ -190,20 +188,20 @@ We can now solve the problem one more time from scratch just as before::
190188
>>> alt_schedule.sort(key=lambda item: item.slot.starts_at)
191189
>>> for item in alt_schedule:
192190
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
193-
Talk 1 at 15-Sep-2016 09:30 in Big
194-
Talk 8 at 15-Sep-2016 09:30 in Small
195-
Talk 4 at 15-Sep-2016 10:00 in Big
196-
Talk 5 at 15-Sep-2016 10:00 in Small
197-
Talk 3 at 15-Sep-2016 12:30 in Small
198-
Talk 9 at 15-Sep-2016 12:30 in Big
199-
Talk 2 at 15-Sep-2016 13:00 in Small
200-
Talk 12 at 15-Sep-2016 13:00 in Big
201-
Talk 11 at 16-Sep-2016 09:30 in Big
191+
Talk 1 at 15-Sep-2016 09:30 in Small
192+
Talk 9 at 15-Sep-2016 09:30 in Big
193+
Talk 5 at 15-Sep-2016 10:00 in Big
194+
Talk 10 at 15-Sep-2016 10:00 in Small
195+
Talk 4 at 15-Sep-2016 12:30 in Big
196+
Talk 7 at 15-Sep-2016 12:30 in Small
197+
Talk 6 at 15-Sep-2016 13:00 in Big
198+
Talk 12 at 15-Sep-2016 13:00 in Small
199+
Talk 2 at 16-Sep-2016 09:30 in Big
202200
Workshop 2 at 16-Sep-2016 09:30 in Small
203-
Talk 10 at 16-Sep-2016 10:00 in Big
204-
Talk 6 at 16-Sep-2016 12:30 in Big
201+
Talk 8 at 16-Sep-2016 10:00 in Big
202+
Talk 3 at 16-Sep-2016 12:30 in Big
205203
Boardgames at 16-Sep-2016 12:30 in Outside
206-
Talk 7 at 16-Sep-2016 13:00 in Big
204+
Talk 11 at 16-Sep-2016 13:00 in Big
207205
Workshop 1 at 16-Sep-2016 13:00 in Small
208206
City tour at 16-Sep-2016 13:00 in Outside
209207

@@ -219,20 +217,20 @@ old schedule::
219217
>>> similar_schedule.sort(key=lambda item: item.slot.starts_at)
220218
>>> for item in similar_schedule:
221219
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
222-
Talk 4 at 15-Sep-2016 09:30 in Big
223-
Talk 7 at 15-Sep-2016 09:30 in Small
224-
Talk 1 at 15-Sep-2016 10:00 in Big
220+
Talk 1 at 15-Sep-2016 09:30 in Big
221+
Talk 12 at 15-Sep-2016 09:30 in Small
222+
Talk 2 at 15-Sep-2016 10:00 in Big
225223
Talk 6 at 15-Sep-2016 10:00 in Small
226-
Talk 8 at 15-Sep-2016 12:30 in Big
227-
Talk 11 at 15-Sep-2016 12:30 in Small
228-
Talk 5 at 15-Sep-2016 13:00 in Big
229-
Talk 10 at 15-Sep-2016 13:00 in Small
230-
Talk 3 at 16-Sep-2016 09:30 in Big
224+
Talk 4 at 15-Sep-2016 12:30 in Small
225+
Talk 9 at 15-Sep-2016 12:30 in Big
226+
Talk 5 at 15-Sep-2016 13:00 in Small
227+
Talk 10 at 15-Sep-2016 13:00 in Big
228+
Talk 7 at 16-Sep-2016 09:30 in Big
231229
Workshop 2 at 16-Sep-2016 09:30 in Small
232-
Talk 2 at 16-Sep-2016 10:00 in Big
233-
Talk 12 at 16-Sep-2016 12:30 in Big
230+
Talk 8 at 16-Sep-2016 10:00 in Big
231+
Talk 3 at 16-Sep-2016 12:30 in Big
234232
Boardgames at 16-Sep-2016 12:30 in Outside
235-
Talk 9 at 16-Sep-2016 13:00 in Big
233+
Talk 11 at 16-Sep-2016 13:00 in Big
236234
Workshop 1 at 16-Sep-2016 13:00 in Small
237235
City tour at 16-Sep-2016 13:00 in Outside
238236

@@ -248,37 +246,34 @@ with the original. Firstly, we can see which events moved to different slots::
248246
>>> event_diff = scheduler.event_schedule_difference(schedule, alt_schedule)
249247
>>> for item in event_diff:
250248
... print(f"{item.event.name} has moved from {item.old_slot.venue} at {item.old_slot.starts_at} to {item.new_slot.venue} at {item.new_slot.starts_at}")
251-
Talk 1 has moved from Big at 15-Sep-2016 10:00 to Big at 15-Sep-2016 09:30
252-
Talk 10 has moved from Small at 15-Sep-2016 13:00 to Big at 16-Sep-2016 10:00
253-
Talk 11 has moved from Big at 16-Sep-2016 12:30 to Big at 16-Sep-2016 09:30
254-
Talk 12 has moved from Small at 15-Sep-2016 12:30 to Big at 15-Sep-2016 13:00
255-
Talk 2 has moved from Big at 16-Sep-2016 10:00 to Small at 15-Sep-2016 13:00
256-
Talk 3 has moved from Big at 16-Sep-2016 09:30 to Small at 15-Sep-2016 12:30
257-
Talk 4 has moved from Big at 15-Sep-2016 09:30 to Big at 15-Sep-2016 10:00
258-
Talk 5 has moved from Big at 15-Sep-2016 13:00 to Small at 15-Sep-2016 10:00
259-
Talk 6 has moved from Small at 15-Sep-2016 10:00 to Big at 16-Sep-2016 12:30
260-
Talk 7 has moved from Small at 15-Sep-2016 09:30 to Big at 16-Sep-2016 13:00
261-
Talk 8 has moved from Big at 15-Sep-2016 12:30 to Small at 15-Sep-2016 09:30
262-
Talk 9 has moved from Big at 16-Sep-2016 13:00 to Big at 15-Sep-2016 12:30
249+
Talk 1 has moved from Big at 15-Sep-2016 09:30 to Small at 15-Sep-2016 09:30
250+
Talk 10 has moved from Big at 15-Sep-2016 13:00 to Small at 15-Sep-2016 10:00
251+
Talk 11 has moved from Big at 15-Sep-2016 12:30 to Big at 16-Sep-2016 13:00
252+
Talk 12 has moved from Small at 15-Sep-2016 09:30 to Small at 15-Sep-2016 13:00
253+
Talk 2 has moved from Big at 15-Sep-2016 10:00 to Big at 16-Sep-2016 09:30
254+
Talk 4 has moved from Small at 15-Sep-2016 12:30 to Big at 15-Sep-2016 12:30
255+
Talk 5 has moved from Small at 15-Sep-2016 13:00 to Big at 15-Sep-2016 10:00
256+
Talk 6 has moved from Small at 15-Sep-2016 10:00 to Big at 15-Sep-2016 13:00
257+
Talk 7 has moved from Big at 16-Sep-2016 09:30 to Small at 15-Sep-2016 12:30
258+
Talk 9 has moved from Big at 16-Sep-2016 13:00 to Big at 15-Sep-2016 09:30
263259

264260

265261
We can also look at slots to see which now have a different event scheduled::
266262

267263
>>> slot_diff = scheduler.slot_schedule_difference(schedule, alt_schedule)
268264
>>> for item in slot_diff:
269265
... print(f"{item.slot.venue} at {item.slot.starts_at} will now host {item.new_event.name} rather than {item.old_event.name}" )
270-
Big at 15-Sep-2016 09:30 will now host Talk 1 rather than Talk 4
271-
Big at 15-Sep-2016 10:00 will now host Talk 4 rather than Talk 1
272-
Big at 15-Sep-2016 12:30 will now host Talk 9 rather than Talk 8
273-
Big at 15-Sep-2016 13:00 will now host Talk 12 rather than Talk 5
274-
Big at 16-Sep-2016 09:30 will now host Talk 11 rather than Talk 3
275-
Big at 16-Sep-2016 10:00 will now host Talk 10 rather than Talk 2
276-
Big at 16-Sep-2016 12:30 will now host Talk 6 rather than Talk 11
277-
Big at 16-Sep-2016 13:00 will now host Talk 7 rather than Talk 9
278-
Small at 15-Sep-2016 09:30 will now host Talk 8 rather than Talk 7
279-
Small at 15-Sep-2016 10:00 will now host Talk 5 rather than Talk 6
280-
Small at 15-Sep-2016 12:30 will now host Talk 3 rather than Talk 12
281-
Small at 15-Sep-2016 13:00 will now host Talk 2 rather than Talk 10
266+
Big at 15-Sep-2016 09:30 will now host Talk 9 rather than Talk 1
267+
Big at 15-Sep-2016 10:00 will now host Talk 5 rather than Talk 2
268+
Big at 15-Sep-2016 12:30 will now host Talk 4 rather than Talk 11
269+
Big at 15-Sep-2016 13:00 will now host Talk 6 rather than Talk 10
270+
Big at 16-Sep-2016 09:30 will now host Talk 2 rather than Talk 7
271+
Big at 16-Sep-2016 13:00 will now host Talk 11 rather than Talk 9
272+
Small at 15-Sep-2016 09:30 will now host Talk 1 rather than Talk 12
273+
Small at 15-Sep-2016 10:00 will now host Talk 10 rather than Talk 6
274+
Small at 15-Sep-2016 12:30 will now host Talk 7 rather than Talk 4
275+
Small at 15-Sep-2016 13:00 will now host Talk 12 rather than Talk 5
276+
282277

283278

284279
We can use this facility to show how using :code:`number_of_changes` as our objective function
@@ -287,8 +282,9 @@ resulted in far fewer changes::
287282
>>> event_diff = scheduler.event_schedule_difference(schedule, similar_schedule)
288283
>>> for item in event_diff:
289284
... print(f"{item.event.name} has moved from {item.old_slot.venue} at {item.old_slot.starts_at} to {item.new_slot.venue} at {item.new_slot.starts_at}")
290-
Talk 11 has moved from Big at 16-Sep-2016 12:30 to Small at 15-Sep-2016 12:30
291-
Talk 12 has moved from Small at 15-Sep-2016 12:30 to Big at 16-Sep-2016 12:30
285+
Talk 11 has moved from Big at 15-Sep-2016 12:30 to Big at 16-Sep-2016 13:00
286+
Talk 9 has moved from Big at 16-Sep-2016 13:00 to Big at 15-Sep-2016 12:30
287+
292288

293289

294290
Scheduling chairs

src/conference_scheduler/lp_problem/constraints.py

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,39 +29,6 @@ def _max_one_event_per_slot(events, slots, X, summation_type=None):
2929
)
3030

3131

32-
def _events_in_session_share_a_tag(events, slots, X, summation_type=None):
33-
"""
34-
Constraint that ensures that if an event has a tag and
35-
is in a given session then it must
36-
share at least one tag with all other event in that session.
37-
"""
38-
39-
session_array, tag_array = lpu.session_array(slots), lpu.tag_array(events)
40-
summation = lpu.summation_functions[summation_type]
41-
42-
label = 'Dissimilar events schedule in same session'
43-
event_indices = range(len(tag_array))
44-
session_indices = range(len(session_array))
45-
for session in session_indices:
46-
slots = lpu._slots_in_session(session, session_array)
47-
for slot, event in it.product(slots, event_indices):
48-
if len(events[event].tags) > 0:
49-
other_events = lpu._events_with_diff_tag(event, tag_array)
50-
for other_slot, other_event in it.product(slots, other_events):
51-
if other_slot != slot and other_event != event:
52-
# If they have different tags they cannot be scheduled
53-
# together
54-
yield Constraint(
55-
f'{label} - event: {event}, slot: {slot}',
56-
summation(
57-
(
58-
X[(event, slot)],
59-
X[(other_event, other_slot)]
60-
)
61-
) <= 1
62-
)
63-
64-
6532
def _events_available_in_scheduled_slot(events, slots, X, **kwargs):
6633
"""
6734
Constraint that ensures that an event is scheduled in slots for which it is
@@ -85,7 +52,8 @@ def _events_available_during_other_events(
8552
):
8653
"""
8754
Constraint that ensures that an event is not scheduled at the same time as
88-
another event for which it is unavailable.
55+
another event for which it is unavailable. Unavailability of events is
56+
either because it is explicitly defined or because they share a tag.
8957
"""
9058
summation = lpu.summation_functions[summation_type]
9159
event_availability_array = lpu.event_availability_array(events)
@@ -114,7 +82,6 @@ def all_constraints(events, slots, X, summation_type=None):
11482
generators = (
11583
_schedule_all_events,
11684
_max_one_event_per_slot,
117-
_events_in_session_share_a_tag,
11885
_events_available_in_scheduled_slot,
11986
_events_available_during_other_events,
12087
)

src/conference_scheduler/lp_problem/utils.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ def event_availability_array(events):
9191
array = np.ones((len(events), len(events)))
9292
for row, event in enumerate(events):
9393
for col, other_event in enumerate(events):
94-
if other_event in event.unavailability:
95-
array[row, col] = 0
96-
array[col, row] = 0
94+
if row != col:
95+
tags = set(event.tags)
96+
events_share_tag = len(tags.intersection(other_event.tags)) > 0
97+
if (other_event in event.unavailability) or events_share_tag:
98+
array[row, col] = 0
99+
array[col, row] = 0
97100
return array
98101

99102

0 commit comments

Comments
 (0)