Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 47 additions & 9 deletions docs/background/mathematical_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ However, it might be desirable to also optimise a given objective function.
Objective functions
+++++++++++++++++++

Optimising to avoid room overflow
---------------------------------
Efficiency: Optimising to avoid total room overflow
---------------------------------------------------

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

.. math::
:label: overflow_objective_function
:label: total_overflow_objective_function

\sum_{i=1}^{M}\sum_{j=1}^{N}X_{ij}(c_j - d_i)
\sum_{i=1}^{M}\sum_{j=1}^{N}X_{ij}(d_i - c_j)

Using this, our optimisation problem to give a desirable schedule is obtained by
solving the following problem:

Minimise :eq:`overflow_objective_function` subject to :eq:`all_events_scheduled_constraint`,
:eq:`all_slots_at_most_1_event_constraint`,
:eq:`slot_constraint` and :eq:`event_constraint`.
Minimise :eq:`total_overflow_objective_function` subject to
:eq:`all_events_scheduled_constraint`,
:eq:`all_slots_at_most_1_event_constraint`, :eq:`slot_constraint` and
:eq:`event_constraint`.

Equity: Optimising to avoid worse room overflow
-----------------------------------------------

Minimising :eq:`total_overflow_objective_function` might still leave a given
slot with a very large overflow relative to all over slots.
We
can aim to minimise the maximum overflow in a given slot given by the following
expression:

.. math::

\max_{i,j}X_{ij}(d_i - c_j)

Note that it is not possible to use :math:`\max` in the objective function for a
linear program (it is none linear). However, instead we can define another
variable: :math:`\beta` as the upper bound for the overflow in each slot:

.. math::
:label: overflow_constraints

X_{ij}(d_i - c_j) \leq \beta \text{ for all }0\leq i\leq N\text{ for all }1\leq j\leq M

The objective function then becomes to minimize:

.. math::
:label: worse_overflow_objective_function

\beta

Using this, our optimisation problem to give a desirable schedule is obtained by
solving the following problem:

Minimise :eq:`worse_overflow_objective_function` subject to
:eq:`all_events_scheduled_constraint`,
:eq:`all_slots_at_most_1_event_constraint`, :eq:`slot_constraint`,
:eq:`event_constraint` and :eq:`overflow_constraints`.

Minimise change from a previous schedule
----------------------------------------
Consistency: Minimise change from a previous schedule
-----------------------------------------------------

Once a schedule has been obtained and publicised to all delegates, a new
constraint might arise (modifying :eq:`all_events_scheduled_constraint`,
Expand Down
163 changes: 96 additions & 67 deletions docs/tutorial/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ duration/location of the slots we know some of them are unavailable for a given

>>> events = [Event(name='Talk 1', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=50),
... Event(name='Talk 2', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=130),
... Event(name='Talk 3', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=3000),
... Event(name='Talk 3', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=200),
... Event(name='Talk 4', duration=30, tags=['beginner'], unavailability=outside_slots[:], demand=30),
... Event(name='Talk 5', duration=30, tags=['intermediate'], unavailability=outside_slots[:], demand=60),
... Event(name='Talk 6', duration=30, tags=['intermediate'], unavailability=outside_slots[:], demand=30),
Expand All @@ -75,7 +75,7 @@ duration/location of the slots we know some of them are unavailable for a given
... Event(name='Talk 10', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=30),
... Event(name='Talk 11', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=30),
... Event(name='Talk 12', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=30),
... Event(name='Workshop 1', duration=60, tags=['testing'], unavailability=outside_slots[:], demand=300),
... Event(name='Workshop 1', duration=60, tags=['testing'], unavailability=outside_slots[:], demand=40),
... Event(name='Workshop 2', duration=60, tags=['testing'], unavailability=outside_slots[:], demand=40),
... Event(name='City tour', duration=90, tags=[], unavailability=talk_slots[:] + workshop_slots[:], demand=100),
... Event(name='Boardgames', duration=90, tags=[], unavailability=talk_slots[:] + workshop_slots[:], demand=20)]
Expand Down Expand Up @@ -137,49 +137,79 @@ Avoiding room overcrowding

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

>>> from conference_scheduler.lp_problem import objective_functions
>>> func = objective_functions.capacity_demand_difference
>>> func = objective_functions.efficiency_capacity_demand_difference
>>> schedule = scheduler.schedule(events, slots, objective_function=func)

>>> schedule.sort(key=lambda item: item.slot.starts_at)
>>> for item in schedule:
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
Talk 1 at 15-Sep-2016 09:30 in Big
Talk 12 at 15-Sep-2016 09:30 in Small
Talk 2 at 15-Sep-2016 10:00 in Big
Talk 6 at 15-Sep-2016 10:00 in Small
Talk 4 at 15-Sep-2016 12:30 in Small
Talk 11 at 15-Sep-2016 12:30 in Big
Talk 5 at 15-Sep-2016 13:00 in Small
Talk 10 at 15-Sep-2016 13:00 in Big
Talk 7 at 16-Sep-2016 09:30 in Big
Talk 4 at 15-Sep-2016 09:30 in Big
Talk 5 at 15-Sep-2016 09:30 in Small
Talk 3 at 15-Sep-2016 10:00 in Big
Talk 9 at 15-Sep-2016 10:00 in Small
Talk 6 at 15-Sep-2016 12:30 in Big
Talk 11 at 15-Sep-2016 12:30 in Small
Talk 2 at 15-Sep-2016 13:00 in Small
Talk 7 at 15-Sep-2016 13:00 in Big
Talk 8 at 16-Sep-2016 09:30 in Big
Workshop 2 at 16-Sep-2016 09:30 in Small
Talk 8 at 16-Sep-2016 10:00 in Big
Talk 3 at 16-Sep-2016 12:30 in Big
Talk 12 at 16-Sep-2016 10:00 in Big
Talk 1 at 16-Sep-2016 12:30 in Big
Boardgames at 16-Sep-2016 12:30 in Outside
Talk 9 at 16-Sep-2016 13:00 in Big
Talk 10 at 16-Sep-2016 13:00 in Big
Workshop 1 at 16-Sep-2016 13:00 in Small
City tour at 16-Sep-2016 13:00 in Outside

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

We see that those talks have moved to the bigger room but that all other
constraints still hold.
>>> from conference_scheduler.lp_problem import objective_functions
>>> func = objective_functions.equity_capacity_demand_difference
>>> schedule = scheduler.schedule(events, slots, objective_function=func)

>>> schedule.sort(key=lambda item: item.slot.starts_at)
>>> for item in schedule:
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
Talk 1 at 15-Sep-2016 09:30 in Small
Talk 9 at 15-Sep-2016 09:30 in Big
Talk 3 at 15-Sep-2016 10:00 in Big
Talk 10 at 15-Sep-2016 10:00 in Small
Talk 4 at 15-Sep-2016 12:30 in Small
Talk 7 at 15-Sep-2016 12:30 in Big
Talk 2 at 15-Sep-2016 13:00 in Big
Talk 8 at 15-Sep-2016 13:00 in Small
Talk 6 at 16-Sep-2016 09:30 in Big
Workshop 2 at 16-Sep-2016 09:30 in Small
Talk 12 at 16-Sep-2016 10:00 in Big
Talk 11 at 16-Sep-2016 12:30 in Big
Boardgames at 16-Sep-2016 12:30 in Outside
Talk 5 at 16-Sep-2016 13:00 in Big
Workshop 1 at 16-Sep-2016 13:00 in Small
City tour at 16-Sep-2016 13:00 in Outside

Now, both :code:`Talk 2` and :code:`Talk 3` are in the bigger rooms.

Coping with new information
---------------------------

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

We can enter this new constraint::

>>> events[10].add_unavailability(*slots[:9])
>>> events[10].add_unavailability(*slots[9:])

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

Expand All @@ -188,20 +218,20 @@ We can now solve the problem one more time from scratch just as before::
>>> alt_schedule.sort(key=lambda item: item.slot.starts_at)
>>> for item in alt_schedule:
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
Talk 1 at 15-Sep-2016 09:30 in Small
Talk 9 at 15-Sep-2016 09:30 in Big
Talk 5 at 15-Sep-2016 10:00 in Big
Talk 3 at 15-Sep-2016 09:30 in Big
Talk 12 at 15-Sep-2016 09:30 in Small
Talk 2 at 15-Sep-2016 10:00 in Big
Talk 10 at 15-Sep-2016 10:00 in Small
Talk 4 at 15-Sep-2016 12:30 in Big
Talk 7 at 15-Sep-2016 12:30 in Small
Talk 6 at 15-Sep-2016 13:00 in Big
Talk 12 at 15-Sep-2016 13:00 in Small
Talk 2 at 16-Sep-2016 09:30 in Big
Talk 1 at 15-Sep-2016 12:30 in Big
Talk 8 at 15-Sep-2016 12:30 in Small
Talk 5 at 15-Sep-2016 13:00 in Big
Talk 9 at 15-Sep-2016 13:00 in Small
Talk 11 at 16-Sep-2016 09:30 in Big
Workshop 2 at 16-Sep-2016 09:30 in Small
Talk 8 at 16-Sep-2016 10:00 in Big
Talk 3 at 16-Sep-2016 12:30 in Big
Talk 4 at 16-Sep-2016 10:00 in Big
Talk 7 at 16-Sep-2016 12:30 in Big
Boardgames at 16-Sep-2016 12:30 in Outside
Talk 11 at 16-Sep-2016 13:00 in Big
Talk 6 at 16-Sep-2016 13:00 in Big
Workshop 1 at 16-Sep-2016 13:00 in Small
City tour at 16-Sep-2016 13:00 in Outside

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


>>> func = objective_functions.number_of_changes
>>> similar_schedule = scheduler.schedule(events, slots, objective_function=func, original_schedule=schedule)

>>> similar_schedule.sort(key=lambda item: item.slot.starts_at)
>>> for item in similar_schedule:
... print(f"{item.event.name} at {item.slot.starts_at} in {item.slot.venue}")
Talk 1 at 15-Sep-2016 09:30 in Big
Talk 12 at 15-Sep-2016 09:30 in Small
Talk 2 at 15-Sep-2016 10:00 in Big
Talk 6 at 15-Sep-2016 10:00 in Small
Talk 1 at 15-Sep-2016 09:30 in Small
Talk 9 at 15-Sep-2016 09:30 in Big
Talk 3 at 15-Sep-2016 10:00 in Big
Talk 10 at 15-Sep-2016 10:00 in Small
Talk 4 at 15-Sep-2016 12:30 in Small
Talk 9 at 15-Sep-2016 12:30 in Big
Talk 5 at 15-Sep-2016 13:00 in Small
Talk 10 at 15-Sep-2016 13:00 in Big
Talk 7 at 16-Sep-2016 09:30 in Big
Talk 7 at 15-Sep-2016 12:30 in Big
Talk 2 at 15-Sep-2016 13:00 in Big
Talk 8 at 15-Sep-2016 13:00 in Small
Talk 11 at 16-Sep-2016 09:30 in Big
Workshop 2 at 16-Sep-2016 09:30 in Small
Talk 8 at 16-Sep-2016 10:00 in Big
Talk 3 at 16-Sep-2016 12:30 in Big
Talk 12 at 16-Sep-2016 10:00 in Big
Talk 6 at 16-Sep-2016 12:30 in Big
Boardgames at 16-Sep-2016 12:30 in Outside
Talk 11 at 16-Sep-2016 13:00 in Big
Talk 5 at 16-Sep-2016 13:00 in Big
Workshop 1 at 16-Sep-2016 13:00 in Small
City tour at 16-Sep-2016 13:00 in Outside

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


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

>>> slot_diff = scheduler.slot_schedule_difference(schedule, alt_schedule)
>>> for item in slot_diff:
... print(f"{item.slot.venue} at {item.slot.starts_at} will now host {item.new_event.name} rather than {item.old_event.name}" )
Big at 15-Sep-2016 09:30 will now host Talk 9 rather than Talk 1
Big at 15-Sep-2016 10:00 will now host Talk 5 rather than Talk 2
Big at 15-Sep-2016 12:30 will now host Talk 4 rather than Talk 11
Big at 15-Sep-2016 13:00 will now host Talk 6 rather than Talk 10
Big at 16-Sep-2016 09:30 will now host Talk 2 rather than Talk 7
Big at 16-Sep-2016 13:00 will now host Talk 11 rather than Talk 9
Small at 15-Sep-2016 09:30 will now host Talk 1 rather than Talk 12
Small at 15-Sep-2016 10:00 will now host Talk 10 rather than Talk 6
Small at 15-Sep-2016 12:30 will now host Talk 7 rather than Talk 4
Small at 15-Sep-2016 13:00 will now host Talk 12 rather than Talk 5

Big at 15-Sep-2016 09:30 will now host Talk 3 rather than Talk 9
Big at 15-Sep-2016 10:00 will now host Talk 2 rather than Talk 3
Big at 15-Sep-2016 12:30 will now host Talk 1 rather than Talk 7
Big at 15-Sep-2016 13:00 will now host Talk 5 rather than Talk 2
Big at 16-Sep-2016 09:30 will now host Talk 11 rather than Talk 6
Big at 16-Sep-2016 10:00 will now host Talk 4 rather than Talk 12
Big at 16-Sep-2016 12:30 will now host Talk 7 rather than Talk 11
Big at 16-Sep-2016 13:00 will now host Talk 6 rather than Talk 5
Small at 15-Sep-2016 09:30 will now host Talk 12 rather than Talk 1
Small at 15-Sep-2016 12:30 will now host Talk 8 rather than Talk 4
Small at 15-Sep-2016 13:00 will now host Talk 9 rather than Talk 8


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

Talk 11 has moved from Big at 16-Sep-2016 12:30 to Big at 16-Sep-2016 09:30
Talk 6 has moved from Big at 16-Sep-2016 09:30 to Big at 16-Sep-2016 12:30


Scheduling chairs
Expand Down
Loading