Minimizing the total amount of drivers with OR-Tools CP-SAT

76 Views Asked by At

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.

0

There are 0 best solutions below