Nurse Rostering Code with custom constraints Not Working

Could someone please check this code. I modified constraints as :

• No. of nurses - 10
• Max No. of nurses in Morning/Afternoon/Evening Shifts - 3/4/2
• Max no. of shifts in a week per nurse - 5
from ortools.sat.python import cp_model

class NursesPartialSolutionPrinter(cp_model.CpSolverSolutionCallback):
"""Print intermediate solutions."""

def __init__(self, shifts, num_nurses, num_days, num_shifts, sols):
cp_model.CpSolverSolutionCallback.__init__(self)
self._shifts = shifts
self._num_nurses = num_nurses
self._num_days = num_days
self._num_shifts = num_shifts
self._solutions = set(sols)
self._solution_count = 0

def on_solution_callback(self):
if self._solution_count in self._solutions:
print('Solution %i' % self._solution_count)
for d in range(self._num_days):
print('Day %i' % d)
for n in range(self._num_nurses):
is_working = False
for s in range(self._num_shifts):
if self.Value(self._shifts[(n, d, s)]):
is_working = True
print('  Nurse %i works shift %i' % (n, s))
if not is_working:
print('  Nurse {} does not work'.format(n))
print()
self._solution_count += 1

def solution_count(self):
return self._solution_count

def main():
# Data.
num_nurses = 10
num_shifts = 3
num_days = 7
all_nurses = range(num_nurses)
all_shifts = range(num_shifts)
all_days = range(num_days)
# Creates the model.
model = cp_model.CpModel()

# Creates shift variables.
# shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'.
shifts = {}
for n in all_nurses:
for d in all_days:
for s in all_shifts:
shifts[(n, d,
s)] = model.NewBoolVar('shift_n%id%is%i' % (n, d, s))

# Each shift is assigned to exactly one nurse in the schedule period.
for d in all_days:
for s in all_shifts:
if s==0:
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) <= 3)
if s==1:
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) <= 4)
if s==2:
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) <= 2)

# Each nurse works at most one shift per day.
for n in all_nurses:
for d in all_days:
model.Add(sum(shifts[(n, d, s)] for s in all_shifts) <= 1)

# Try to distribute the shifts evenly, so that each nurse works
# min_shifts_per_nurse shifts. If this is not possible, because the total
# number of shifts is not divisible by the number of nurses, some nurses will
# be assigned one more shift.
min_shifts_per_nurse = (num_shifts * num_days) // num_nurses
if num_shifts * num_days % num_nurses == 0:
max_shifts_per_nurse = min_shifts_per_nurse
else:
max_shifts_per_nurse = 5
for n in all_nurses:
num_shifts_worked = 0
for d in all_days:
for s in all_shifts:
num_shifts_worked += shifts[(n, d, s)]

# Creates the solver and solve.
solver = cp_model.CpSolver()
solver.parameters.linearization_level = 0
# Display the first five solutions.
a_few_solutions = range(5)
solution_printer = NursesPartialSolutionPrinter(shifts, num_nurses,
num_days, num_shifts,
a_few_solutions)
solver.SearchForAllSolutions(model, solution_printer)

# Statistics.
print()
print('Statistics')
print('  - conflicts       : %i' % solver.NumConflicts())
print('  - branches        : %i' % solver.NumBranches())
print('  - wall time       : %f s' % solver.WallTime())
print('  - solutions found : %i' % solution_printer.solution_count())

if __name__ == '__main__':
main()

It's not showing correct solutions as per constraints:

Solution 0
Day 0
Nurse 0 works shift 1
Nurse 1 works shift 0
Nurse 2 works shift 0
Nurse 3 does not work
Nurse 4 works shift 0
Nurse 5 works shift 1
Nurse 6 works shift 1
Nurse 7 works shift 1
Nurse 8 works shift 2
Nurse 9 works shift 2
Day 1
Nurse 0 works shift 2
Nurse 1 does not work
Nurse 2 works shift 1
Nurse 3 does not work
Nurse 4 does not work
Nurse 5 works shift 1
Nurse 6 works shift 1
Nurse 7 works shift 0
Nurse 8 works shift 0
Nurse 9 works shift 0
Day 2
Nurse 0 does not work
Nurse 1 does not work
Nurse 2 does not work
Nurse 3 does not work
Nurse 4 does not work
Nurse 5 does not work
Nurse 6 does not work
Nurse 7 does not work
Nurse 8 does not work
Nurse 9 does not work
Day 3
Nurse 0 does not work
Nurse 1 does not work
Nurse 2 does not work
Nurse 3 works shift 1
Nurse 4 works shift 1
Nurse 5 does not work
Nurse 6 does not work
Nurse 7 does not work
Nurse 8 does not work
Nurse 9 does not work
Day 4
Nurse 0 does not work
Nurse 1 does not work
Nurse 2 does not work
Nurse 3 does not work
Nurse 4 does not work
Nurse 5 does not work
Nurse 6 does not work
Nurse 7 does not work
Nurse 8 does not work
Nurse 9 does not work
Day 5
Nurse 0 does not work
Nurse 1 does not work
Nurse 2 does not work
Nurse 3 does not work
Nurse 4 does not work
Nurse 5 does not work
Nurse 6 does not work
Nurse 7 does not work
Nurse 8 does not work
Nurse 9 does not work
Day 6
Nurse 0 does not work
Nurse 1 works shift 2
Nurse 2 does not work
Nurse 3 works shift 2
Nurse 4 does not work
Nurse 5 does not work
Nurse 6 does not work
Nurse 7 does not work
Nurse 8 does not work
Nurse 9 does not work

I didn't added lower bound for shift requirements so here's working code:

from ortools.sat.python import cp_model

class NursesPartialSolutionPrinter(cp_model.CpSolverSolutionCallback):
"""Print intermediate solutions."""

def __init__(self, shifts, num_nurses, num_days, num_shifts, sols):
cp_model.CpSolverSolutionCallback.__init__(self)
self._shifts = shifts
self._num_nurses = num_nurses
self._num_days = num_days
self._num_shifts = num_shifts
self._solutions = set(sols)
self._solution_count = 0

def on_solution_callback(self):
if self._solution_count in self._solutions:
print('Solution %i' % self._solution_count)
for d in range(self._num_days):
print('Day %i' % d)
for n in range(self._num_nurses):
is_working = False
for s in range(self._num_shifts):
if self.Value(self._shifts[(n, d, s)]):
is_working = True
print('  Nurse %i works shift %i' % (n, s))
if not is_working:
print('  Nurse {} does not work'.format(n))
print()
self._solution_count += 1

def solution_count(self):
return self._solution_count

def main():
# Data.
num_nurses = 10
num_shifts = 3
num_days = 7
all_nurses = range(num_nurses)
all_shifts = range(num_shifts)
all_days = range(num_days)
# Creates the model.
model = cp_model.CpModel()

# Creates shift variables.
# shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'.
shifts = {}
for n in all_nurses:
for d in all_days:
for s in all_shifts:
shifts[(n, d,
s)] = model.NewBoolVar('shift_n%id%is%i' % (n, d, s))

# Each shift is assigned to exactly one nurse in the schedule period.
for d in all_days:
for s in all_shifts:
if s==0:
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) <= 3)
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) >= 2)
if s==1:
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) <= 4)
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) >= 2)
if s==2:
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) <= 2)
model.Add(sum(shifts[(n, d, s)] for n in all_nurses) >= 1)

# Each nurse works at most one shift per day.
for n in all_nurses:
for d in all_days:
model.Add(sum(shifts[(n, d, s)] for s in all_shifts) <= 1)

# Try to distribute the shifts evenly, so that each nurse works
# min_shifts_per_nurse shifts. If this is not possible, because the total
# number of shifts is not divisible by the number of nurses, some nurses will
# be assigned one more shift.
min_shifts_per_nurse = (num_shifts * num_days) // num_nurses
if num_shifts * num_days % num_nurses == 0:
max_shifts_per_nurse = min_shifts_per_nurse
else:
max_shifts_per_nurse = 5
for n in all_nurses:
num_shifts_worked = 0
for d in all_days:
for s in all_shifts:
num_shifts_worked += shifts[(n, d, s)]

# Creates the solver and solve.
solver = cp_model.CpSolver()
solver.parameters.linearization_level = 0
# Display the first five solutions.
a_few_solutions = range(20)
solution_printer = NursesPartialSolutionPrinter(shifts, num_nurses,
num_days, num_shifts,
a_few_solutions)
solver.SearchForAllSolutions(model, solution_printer)

# Statistics.
print()
print('Statistics')
print('  - conflicts       : %i' % solver.NumConflicts())
print('  - branches        : %i' % solver.NumBranches())
print('  - wall time       : %f s' % solver.WallTime())
print('  - solutions found : %i' % solution_printer.solution_count())

if __name__ == '__main__':
main()