Tutorial

Welcome! You are the head scheduler for the upcoming name con. You have a venue, you have talks and you have a week to schedule everything!

Your venue has two rooms in which you will schedule talks and workshops in parallel but you’re also going to want to find time for some social events.

You have organised your time slots as follows:

  • The first day will have 2 sessions (morning and afternoon) with two 30 minute time slots in each room.
  • The second day will have 1 room used for longer 1 hour workshops, the other room used for more talks and 2 long sessions set aside for the social events.

Installing the conference scheduler

The conference scheduler is compatible with Python 3.6+ only.

You can install the latest version of conference_scheduler from PyPI:

$ pip install conference_scheduler

If you want to, you can also install a development version from source:

$ git clone https://github.com/PyconUK/ConferenceScheduler
$ cd ConferenceScheduler
$ python setup.py develop

Inputting the data

Let us create these time slots using the conference_scheduler:

>>> from datetime import datetime
>>> from conference_scheduler.resources import Slot, Event

>>> talk_slots  = [Slot(venue='Big', starts_at=datetime(2016, 9, 15, 9, 30), duration=30, session="A", capacity=200),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 15, 10, 0), duration=30, session="A", capacity=200),
...                Slot(venue='Small', starts_at=datetime(2016, 9, 15, 9, 30), duration=30, session="B", capacity=50),
...                Slot(venue='Small', starts_at=datetime(2016, 9, 15, 10, 0), duration=30, session="B", capacity=50),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 15, 12, 30), duration=30, session="C", capacity=200),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 15, 13, 0), duration=30, session="C", capacity=200),
...                Slot(venue='Small', starts_at=datetime(2016, 9, 15, 12, 30), duration=30, session="D", capacity=50),
...                Slot(venue='Small', starts_at=datetime(2016, 9, 15, 13, 0), duration=30, session="D", capacity=50),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 16, 9, 30), duration=30, session="E", capacity=50),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 16, 10, 00), duration=30, session="E", capacity=50),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 16, 12, 30), duration=30, session="F", capacity=50),
...                Slot(venue='Big', starts_at=datetime(2016, 9, 16, 13, 0), duration=30, session="F", capacity=50)]
>>> workshop_slots = [Slot(venue='Small', starts_at=datetime(2016, 9, 16, 9, 30), duration=60, session="G", capacity=50),
...                   Slot(venue='Small', starts_at=datetime(2016, 9, 16, 13, 0), duration=60, session="H", capacity=50)]
>>> outside_slots = [Slot(venue='Outside', starts_at=datetime(2016, 9, 16, 12, 30), duration=90, session="I", capacity=1000),
...                  Slot(venue='Outside', starts_at=datetime(2016, 9, 16, 13, 0), duration=90, session="J", capacity=1000)]
>>> slots = talk_slots + workshop_slots + outside_slots

Note that the duration must be given in minutes.

We also have a number of talks and workshops to schedule, because of the duration/location of the slots we know some of them are unavailable for a given slot:

>>> 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=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),
...           Event(name='Talk 7', duration=30, tags=['intermediate', 'advanced'], unavailability=outside_slots[:], demand=60),
...           Event(name='Talk 8', duration=30, tags=['intermediate', 'advanced'], unavailability=outside_slots[:], demand=60),
...           Event(name='Talk 9', duration=30, tags=['advanced'], unavailability=outside_slots[:], demand=60),
...           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=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)]

Further to this we have a couple of other constraints:

  • The speaker for Talk 1 is also the person delivering Workshop 1:

    >>> events[0].add_unavailability(events[6])
    
  • Also, the person running Workshop 2 is the person hosting the Boardgames:

    >>> events[13].add_unavailability(events[-1])
    

Note that we haven’t indicated the workshops cannot happen in the talk slots but this will automatically be taken care of because of the duration of the workshops (60mins) and the duration of the talk slots (30mins).

Creating a schedule

Now that we have slots and events we can schedule our event:

>>> from conference_scheduler import scheduler
>>> schedule = scheduler.schedule(events, slots)

>>> 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 5 at 2016-09-15 09:30:00 in Small
Talk 11 at 2016-09-15 09:30:00 in Big
Talk 4 at 2016-09-15 10:00:00 in Small
Talk 10 at 2016-09-15 10:00:00 in Big
Talk 1 at 2016-09-15 12:30:00 in Small
Talk 6 at 2016-09-15 12:30:00 in Big
Talk 3 at 2016-09-15 13:00:00 in Small
Talk 8 at 2016-09-15 13:00:00 in Big
Talk 2 at 2016-09-16 09:30:00 in Big
Workshop 2 at 2016-09-16 09:30:00 in Small
Talk 9 at 2016-09-16 10:00:00 in Big
Talk 12 at 2016-09-16 12:30:00 in Big
Boardgames at 2016-09-16 12:30:00 in Outside
Talk 7 at 2016-09-16 13:00:00 in Big
Workshop 1 at 2016-09-16 13:00:00 in Small
City tour at 2016-09-16 13:00:00 in Outside

We see that all the events are scheduled in appropriate rooms (as indicated by the unavailability attribute for the events). Also we have that Talk 1 doesn’t clash with Workshop 1. Similarly, the Boardgame does not clash with Workshop 2.

You will also note that no two events with the same tags are on at the same time. Tags allow for a quick way to batch define unavailability.

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 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.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 4 at 2016-09-15 09:30:00 in Big
Talk 5 at 2016-09-15 09:30:00 in Small
Talk 3 at 2016-09-15 10:00:00 in Big
Talk 9 at 2016-09-15 10:00:00 in Small
Talk 6 at 2016-09-15 12:30:00 in Big
Talk 11 at 2016-09-15 12:30:00 in Small
Talk 2 at 2016-09-15 13:00:00 in Small
Talk 7 at 2016-09-15 13:00:00 in Big
Talk 8 at 2016-09-16 09:30:00 in Big
Workshop 2 at 2016-09-16 09:30:00 in Small
Talk 12 at 2016-09-16 10:00:00 in Big
Talk 1 at 2016-09-16 12:30:00 in Big
Boardgames at 2016-09-16 12:30:00 in Outside
Talk 10 at 2016-09-16 13:00:00 in Big
Workshop 1 at 2016-09-16 13:00:00 in Small
City tour at 2016-09-16 13:00:00 in Outside

We see that Talk 3 has moved to the bigger room but that all other constraints still hold. Note however that this has also moved 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:

>>> 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 2016-09-15 09:30:00 in Small
Talk 9 at 2016-09-15 09:30:00 in Big
Talk 3 at 2016-09-15 10:00:00 in Big
Talk 10 at 2016-09-15 10:00:00 in Small
Talk 4 at 2016-09-15 12:30:00 in Small
Talk 7 at 2016-09-15 12:30:00 in Big
Talk 2 at 2016-09-15 13:00:00 in Big
Talk 8 at 2016-09-15 13:00:00 in Small
Talk 6 at 2016-09-16 09:30:00 in Big
Workshop 2 at 2016-09-16 09:30:00 in Small
Talk 12 at 2016-09-16 10:00:00 in Big
Talk 11 at 2016-09-16 12:30:00 in Big
Boardgames at 2016-09-16 12:30:00 in Outside
Talk 5 at 2016-09-16 13:00:00 in Big
Workshop 1 at 2016-09-16 13:00:00 in Small
City tour at 2016-09-16 13:00:00 in Outside

Now, both Talk 2 and 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 Talk 11 is unable to speak on the second day.

We can enter this new constraint:

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

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

>>> alt_schedule = scheduler.schedule(events, slots, objective_function=func)

>>> 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 3 at 2016-09-15 09:30:00 in Big
Talk 12 at 2016-09-15 09:30:00 in Small
Talk 2 at 2016-09-15 10:00:00 in Big
Talk 10 at 2016-09-15 10:00:00 in Small
Talk 1 at 2016-09-15 12:30:00 in Big
Talk 8 at 2016-09-15 12:30:00 in Small
Talk 5 at 2016-09-15 13:00:00 in Big
Talk 9 at 2016-09-15 13:00:00 in Small
Talk 11 at 2016-09-16 09:30:00 in Big
Workshop 2 at 2016-09-16 09:30:00 in Small
Talk 4 at 2016-09-16 10:00:00 in Big
Talk 7 at 2016-09-16 12:30:00 in Big
Boardgames at 2016-09-16 12:30:00 in Outside
Talk 6 at 2016-09-16 13:00:00 in Big
Workshop 1 at 2016-09-16 13:00:00 in Small
City tour at 2016-09-16 13:00:00 in Outside

This has resulted in a 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 2016-09-15 09:30:00 in Small
Talk 9 at 2016-09-15 09:30:00 in Big
Talk 3 at 2016-09-15 10:00:00 in Big
Talk 10 at 2016-09-15 10:00:00 in Small
Talk 4 at 2016-09-15 12:30:00 in Small
Talk 7 at 2016-09-15 12:30:00 in Big
Talk 2 at 2016-09-15 13:00:00 in Big
Talk 8 at 2016-09-15 13:00:00 in Small
Talk 11 at 2016-09-16 09:30:00 in Big
Workshop 2 at 2016-09-16 09:30:00 in Small
Talk 12 at 2016-09-16 10:00:00 in Big
Talk 6 at 2016-09-16 12:30:00 in Big
Boardgames at 2016-09-16 12:30:00 in Outside
Talk 5 at 2016-09-16 13:00:00 in Big
Workshop 1 at 2016-09-16 13:00:00 in Small
City tour at 2016-09-16 13:00:00 in Outside

Spotting the Changes

It can be a little difficult to spot what has changed when we compute a new schedule and so there are two functions which can help. Let’s take our alt_schedule and compare it 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 Small at 2016-09-15 09:30:00 to Big at 2016-09-15 12:30:00
Talk 11 has moved from Big at 2016-09-16 12:30:00 to Big at 2016-09-16 09:30:00
Talk 12 has moved from Big at 2016-09-16 10:00:00 to Small at 2016-09-15 09:30:00
Talk 2 has moved from Big at 2016-09-15 13:00:00 to Big at 2016-09-15 10:00:00
Talk 3 has moved from Big at 2016-09-15 10:00:00 to Big at 2016-09-15 09:30:00
Talk 4 has moved from Small at 2016-09-15 12:30:00 to Big at 2016-09-16 10:00:00
Talk 5 has moved from Big at 2016-09-16 13:00:00 to Big at 2016-09-15 13:00:00
Talk 6 has moved from Big at 2016-09-16 09:30:00 to Big at 2016-09-16 13:00:00
Talk 7 has moved from Big at 2016-09-15 12:30:00 to Big at 2016-09-16 12:30:00
Talk 8 has moved from Small at 2016-09-15 13:00:00 to Small at 2016-09-15 12:30:00
Talk 9 has moved from Big at 2016-09-15 09:30:00 to Small at 2016-09-15 13:00: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 2016-09-15 09:30:00 will now host Talk 3 rather than Talk 9
Big at 2016-09-15 10:00:00 will now host Talk 2 rather than Talk 3
Big at 2016-09-15 12:30:00 will now host Talk 1 rather than Talk 7
Big at 2016-09-15 13:00:00 will now host Talk 5 rather than Talk 2
Big at 2016-09-16 09:30:00 will now host Talk 11 rather than Talk 6
Big at 2016-09-16 10:00:00 will now host Talk 4 rather than Talk 12
Big at 2016-09-16 12:30:00 will now host Talk 7 rather than Talk 11
Big at 2016-09-16 13:00:00 will now host Talk 6 rather than Talk 5
Small at 2016-09-15 09:30:00 will now host Talk 12 rather than Talk 1
Small at 2016-09-15 12:30:00 will now host Talk 8 rather than Talk 4
Small at 2016-09-15 13:00:00 will now host Talk 9 rather than Talk 8

We can use this facility to show how using number_of_changes as our objective function 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 2016-09-16 12:30:00 to Big at 2016-09-16 09:30:00
Talk 6 has moved from Big at 2016-09-16 09:30:00 to Big at 2016-09-16 12:30:00

Scheduling chairs

Once we have a schedule for our talks, workshops and social events, we have the last task which is to schedule chairs for the talk sessions.

We have 6 different sessions of talks to chair:

Talk 4 at 2016-09-15 09:30:00 in Big
Talk 1 at 2016-09-15 10:00:00 in Big

Talk 7 at 2016-09-15 09:30:00 in Small
Talk 6 at 2016-09-15 10:00:00 in Small

Talk 8 at 2016-09-15 12:30:00 in Big
Talk 5 at 2016-09-15 13:00:00 in Big

Talk 11 at 2016-09-15 12:30:00 in Small
Talk 10 at 2016-09-15 13:00:00 in Small

Talk 3 at 2016-09-16 09:30:00 in Big
Talk 2 at 2016-09-16 10:00:00 in Big

Talk 12 at 2016-09-16 12:30:00 in Big
Talk 9 at 2016-09-16 13:00:00 in Big

We will use the conference scheduler, with these sessions corresponding to slots:

>>> chair_slots  = [Slot(venue='Big', starts_at=datetime(2016, 9, 15, 9, 30), duration=60, session="A", capacity=200),
...                 Slot(venue='Small', starts_at=datetime(2016, 9, 15, 9, 30), duration=60, session="B", capacity=50),
...                 Slot(venue='Big', starts_at=datetime(2016, 9, 15, 12, 30), duration=60, session="C", capacity=200),
...                 Slot(venue='Small', starts_at=datetime(2016, 9, 15, 12, 30), duration=60, session="D", capacity=50),
...                 Slot(venue='Big', starts_at=datetime(2016, 9, 16, 12, 30), duration=60, session="E", capacity=200),
...                 Slot(venue='Small', starts_at=datetime(2016, 9, 16, 12, 30), duration=60, session="F", capacity=50)]

We will need 6 chairpersons for these slots and we will use events as chairs. In practice, all chairing will be taken care of by 3 people, with each person chairing 2 sessions:

>>> events = [Event(name='Chair A-1', duration=60, demand=0),
...           Event(name='Chair A-2', duration=60, demand=0),
...           Event(name='Chair B-1', duration=60, demand=0),
...           Event(name='Chair B-2', duration=60, demand=0),
...           Event(name='Chair C-1', duration=60, demand=0),
...           Event(name='Chair D-2', duration=60, demand=0)]

As you can see, we have set all unavailabilities to be empty however Chair A is in fact the speaker for Talk 11. Also Chair B has informed us that they are not present on the first day. We can include these constraints:

>>> events[0].add_unavailability(chair_slots[4])
>>> events[1].add_unavailability(chair_slots[4])
>>> events[2].add_unavailability(*chair_slots[4:])
>>> events[3].add_unavailability(*chair_slots[4:])

Finally, each chair cannot chair more than one session at a time:

>>> events[0].add_unavailability(events[1])
>>> events[2].add_unavailability(events[3])
>>> events[4].add_unavailability(events[5])

Now let us get the chair schedule:

>>> chair_schedule = scheduler.schedule(events, chair_slots)

>>> chair_schedule.sort(key=lambda item: item.slot.starts_at)
>>> for item in chair_schedule:
...     print(f"{item.event.name} chairing {item.slot.starts_at} in {item.slot.venue}")
Chair A-2 chairing 2016-09-15 09:30:00 in Big
Chair B-1 chairing 2016-09-15 09:30:00 in Small
Chair B-2 chairing 2016-09-15 12:30:00 in Small
Chair C-1 chairing 2016-09-15 12:30:00 in Big
Chair A-1 chairing 2016-09-16 12:30:00 in Small
Chair D-2 chairing 2016-09-16 12:30:00 in Big

Validating a schedule

It might of course be helpful to use the tool simply to check if a given schedule is correct: perhaps someone makes a manual change and it is desirable to verify that this is still a valid schedule. Let us first check that our schedule obtained from the algorithm is correct:

>>> from conference_scheduler.validator import is_valid_schedule, schedule_violations
>>> is_valid_schedule(chair_schedule, events=events, slots=chair_slots)
True

Let us modify our schedule so that it schedules an event twice:

>>> from conference_scheduler.resources import ScheduledItem
>>> chair_schedule[0] = ScheduledItem(event=events[2], slot=chair_slots[0])
>>> for item in chair_schedule[:2]:
...     print(f"{item.event.name} chairing {item.slot.starts_at} in {item.slot.venue}")
Chair B-1 chairing 2016-09-15 09:30:00 in Big
Chair B-1 chairing 2016-09-15 09:30:00 in Small

We now see that we have an invalid schedule:

>>> is_valid_schedule(chair_schedule, events=events, slots=chair_slots)
False

We can furthermore identify which constraints were broken:

>>> for v in schedule_violations(chair_schedule, events=events, slots=chair_slots):
...     print(v)
Event either not scheduled or scheduled multiple times - event: 1
Event either not scheduled or scheduled multiple times - event: 2