Skip to content

Commit 1c836ec

Browse files
committed
Implement equity objective function
Note this also fixed what was the overcrowding objective (it was incorrect). - Write documentation for demand equity optimisation. - Implement equity - Change list of constraints in `all_constraints` to ensure artificial constraint not used for valid
1 parent ed5a9a7 commit 1c836ec

File tree

10 files changed

+260
-121
lines changed

10 files changed

+260
-121
lines changed

docs/background/mathematical_model.rst

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ However, it might be desirable to also optimise a given objective function.
130130
Objective functions
131131
+++++++++++++++++++
132132

133-
Optimising to avoid room overflow
134-
---------------------------------
133+
Efficiency: Optimising to avoid total room overflow
134+
---------------------------------------------------
135135

136136
Demand for events might be known: this will be captured using a vector
137137
:math:`d\in\mathbb{R}_{\geq 0}^{M}`. Similarly capacity for rooms might be
@@ -141,19 +141,57 @@ with parallel sessions delegates might not go where they originally intended) we
141141
can aim to minimise the expected overflow given by the following expression:
142142

143143
.. math::
144-
:label: overflow_objective_function
144+
:label: total_overflow_objective_function
145145
146-
\sum_{i=1}^{M}\sum_{j=1}^{N}X_{ij}(c_j - d_i)
146+
\sum_{i=1}^{M}\sum_{j=1}^{N}X_{ij}(d_i - c_j)
147147
148148
Using this, our optimisation problem to give a desirable schedule is obtained by
149149
solving the following problem:
150150

151-
Minimise :eq:`overflow_objective_function` subject to :eq:`all_events_scheduled_constraint`,
152-
:eq:`all_slots_at_most_1_event_constraint`,
153-
:eq:`slot_constraint` and :eq:`event_constraint`.
151+
Minimise :eq:`total_overflow_objective_function` subject to
152+
:eq:`all_events_scheduled_constraint`,
153+
:eq:`all_slots_at_most_1_event_constraint`, :eq:`slot_constraint` and
154+
:eq:`event_constraint`.
155+
156+
Equity: Optimising to avoid worse room overflow
157+
-----------------------------------------------
158+
159+
Minimising :eq:`total_overflow_objective_function` might still leave a given
160+
slot with a very large overflow relative to all over slots.
161+
We
162+
can aim to minimise the maximum overflow in a given slot given by the following
163+
expression:
164+
165+
.. math::
166+
167+
\max_{i,j}X_{ij}(d_i - c_j)
168+
169+
Note that it is not possible to use :math:`\max` in the objective function for a
170+
linear program (it is none linear). However, instead we can define another
171+
variable: :math:`\beta` as the upper bound for the overflow in each slot:
172+
173+
.. math::
174+
:label: overflow_constraints
175+
176+
X_{ij}(d_i - c_j) \leq \beta \text{ for all }0\leq i\leq N\text{ for all }1\leq j\leq M
177+
178+
The objective function then becomes to minimize:
179+
180+
.. math::
181+
:label: worse_overflow_objective_function
182+
183+
\beta
184+
185+
Using this, our optimisation problem to give a desirable schedule is obtained by
186+
solving the following problem:
187+
188+
Minimise :eq:`worse_overflow_objective_function` subject to
189+
:eq:`all_events_scheduled_constraint`,
190+
:eq:`all_slots_at_most_1_event_constraint`, :eq:`slot_constraint`,
191+
:eq:`event_constraint` and :eq:`overflow_constraints`.
154192

155-
Minimise change from a previous schedule
156-
----------------------------------------
193+
Consistency: Minimise change from a previous schedule
194+
-----------------------------------------------------
157195

158196
Once a schedule has been obtained and publicised to all delegates, a new
159197
constraint might arise (modifying :eq:`all_events_scheduled_constraint`,

docs/tutorial/index.rst

Lines changed: 96 additions & 67 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=3000),
68+
... Event(name='Talk 3', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=200),
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),
@@ -75,7 +75,7 @@ duration/location of the slots we know some of them are unavailable for a given
7575
... Event(name='Talk 10', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=30),
7676
... Event(name='Talk 11', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=30),
7777
... Event(name='Talk 12', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=30),
78-
... Event(name='Workshop 1', duration=60, tags=['testing'], unavailability=outside_slots[:], demand=300),
78+
... Event(name='Workshop 1', duration=60, tags=['testing'], unavailability=outside_slots[:], demand=40),
7979
... Event(name='Workshop 2', duration=60, tags=['testing'], unavailability=outside_slots[:], demand=40),
8080
... Event(name='City tour', duration=90, tags=[], unavailability=talk_slots[:] + workshop_slots[:], demand=100),
8181
... Event(name='Boardgames', duration=90, tags=[], unavailability=talk_slots[:] + workshop_slots[:], demand=20)]
@@ -137,49 +137,79 @@ Avoiding room overcrowding
137137

138138
The data we input in to the model included information about demand for a talk;
139139
this could be approximated from previous popularity for a talk. However, the
140-
scheduler has put :code:`Talk 2` and :code:`Talk 3` (which have high demand) in
140+
scheduler has put :code:`Talk 3` (which have high demand) in
141141
the small room (which has capacity 50). We can include an objective function in
142142
our scheduler to minimise the difference between room capacity and demand::
143143

144144
>>> from conference_scheduler.lp_problem import objective_functions
145-
>>> func = objective_functions.capacity_demand_difference
145+
>>> func = objective_functions.efficiency_capacity_demand_difference
146146
>>> schedule = scheduler.schedule(events, slots, objective_function=func)
147147

148148
>>> schedule.sort(key=lambda item: item.slot.starts_at)
149149
>>> for item in schedule:
150150
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
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
154-
Talk 6 at 15-Sep-2016 10:00 in Small
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
151+
Talk 4 at 15-Sep-2016 09:30 in Big
152+
Talk 5 at 15-Sep-2016 09:30 in Small
153+
Talk 3 at 15-Sep-2016 10:00 in Big
154+
Talk 9 at 15-Sep-2016 10:00 in Small
155+
Talk 6 at 15-Sep-2016 12:30 in Big
156+
Talk 11 at 15-Sep-2016 12:30 in Small
157+
Talk 2 at 15-Sep-2016 13:00 in Small
158+
Talk 7 at 15-Sep-2016 13:00 in Big
159+
Talk 8 at 16-Sep-2016 09:30 in Big
160160
Workshop 2 at 16-Sep-2016 09:30 in Small
161-
Talk 8 at 16-Sep-2016 10:00 in Big
162-
Talk 3 at 16-Sep-2016 12:30 in Big
161+
Talk 12 at 16-Sep-2016 10:00 in Big
162+
Talk 1 at 16-Sep-2016 12:30 in Big
163163
Boardgames at 16-Sep-2016 12:30 in Outside
164-
Talk 9 at 16-Sep-2016 13:00 in Big
164+
Talk 10 at 16-Sep-2016 13:00 in Big
165165
Workshop 1 at 16-Sep-2016 13:00 in Small
166166
City tour at 16-Sep-2016 13:00 in Outside
167167

168+
We see that :code:`Talk 3` has moved to the bigger room but that all other
169+
constraints still hold. Note however that this has also moved :code:`Talk 2`
170+
(which has relatively high demand) to a small room. This is because we have
171+
minimised the overall overcrowding. This can have the negative effect of leaving
172+
one slot with a high overcrowding for the benefit of overall efficiency. We can
173+
however include a different objective function to minimise the maximum
174+
overcrowding in any given slot::
168175

169-
We see that those talks have moved to the bigger room but that all other
170-
constraints still hold.
176+
>>> from conference_scheduler.lp_problem import objective_functions
177+
>>> func = objective_functions.equity_capacity_demand_difference
178+
>>> schedule = scheduler.schedule(events, slots, objective_function=func)
179+
180+
>>> schedule.sort(key=lambda item: item.slot.starts_at)
181+
>>> for item in schedule:
182+
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
183+
Talk 1 at 15-Sep-2016 09:30 in Small
184+
Talk 9 at 15-Sep-2016 09:30 in Big
185+
Talk 3 at 15-Sep-2016 10:00 in Big
186+
Talk 10 at 15-Sep-2016 10:00 in Small
187+
Talk 4 at 15-Sep-2016 12:30 in Small
188+
Talk 7 at 15-Sep-2016 12:30 in Big
189+
Talk 2 at 15-Sep-2016 13:00 in Big
190+
Talk 8 at 15-Sep-2016 13:00 in Small
191+
Talk 6 at 16-Sep-2016 09:30 in Big
192+
Workshop 2 at 16-Sep-2016 09:30 in Small
193+
Talk 12 at 16-Sep-2016 10:00 in Big
194+
Talk 11 at 16-Sep-2016 12:30 in Big
195+
Boardgames at 16-Sep-2016 12:30 in Outside
196+
Talk 5 at 16-Sep-2016 13:00 in Big
197+
Workshop 1 at 16-Sep-2016 13:00 in Small
198+
City tour at 16-Sep-2016 13:00 in Outside
199+
200+
Now, both :code:`Talk 2` and :code:`Talk 3` are in the bigger rooms.
171201

172202
Coping with new information
173203
---------------------------
174204

175205
This is fantastic! Our schedule has now been published and everyone is excited
176206
about the conference. However, as can often happen, one of the speakers now
177207
informs us of a particular new constraints. For example, the speaker for
178-
:code:`Talk 11` is unable to speak on the first day.
208+
:code:`Talk 11` is unable to speak on the second day.
179209

180210
We can enter this new constraint::
181211

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

184214
We can now solve the problem one more time from scratch just as before::
185215

@@ -188,20 +218,20 @@ We can now solve the problem one more time from scratch just as before::
188218
>>> alt_schedule.sort(key=lambda item: item.slot.starts_at)
189219
>>> for item in alt_schedule:
190220
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
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
221+
Talk 3 at 15-Sep-2016 09:30 in Big
222+
Talk 12 at 15-Sep-2016 09:30 in Small
223+
Talk 2 at 15-Sep-2016 10:00 in Big
194224
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
225+
Talk 1 at 15-Sep-2016 12:30 in Big
226+
Talk 8 at 15-Sep-2016 12:30 in Small
227+
Talk 5 at 15-Sep-2016 13:00 in Big
228+
Talk 9 at 15-Sep-2016 13:00 in Small
229+
Talk 11 at 16-Sep-2016 09:30 in Big
200230
Workshop 2 at 16-Sep-2016 09:30 in Small
201-
Talk 8 at 16-Sep-2016 10:00 in Big
202-
Talk 3 at 16-Sep-2016 12:30 in Big
231+
Talk 4 at 16-Sep-2016 10:00 in Big
232+
Talk 7 at 16-Sep-2016 12:30 in Big
203233
Boardgames at 16-Sep-2016 12:30 in Outside
204-
Talk 11 at 16-Sep-2016 13:00 in Big
234+
Talk 6 at 16-Sep-2016 13:00 in Big
205235
Workshop 1 at 16-Sep-2016 13:00 in Small
206236
City tour at 16-Sep-2016 13:00 in Outside
207237

@@ -210,27 +240,26 @@ completely different schedule with a number of changes. We can however solve the
210240
problem with a new objective function which is to minimise the changes from the
211241
old schedule::
212242

213-
214243
>>> func = objective_functions.number_of_changes
215244
>>> similar_schedule = scheduler.schedule(events, slots, objective_function=func, original_schedule=schedule)
216245

217246
>>> similar_schedule.sort(key=lambda item: item.slot.starts_at)
218247
>>> for item in similar_schedule:
219248
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
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
223-
Talk 6 at 15-Sep-2016 10:00 in Small
249+
Talk 1 at 15-Sep-2016 09:30 in Small
250+
Talk 9 at 15-Sep-2016 09:30 in Big
251+
Talk 3 at 15-Sep-2016 10:00 in Big
252+
Talk 10 at 15-Sep-2016 10:00 in Small
224253
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
254+
Talk 7 at 15-Sep-2016 12:30 in Big
255+
Talk 2 at 15-Sep-2016 13:00 in Big
256+
Talk 8 at 15-Sep-2016 13:00 in Small
257+
Talk 11 at 16-Sep-2016 09:30 in Big
229258
Workshop 2 at 16-Sep-2016 09:30 in Small
230-
Talk 8 at 16-Sep-2016 10:00 in Big
231-
Talk 3 at 16-Sep-2016 12:30 in Big
259+
Talk 12 at 16-Sep-2016 10:00 in Big
260+
Talk 6 at 16-Sep-2016 12:30 in Big
232261
Boardgames at 16-Sep-2016 12:30 in Outside
233-
Talk 11 at 16-Sep-2016 13:00 in Big
262+
Talk 5 at 16-Sep-2016 13:00 in Big
234263
Workshop 1 at 16-Sep-2016 13:00 in Small
235264
City tour at 16-Sep-2016 13:00 in Outside
236265

@@ -246,34 +275,35 @@ with the original. Firstly, we can see which events moved to different slots::
246275
>>> event_diff = scheduler.event_schedule_difference(schedule, alt_schedule)
247276
>>> for item in event_diff:
248277
... 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}")
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
278+
Talk 1 has moved from Small at 15-Sep-2016 09:30 to Big at 15-Sep-2016 12:30
279+
Talk 11 has moved from Big at 16-Sep-2016 12:30 to Big at 16-Sep-2016 09:30
280+
Talk 12 has moved from Big at 16-Sep-2016 10:00 to Small at 15-Sep-2016 09:30
281+
Talk 2 has moved from Big at 15-Sep-2016 13:00 to Big at 15-Sep-2016 10:00
282+
Talk 3 has moved from Big at 15-Sep-2016 10:00 to Big at 15-Sep-2016 09:30
283+
Talk 4 has moved from Small at 15-Sep-2016 12:30 to Big at 16-Sep-2016 10:00
284+
Talk 5 has moved from Big at 16-Sep-2016 13:00 to Big at 15-Sep-2016 13:00
285+
Talk 6 has moved from Big at 16-Sep-2016 09:30 to Big at 16-Sep-2016 13:00
286+
Talk 7 has moved from Big at 15-Sep-2016 12:30 to Big at 16-Sep-2016 12:30
287+
Talk 8 has moved from Small at 15-Sep-2016 13:00 to Small at 15-Sep-2016 12:30
288+
Talk 9 has moved from Big at 15-Sep-2016 09:30 to Small at 15-Sep-2016 13:00
259289

260290

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

263293
>>> slot_diff = scheduler.slot_schedule_difference(schedule, alt_schedule)
264294
>>> for item in slot_diff:
265295
... print(f"{item.slot.venue} at {item.slot.starts_at} will now host {item.new_event.name} rather than {item.old_event.name}" )
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-
296+
Big at 15-Sep-2016 09:30 will now host Talk 3 rather than Talk 9
297+
Big at 15-Sep-2016 10:00 will now host Talk 2 rather than Talk 3
298+
Big at 15-Sep-2016 12:30 will now host Talk 1 rather than Talk 7
299+
Big at 15-Sep-2016 13:00 will now host Talk 5 rather than Talk 2
300+
Big at 16-Sep-2016 09:30 will now host Talk 11 rather than Talk 6
301+
Big at 16-Sep-2016 10:00 will now host Talk 4 rather than Talk 12
302+
Big at 16-Sep-2016 12:30 will now host Talk 7 rather than Talk 11
303+
Big at 16-Sep-2016 13:00 will now host Talk 6 rather than Talk 5
304+
Small at 15-Sep-2016 09:30 will now host Talk 12 rather than Talk 1
305+
Small at 15-Sep-2016 12:30 will now host Talk 8 rather than Talk 4
306+
Small at 15-Sep-2016 13:00 will now host Talk 9 rather than Talk 8
277307

278308

279309
We can use this facility to show how using :code:`number_of_changes` as our objective function
@@ -282,9 +312,8 @@ resulted in far fewer changes::
282312
>>> event_diff = scheduler.event_schedule_difference(schedule, similar_schedule)
283313
>>> for item in event_diff:
284314
... 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}")
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-
315+
Talk 11 has moved from Big at 16-Sep-2016 12:30 to Big at 16-Sep-2016 09:30
316+
Talk 6 has moved from Big at 16-Sep-2016 09:30 to Big at 16-Sep-2016 12:30
288317

289318

290319
Scheduling chairs

0 commit comments

Comments
 (0)