I'm trying to learn Or-Tools for some assignment and scheduling optimizations but I can't find the specific answers I'm looking for.
I don't have a background in maths nor studied optimizations until recently when I started with OR-Tools, so my knowledge is very limited, however, I think I got the basics: the main focus is to get a matrix with 0 and 1 representing False and True but respecting the constraints and then minimize or maximize a row or column.
I tried the example in https://github.com/google/or-tools/blob/stable/examples/python/bus_driver_scheduling_sat.py, it works well but I need to make changes to adapt it to my needs. In the code below I started from the beginning, trying to minimize the number of drivers. I have 6 shifts, only one shift must be assigned to a driver at any point, and a driver can take a shift only if the end time of a shift ([4]) if less or equal to the start time of the next shift ([3]). I think I need a matrix with the shift overlaps (task_overlap) where 0 means those task can't be made one after another and 1 meaning both tasks can be made one after another. I commented the values I think should be in task_overlap and tasks_assignment and tried to get those with the constraints. The problem is that the current code works fine with 3 shifts, but is infeasible with 4 and feasible but wrong with 5 and 6. If I put the constraint "model.Add((tasks_assignment[driver, task]) == 0).OnlyEnforceIf(task_overlap[driver, task].Not())" and "model.Add(driver_time[driver] == 1).OnlyEnforceIf(task_overlap[driver, task])" instead of "model.Add((1 - tasks_assignment[driver, task]) >= tasks_assignment[driver,task2]).OnlyEnforceIf(task_overlap[driver, task])" and "model.Add(driver_time[driver] == 1).OnlyEnforceIf(tasks_assignment[driver, task])", I get the correct result with 3 and 4 but not 5 or 6.
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from ortools.sat.python import cp_model
SAMPLE_SHIFTS_CUSTOM = [
['44321', '08:00:00', '9:30:00', 480, 570, 90], #
['22351', '08:00:00', '8:30:00', 480, 510, 30], ##
['27280', '09:00:00', '9:30:00', 540, 570, 30], ##
# ['27294', '09:30:00', '10:30:00', 570, 630, 60], # | ##
# ['27281', '09:30:00', '12:00:00', 570, 720, 150], # | ##
# ['27295', '10:00:00', '11:00:00', 600, 660, 60,], ###
]
tasks = len(SAMPLE_SHIFTS_CUSTOM)
drivers = len(SAMPLE_SHIFTS_CUSTOM)
driver_time = []
tasks_assignment = {}
task_overlap = {}
model = cp_model.CpModel()
# Vars
for driver in range(drivers):
driver_time.append(model.NewIntVar(0, 1, 'driver_at_%i' % driver))
for driver in range(drivers):
for task in range(tasks):
tasks_assignment[driver, task] = model.NewBoolVar("{}_toTask_{}".format(driver,task))
for task in range(tasks):
for task2 in range(tasks):
task_overlap[task, task2] = model.NewBoolVar("{}_overlaps_{}".format(task,task2))
#task_overlap = { # for 3 shifts #tasks_assignment
# (0, 0): 1, (0, 1): 0, (0, 2): 0, # 1 0 0
# (1, 0): 0, (1, 1): 1, (1, 2): 1, # 0 1 1
# (2, 0): 0, (2, 1): 0, (2, 2): 1, # 0 0 0
#}
#task_overlap = { # for 4 shifts #tasks_assignment
# (0, 0): 1, (0, 1): 0, (0, 2): 0, (0,3): 1, # 1 0 0 1 1 0 0 0
# (1, 0): 0, (1, 1): 1, (1, 2): 1, (1,3): 1, # 0 1 1 0 0 1 1 1
# (2, 0): 0, (2, 1): 0, (2, 2): 1, (2,3): 1, # 0 0 0 0 0 0 0 0
# (3, 0): 0, (3, 1): 0, (3, 2): 0, (3,3): 1, # 0 0 0 0 0 0 0 0
#}
#task_overlap = { # for 5 shifts #tasks_assignment
# (0, 0): 1, (0, 1): 0, (0, 2): 0, (0,3): 1, (0, 4): 1, # 1 0 0 1 0 1 0 0 0 1
# (1, 0): 0, (1, 1): 1, (1, 2): 1, (1,3): 1, (1, 4): 1, # 0 1 1 0 1 0 1 1 1 0
# (2, 0): 0, (2, 1): 0, (2, 2): 1, (2,3): 1, (2, 4): 1, # 0 0 0 0 0 0 0 0 0 0
# (3, 0): 0, (3, 1): 0, (3, 2): 0, (3,3): 1, (3, 4): 0, # 0 0 0 0 0 0 0 0 0 0
# (4, 0): 0, (4, 1): 0, (4, 2): 0, (4,3): 0, (4, 4): 1, # 0 0 0 0 0 0 0 0 0 0
#}
# Constraints
for task in range(tasks):
model.AddExactlyOne(tasks_assignment[driver, task] for driver in range(drivers))
for task in range(tasks):
for task2 in range(tasks):
if task != task2:
model.Add(task_overlap[task, task2] == 1).OnlyEnforceIf(SAMPLE_SHIFTS_CUSTOM[task][4] <= SAMPLE_SHIFTS_CUSTOM[task2][3])
else:
model.Add(task_overlap[task, task2] == 1)
for driver in range(drivers):
for task in range(tasks):
for task2 in range(tasks):
model.Add((1 - tasks_assignment[driver, task]) >= tasks_assignment[driver,task2]).OnlyEnforceIf(task_overlap[driver, task])
#model.Add((tasks_assignment[driver, task]) == 0).OnlyEnforceIf(task_overlap[driver, task].Not())
for driver in range(drivers):
for task in range(tasks):
model.Add(driver_time[driver] == 1).OnlyEnforceIf(tasks_assignment[driver, task]) #correct for 3 shifts
#model.Add(driver_time[driver] == 1).OnlyEnforceIf(task_overlap[driver, task]) #correct for 3 and 4 shifts
# Objective
model.Minimize(sum(driver_time))
solver = cp_model.CpSolver()
#solver.parameters.log_search_progress = True
status = solver.Solve(model)
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
print(f"Total cost = {solver.ObjectiveValue()}\n")
for driver in range(drivers):
for task in range(tasks):
if solver.BooleanValue(tasks_assignment[driver, task]):
print(
f"Worker {driver+1} assigned to task {task+1}."
)
elif status == cp_model.INFEASIBLE:
print("Infeasible.")
else:
print("Unknown status: {}.".format(status))
I believe the problem is with the constraint "model.Add((1 - tasks_assignment[driver, task]) >= tasks_assignment[driver,task2]).OnlyEnforceIf(task_overlap[driver, task])", but I have no idea what to put there or even if I need more constraints. Furthermore, I don't understand why the changes I made gave me a correct answer with 3 and 4 shifts. I appreciate any help.