1 of 128

Finding the Perfect�Shift Schedule

PyCon APAC 2022

Shung-Hsi Yu

2 of 128

About Me

Shung-Hsi Yu (@shunghsiyu)

Kernel Engineer at

working from Taitung

3 of 128

Scheduling Problem

4 of 128

Situation where you…

Allocate resource

    • e.g. worker
    • usually limited

For some purpose

    • e.g. task/job/shift

Under some rules

    • e.g. 8 hours a day

5 of 128

In fact, you being here mean…

  1. PyCon organizers scheduling each talk
    • meeting rooms are the limited resource

  • You're experiencing a scheduling problem
    • choosing talks to listen
    • your time & attention is the limited resource

6 of 128

Shift Scheduling

a.k.a. employee rostering

Timeslot

    • e.g. Monday, Tuesday

Person

    • more commonly referred as employee
    • the limited resource

7 of 128

Shift Scheduling

a.k.a. employee rostering

Fairly similar to solving Sudoku

Instead of reinventing the wheel…

use existing solvers

8 of 128

Using Solvers

Magical tools

Describe the problem

instead of solving it

Too good to be true?

9 of 128

Describing problem to a solver

Alice works on Monday ∧ ¬Bob works on Monday ∧ ¬Carol works on Monday�∨�¬Alice works on Monday ∧ Bob works on Monday ∧ ¬Carol works on Monday�∨�¬Alice works on Monday ∧ ¬Bob works on Monday ∧ Carol works on Monday

10 of 128

Modeling the Problem

11 of 128

Encoding the Model

Person

from dataclasses import dataclass # Python 3.7+

@dataclass

class Person:

name: str

12 of 128

Encoding the Model

Person

Three person/employees

persons = [

Person('Alice'),

Person('Bob'),

Person('Carol'),

]

13 of 128

Encoding the Model

Timeslot

Just one timeslot, for now

@dataclass

class TimeSlot:

name: str # Let's start simple

14 of 128

Encoding the Model

Timeslot

Just one timeslot, for now

@dataclass

class TimeSlot:

name: str # Let's start simple

monday = TimeSlot('Monday')

15 of 128

Encoding the Model

How to encode Schedule?

Life would be easy if…

class Schedule:

time: TimeSlot

assignee: Person

scheds = solver(persons, timeslots=[monday])

len(scheds) == 1

scheds[0].assignee.name == 'Bob'

16 of 128

Encoding the Model

How to encode Schedule?

Life would be easy if…

class Schedule:

time: TimeSlot

assignee: Person

scheds = solver(persons, timeslots=[monday])

len(scheds) == 1

scheds[0].assignee.name == 'Bob'

17 of 128

Encoding the Model

How to encode Schedule?

Life would be easy if…

class Schedule:

time: TimeSlot

assignee: Person

scheds = solver(persons, timeslots=[monday])

len(scheds) == 1

scheds[0].assignee.name == 'Bob'

18 of 128

Basic of Solvers

Solvers work with Boolean Variables*

* Actually, SMT solvers like Z3 can work with other variables such as Bit Vector, custom Data-type, etc. It’s the SAT solvers that only work with Boolean Variables.

19 of 128

Basic of Solvers

Solvers work with Boolean Variables

Bool('It is sunny outside')

ID / Name

20 of 128

Encoding the Model

Turning an (ideal) Python class into Boolean Variable

class Schedule:

time: TimeSlot

assignee: Person

Bool('...')

?

21 of 128

Encoding the Model

Turning an (ideal) Python class into Boolean Variable

class Schedule:

time: TimeSlot

assignee: Person

Bool('...')

Bool('...')

Bool('...')

22 of 128

Encoding the Model

# Represent whether Alice is works on Monday

alice_works_on_monday = Bool('Alice works on Monday')

# Similar Bob

bob_works_on_monday = Bool('Bob works on Monday')

# and Carol

carol_works_on_monday = Bool('Carol works on Monday')

23 of 128

Basic of Solvers

Solver decides it’s value automatically

Bool('Alice works on Monday')

True

False

?

?

24 of 128

Rules

25 of 128

One Person per Shift

Each shift only needs one person (for now)

26 of 128

One Person per Shift

Only one of them should be working, so either

  1. Alice works, while Bob and Carol does not, or
  2. Bob works, while Alice and Carol does not, or
  3. Carol works, while Alice and Bob does not

27 of 128

One Person per Shift

Only one of them should be working, so either

  • Alice works, while Bob and Carol does not, or
  • Bob works, while Alice and Carol does not, or
  • Carol works, while Alice and Bob does not

28 of 128

One Person per Shift

Here’s how you represent the observation

Alice works, while Bob and Carol does not

And(

alice_works_on_monday == True,

bob_works_on_monday == False,

carol_works_on_monday == False,

)

29 of 128

One Person per Shift

Here’s how you represent the observation

Alice works, while Bob and Carol does not

And(

alice_works_on_monday == True,

bob_works_on_monday == False,

carol_works_on_monday == False,

)

30 of 128

One Person per Shift

Here’s how you represent the observation

Alice works, while Bob and Carol does not

And(

alice_works_on_monday == True,

bob_works_on_monday == False,

carol_works_on_monday == False,

)

31 of 128

One Person per Shift

Here’s how you represent the observation

Alice works, while Bob and Carol does not

And(

alice_works_on_monday == True,

bob_works_on_monday == False,

carol_works_on_monday == False,

)

32 of 128

One Person per Shift

Here’s how you represent the observation

Alice works, while Bob and Carol does not

And(

alice_works_on_monday == True,

bob_works_on_monday == False,

carol_works_on_monday == False,

)

33 of 128

One Person per Shift

Here’s how you represent the observation

Alice works, while Bob and Carol does not

And(

alice_works_on_monday,

Not(bob_works_on_monday),

Not(carol_works_on_monday),

)

34 of 128

One Person per Shift

Only one of them should be working, so either

  • Alice works, while Bob and Carol does not, or
  • Bob works, while Alice and Carol does not, or
  • Carol works, while Alice and Bob does not

35 of 128

One Person per Shift

Only one of them should be working, so either

  • Alice works, while Bob and Carol does not, or
  • Bob works, while Alice and Carol does not, or
  • Carol works, while Alice and Bob does not

36 of 128

One Person per Shift

Only one of them should be working, so either

  • Alice works, while Bob and Carol does not, or
  • Bob works, while Alice and Carol does not, or
  • Carol works, while Alice and Bob does not

37 of 128

One Person per Shift

Or(

# Only Alice is working

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

# Only Bob is working

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

# Only Carol is working

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

38 of 128

One Person per Shift

Or(

# Only Alice is working

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

# Only Bob is working

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

# Only Carol is working

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

39 of 128

One Person per Shift

Or(

# Only Alice is working

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

# Only Bob is working

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

# Only Carol is working

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

40 of 128

One Person per Shift

Or(

# Only Alice is working

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

# Only Bob is working

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

# Only Carol is working

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

41 of 128

One Person per Shift

Or(

# Only Alice is working

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

# Only Bob is working

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

# Only Carol is working

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

42 of 128

One Person per Shift

only_one_works_monday = Or(

# Only Alice is working

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

# Only Bob is working

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

# Only Carol is working

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

43 of 128

One Person per Shift

Plugging it in…

solve(

only_one_works_monday,

)

[Bob works on Monday = True,

Carol works on Monday = False,

Alice works on Monday = False]

44 of 128

One Person per Shift

Plugging it in…

solve(

only_one_works_monday,

)

[Bob works on Monday = True,

Carol works on Monday = False,

Alice works on Monday = False]

Ta-da!

45 of 128

Scaling Up

More timeslots

timeslots = [

TimeSlot('Monday'),

TimeSlot('Tuesday'),

TimeSlot('Wednesday'),

TimeSlot('Thursday'),

...

TimeSlot('Sunday'),

]

46 of 128

Scaling Up

More timeslots

solve(

only_one_works_monday,

only_one_works_tuesday,

only_one_works_wednesday,

only_one_works_thursday,

...

only_one_works_sunday,

)

47 of 128

Scaling Up

Getting rid of hard-coding

alice_works_on_monday = Bool('Alice works on Monday')

only_one_works_monday = Or(

And(alice_works..., Not(bob_work...),

Not(carol_works_on_monday)),

...

)

48 of 128

Scaling Up

Getting rid of hard-coding

alice_works_on_monday = Bool('Alice works on Monday')

only_one_works_monday = Or(

And(alice_works..., Not(bob_work...),

Not(carol_works_on_monday)),

...

)

49 of 128

Refactoring

50 of 128

Getting rid of hard-coding

in Bool()

def get_var(timeslot, person):

name = person.name

day = timeslot.name

return Bool(f'{name} works on {day}')

51 of 128

Getting rid of hard-coding

in Bool()

alice_works_on_monday = Bool('Alice works on Monday')

person = Person('Alice')

timeslot = Timeslot('Monday')

alice_works_on_monday = get_var(timeslot, person)

52 of 128

Getting rid of hard-coding

in Bool()

alice_works_on_monday = Bool('Alice works on Monday')

person = Person('Alice')

timeslot = Timeslot('Monday')

alice_works_on_monday = get_var(timeslot, person)

53 of 128

Getting rid of hard-coding

in Bool()

def get_vars(timeslot, persons):

vars = [get_var(timeslot, person)

for person in persons]

return vars

54 of 128

Getting rid of hard-coding

in Bool()

def get_vars(timeslot, persons):

vars = [get_var(timeslot, person)

for person in persons]

return vars

alice_works_on_monday, bob..., carol... = \

get_vars(monday, persons)

55 of 128

Simplifying the Rule

only_one_works_monday = Or(

And(alice_works..., Not(bob_work...),� Not(carol_works_on_monday)),

And(Not(alice_works...), bob_works...,

Not(carol_works_on_monday)),

And(Not(alice_works...), Not(bob_works…),

carol_works_on_monday),

)

56 of 128

Simplifying the Rule

Using helpers

AtMost(..., n)

AtLeast(..., n)

57 of 128

Simplifying the Rule

Using helpers

AtMost(..., n)

AtLeast(..., n)

58 of 128

Simplifying the Rule

Using helpers

AtMost(..., n)

AtLeast(..., n)

59 of 128

Simplifying the Rule

Using helpers

Either one or none of them works

at_most_one_works_monday = AtMost(

alice_works_on_monday,

bob_works_on_monday,

carol_works_on_monday,

1

)

60 of 128

Simplifying the Rule

Using helpers

Either one or none of them works

at_most_one_works_monday = AtMost(

*get_vars(monday, persons),

1

)

61 of 128

Simplifying the Rule

Using helpers

Either one, two, or three of them works

at_least_one_works_monday = AtLeast(

*get_vars(monday, persons),

1

)

62 of 128

Simplifying the Rule

Using helpers

only_one_works_monday = And(

at_most_one_works_monday,

at_least_one_works_monday,

)

63 of 128

Simplifying the Rule

Creating a helper

def ExactlyOne(vars):

return And(AtMost(*vars, 1), AtLeast(*vars, 1))

64 of 128

Simplifying the Rule

Creating a helper

def ExactlyOne(vars):

return And(AtMost(*vars, 1), AtLeast(*vars, 1))

only_one_works_monday = \

ExactlyOne(get_vars(monday, persons))

65 of 128

Putting it All Together

rules = [] # Store the rules for re-use

# For each timeslot, only one person works

for timeslot in timeslots:

working = get_vars(timeslot, persons)

rules.append(ExactlyOne(working))

solve(rules) # Call the solver

66 of 128

Putting it All Together

rules = [] # Store the rules for re-use

# For each timeslot, only one person works

for timeslot in timeslots:

working = get_vars(timeslot, persons)

rules.append(ExactlyOne(working))

solve(rules) # Call the solver

67 of 128

Putting it All Together

rules = [] # Store the rules for re-use

# For each timeslot, only one person works

for timeslot in timeslots:

working = get_vars(timeslot, persons)

rules.append(ExactlyOne(working))

solve(rules) # Call the solver

68 of 128

A schedule for the week

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Alice

Bob

Bob

Carol

Alice

Alice

Alice

69 of 128

A schedule for the week

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Alice

Bob

Bob

Carol

Alice

Alice

Alice

70 of 128

More Rules

71 of 128

Fairness

Making people equally (un)happy

Can be a tricky subject

Using an objective measure:

Number of shifts

72 of 128

Fairness

Making people equally (un)happy

Find the ideal number of shift each person should take

num_shifts, num_persons = len(timeslots), len(persons)

min_shifts = math.floor(num_shifts / num_persons)

max_shifts = math.ceil(num_shifts / num_persons)

73 of 128

Fairness

Making people equally (un)happy

for person in persons:

# shifts belonging to person

shifts = [get_var(timeslot, person)

for timeslot in timeslots]

rules.append(AtLeast(*shifts, min_shifts))

rules.append(AtMost(*shifts, max_shifts))

solve(rules)

74 of 128

Fairness

Making people equally (un)happy

for person in persons:

# shifts belonging to person

shifts = [get_var(timeslot, person)

for timeslot in timeslots]

rules.append(AtLeast(*shifts, min_shifts))

rules.append(AtMost(*shifts, max_shifts))

solve(rules)

75 of 128

A fairer schedule for the week

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Carol

Bob

Bob

Carol

Alice

Alice

Alice

76 of 128

Days-off

Certain person may not be available for certain timeslot

How to not assign them?

77 of 128

Days-off

Let’s say Carol can’t work on Monday due to urgent issue

rules.append(

get_var(monday, Person('Carol')) == False

)

78 of 128

Days-off

Let’s say Carol can’t work on Monday due to urgent issue

rules.append(

Not(get_var(monday, Person('Carol')))

)

79 of 128

Carol taking day-off on Monday

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Bob

Carol

Bob

Carol

Alice

Alice

Alice

80 of 128

Days-off

What if besides Carol,

Bob and Alice both prefer not to work on Monday

rules.append(

get_var(monday, Person('Alice')) == False

)

rules.append(

get_var(monday, Person('Bob')) == False

)

81 of 128

Solver return value

UNSAT

82 of 128

Days-off

No one can be assigned for Monday

# Urgent issue

get_var(monday, Person('Carol')) == False

# Prefer not to work on Monday

get_var(monday, Person('Alice')) == False

get_var(monday, Person('Bob')) == False

83 of 128

No schedule can satisfy the rules

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

-

-

-

-

-

-

-

84 of 128

Days-off

Better way to express preference?

85 of 128

Soft Constraints

86 of 128

Expressing Preference

We create a solver variable to keep track of

how bad/good the schedule is

87 of 128

Expressing Preference

We create a solver variable to keep track of

how bad/good the schedule is

# Higher value means worst schedule

penalty = IntVal(0)

88 of 128

Expressing Preference

Increase it everything something not preferred happens

Alice prefers not to work on Monday

penalty += 1 if get_var(monday, Person('Alice')) else 0

89 of 128

Expressing Preference

Increase it everything something not preferred happens

Alice prefers not to work on Monday

penalty += 1 if get_var(monday, Person('Alice')) else 0

90 of 128

Expressing Preference

Increase it everything something not preferred happens

Alice prefers not to work on Monday

penalty += 1 if get_var(monday, Person('Alice')) else 0

solver boolean variables,

not a Python boolean

91 of 128

Expressing Preference

Increase it everything something not preferred happens

Alice prefers not to work on Monday

penalty += If(get_var(monday, Person('Alice')), 1, 0)

92 of 128

Expressing Preference

def var_to_int(var):

return If(var, 1, 0)

def get_int(timeslot, person):

return var_to_int(get_var(timeslot, person))

93 of 128

Expressing Preference

def var_to_int(var):

return If(var, 1, 0)

def get_int(timeslot, person):

return var_to_int(get_var(timeslot, person))

penalty += get_int(monday, Person('Alice'))

94 of 128

Putting it Together

# Hard constraints

rules.append(

get_var(monday, Person('Carol')) == False

)

# Soft constraints

penalty += get_int(monday, Person('Alice'))

penalty += get_int(monday, Person('Bob'))

95 of 128

Putting it Together

# Hard constraints

rules.append(

get_var(monday, Person('Carol')) == False

)

# Soft constraints

penalty += get_int(monday, Person('Alice'))

penalty += get_int(monday, Person('Bob'))

96 of 128

Putting it Together

# Hard constraints

rules.append(

get_var(monday, Person('Carol')) == False

)

# Soft constraints

penalty += get_int(monday, Person('Alice'))

penalty += get_int(monday, Person('Bob'))

97 of 128

Putting it Together

Use the Optimize solver, which support soft-constraints

solver = Optimize()

solver.add(rules)

solver.minimize(penalty)

solver.check()

model = solver.model() # the schedule

98 of 128

A schedule for the week with soft-constraints

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Bob

Alice

Carol

Bob

Carol

Alice

Alice

99 of 128

Quick Summary

The basic of scheduling

We’ve covered:

  • Encoding scheduling problem with boolean variables
  • One person per shift rule
  • Shift fairness
  • Using soft-constraints to express preferences

100 of 128

Advance Scheduling

101 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

102 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

non-working day

103 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

two-person shifts

104 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

(special) two-person shifts

105 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

consistent schedule on the same week day

106 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

intern is not allowed to work alone

107 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

Alice doesn’t want to work on Monday and Tuesday of the first week

Bob doesn’t want to work on Wednesdays

(NOT entirely fulfilled)

Dave prefers to work one day on the weekend consistently

Erin doesn’t want to on the last Friday

108 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

2x load

4x load

109 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

Alice

Bob

Carol

Dave

Erin

Shifts

7

9

13

5

10

Load

11

14

13

11

13

110 of 128

Monday

Tuesday

Wednesday

Thursday

Friday

Saturday

Sunday

Week 1

Carol, Erin

Carol, Erin

Alice

Alice, Bob

Dave

Bob

Week 2

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Dave

Dave

Bob

Week 3

Bob, Carol

Carol, Erin

Carol

Carol, Erin

Alice, Bob

Dave

Alice

Week 4

Carol, Erin

Carol, Erin

Bob

Carol, Erin

Alice, Bob

Dave, Erin

Alice, Bob

Alice

Bob

Carol

Dave

Erin

Shifts

7

9

13

5

10

Load

11

14

13

11

13

111 of 128

Irregular Schedules

Some shifts have:

  • Two person
  • Zero person (i.e. non-working day)

112 of 128

Irregular Schedules

@dataclass

class TimeSlot:

week: int

day: WeekDay

num_worker: int = 1

load: int = 1

class WeekDay(Enum):

Monday = auto()

Tuesday = auto()

Wednesday = auto()

Thursday = auto()

...

Sunday = auto()

113 of 128

Irregular Schedules

timeslots = [

TimeSlot(week=1, day=WeekDay.Monday, num_worker=2),

TimeSlot(week=1, day=WeekDay.Tuesday, num_worker=2),

TimeSlot(week=1, day=WeekDay.Wednesday),

TimeSlot(week=1, day=WeekDay.Thursday, num_worker=0),

TimeSlot(week=1, day=WeekDay.Friday, num_worker=2),

TimeSlot(week=4, day=WeekDay.Sunday, num_worker=2,

load=4),

]

114 of 128

Spreading Loads Evenly

With soft constraints penalizing large deviation

for person in persons:

loads = sum(

get_int(timeslot, person))*timeslot.load

for timeslot in timeslots

)

deviation = If(

loads < min_loads,

loads - min_loads,

If(loads > max_loads,

max_loads - loads, 0)

)

penalty += weight * deviation**2

115 of 128

Spreading Loads Evenly

With soft constraints penalizing large deviation

for person in persons:

loads = sum(

get_int(timeslot, person))*timeslot.load

for timeslot in timeslots

)

deviation = If(

loads < min_loads,

loads - min_loads,

If(loads > max_loads,

max_loads - loads, 0)

)

penalty += weight * deviation**2

116 of 128

Spreading Loads Evenly

With soft constraints penalizing large deviation

for person in persons:

loads = sum(

get_int(timeslot, person))*timeslot.load

for timeslot in timeslots

)

deviation = If(

loads < min_loads,

loads - min_loads,

If(loads > max_loads,

max_loads - loads, 0)

)

penalty += weight * deviation**2

117 of 128

Conclusion

118 of 128

Compared to a Custom Scheduler

The Good

  • (Usually) less code ⇒ less bugs & faster to write
    • and easier to maintain
  • Gets better over time
  • Runs fast enough

119 of 128

Compared to a Custom Scheduler

The Bad

  • Can be intimidating at first
  • Solving time is not deterministic
  • Tuning requires in-depth knowledge of solvers
    • e.g. need to know what tactics are

120 of 128

My Suggestion for Writing a Scheduler

General suggestions

  • Rules should be known and agreed upon
  • Best for mundane, recurring schedule
  • Graphical user interface
    • Or take & output Excel/CSV
  • Soft constraints make all the difference

121 of 128

My Suggestion for Writing a Scheduler

Picking a tool

Solver-based Tools

  • Z3, OR-Tools, Gurobi

High-level Tools

  • OptaPlanner/OptaPy

122 of 128

My Suggestion for Writing a Scheduler

Picking a tool

Solver-based Tools

  • Z3, OR-Tools, Gurobi

High-level Tools

  • OptaPlanner/OptaPy

What this talk is about

123 of 128

My Suggestion for Writing a Scheduler

Picking a tool

Solver-based Tools

  • Z3, OR-Tools, Gurobi

High-level Tools

  • OptaPlanner/OptaPy

Powerful, flexible

Math > Programming

124 of 128

My Suggestion for Writing a Scheduler

Picking a tool

Solver-based Tools

  • Z3, OR-Tools, Gurobi

High-level Tools

  • OptaPlanner/OptaPy

Easier for scheduling

Object-oriented approach

125 of 128

My Suggestion for Writing a Scheduler

Picking a tool

Solver-based Tools

  • Z3, OR-Tools, Gurobi

High-level Tools

  • OptaPlanner/OptaPy

If you need a scheduler made right away

Learning for fun & intellectual challenges

Portability

126 of 128

Other Superpowers of Z3

with also applied to other SMT solvers

  • Software verification
  • Program synthesis
  • Dependency resolution
  • Auto-layout
  • and many more…

127 of 128

The End

128 of 128

Happy Scheduling!