Search code examples
pythonor-toolscp-sat

Solving assignment problem with OR-TOOLS with number of tasks


I am trying to understand how OR-TOOLS works and wanted to solve the assignment problem but with number of tasks (2 out of 4) as constraints. But my code does not work as expectected.

I have tried:

from ortools.sat.python import cp_model


costs = [
    [90, 80, 75, 70],
    [35, 85, 55, 65],
    [125, 95, 90, 95],
    [45, 110, 95, 115],
    [50, 100, 90, 100],
]
num_workers = len(costs)
num_tasks = len(costs[0])

# Model
model = cp_model.CpModel()

# Variables
x = []
for i in range(num_workers):
    t = []
    for j in range(num_tasks):
        t.append(model.NewBoolVar(f'x[{i},{j}]'))
    x.append(t)

# Constraints
# Each worker is assigned to exactly one task.
for worker in range(num_workers):
    model.AddExactlyOne(x[worker][task] for task in range(num_tasks))

# Choosing number of tasks.
for worker in range(num_workers):
    model.Add(sum([x[worker][task] for task in range(num_tasks)]) == 2)

# Objective
objective_terms = []
for i in range(num_workers):
    for j in range(num_tasks):
        objective_terms.append(costs[i][j] * x[i][j])
model.Minimize(sum(objective_terms))

# Solve
solver = cp_model.CpSolver()
status = solver.Solve(model)

# Print solution.
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f'Total cost = {solver.ObjectiveValue()}')
    print()
    for i in range(num_workers):
        for j in range(num_tasks):
            if solver.BooleanValue(x[i][j]):
                print(
                    f'Worker {i} assigned to task {j} Cost = {costs[i][j]}')
else:
    print('No solution found.')

So how do I define a number of tasks to use as a constraint?

I get "No solution found" as output. But I expect that task 0 and task 3 will be selected and output should look like this

Worker 0 assigned to task 1 Cost = 70
Worker 1 assigned to task 0 Cost = 35
Worker 2 assigned to task 1 Cost = 95
Worker 3 assigned to task 0 Cost = 45
Worker 4 assigned to task 0 Cost = 50

If we assign every worker to only one task and choose two tasks, this should be the optimal solution.


Solution

  • This can be achieved using channeling constraint

    Expressing @AirSquid's idea in CP-SAT syntax:

    task_assigned = [model.NewBoolVar(f"task_assigned{i}") for i in range(num_tasks)]
    for task_id in range(num_tasks):
        sum_expr = sum([x[worker][task_id] for worker in range(num_workers)])
        model.Add(sum_expr >= 1).OnlyEnforceIf(task_assigned[task_id])
        model.Add(sum_expr == 0).OnlyEnforceIf(task_assigned[task_id].Not())
    model.Add(sum(task_assigned) == 2)
    

    This gives the output:

    Total cost = 295.0
    
    Worker 0 assigned to task 2 Cost = 75
    Worker 1 assigned to task 0 Cost = 35
    Worker 2 assigned to task 2 Cost = 90
    Worker 3 assigned to task 0 Cost = 45
    Worker 4 assigned to task 0 Cost = 50