I am working on a small project to learn linear programming.
I have formulated a problem where we want to assign P people to N projects. Each person give a preference, 2 (strongly desired, 1 desired, 0 not desired) for each of the N project.
We define preferences as a matrix of size PxN.
then, we create a boolean matrix D PxN that represent the decision, of in which project each person is going to be.
from pulp import *
import pandas as pd
preferences = [
[2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 2, 0, 2, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0]
]
N = len(preferences[0]) # Number of projects
P = len(preferences) # Number of people
D = [[LpVariable("D[%i,%i]" % (i, j), 0, 1, LpInteger) for j in range(N)] for i in range(P)]
And I'd like to maximize preferences * D (where * is the element-wise multiplication).
I am defining some constraints:
- A person can be assigned to only 1 project
for i in range(P):
prob += lpSum(D[i][j] for j in range(N)) == 1
- A project can't have only one person, (but a project can have 0 people assigned to it, which means that the project is discarded).
To enforce the second constraint, I followed this suggestion. https://math.stackexchange.com/questions/37075/how-can-not-equals-be-expressed-as-an-inequality-for-a-linear-programming-model.
B = 100 # large value
Z = [LpVariable("Z[%i]" % j, 0, 1, LpInteger) for j in range(N)] #Auxiliary boolean
for j in range(N):
prob += lpSum(D[i][j] for i in range(P)) <= B * Z[j]
prob += lpSum(D[i][j] for i in range(P)) >= 2-B+B*Z[j]
So far everything seems to work, the result D that maximize the function is a binary matrix that respect all the constraints.
Example output for D:
1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0
The problems come when I try to penalize groups of size different from 3, the more they get bigger or smaller than 3. (I'd actually like to do it for 3 and 4, starting from penalizing only at size 2 and size >=5, but better start simple).
To do so, I create a vector "Size penalty" (SP) and I try to enforce
|Sum(D[i][j] for i in range(P)) - 3| <= SP[j]
breaking down this modulo into two equations
Sum(D[i][j] for i in range(P)) - 3 <= SP[j] and
Sum(D[i][j] for i in range(P)) - 3 >= -SP[j]
SP1 = [LpVariable("SP[%i]" % j, 0, 1, LpInteger) for j in range(N)]
for j in range(N):
prob += lpSum(D[i][j] for i in range(P)) - 3 <= SP1[j]
prob += lpSum(D[i][j] for i in range(P)) - 3 >= -SP1[j]
and adding the Sum(SP) to the function to maximize
f = M * D - SUM(SP).
prob += lpSum(D[i][j] * preferences[i][j] for i in range(P) for j in range(N)) - lpSum(SP1[j] for j in range(N))
When I add this, the model still works, but it somehow breaks the constraint of D being an integer between 0 and 1. Example of broken output:
1.0 -3.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0
1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0
0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0
0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 1.0 0.0 -2.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 -8.0 9.0
0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 1.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 1.0 0.0 -1.0 0.0 1.0 0.0 -13.0 0.0 0.0 1.0 0.0 12.0 0.0
0.0 1.0 -1.0 1.0 1.0 -3.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0
-1.0 0.0 0.0 0.0 1.0 1.0 0.0 0.0 2.0 0.0 0.0 0.0 -2.0 0.0
1.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 3.0 1.0 -8.0
How can this be, considering that I defined D as Lp integers?
D = [[LpVariable("D[%i,%i]" % (i, j), 0, 1, LpInteger) for j in range(N)] for i in range(P)]
Full code:
from pulp import *
import pandas as pd
preferences = [
[2, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0],
[0, 0, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 2],
[0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, 0, 2, 0, 2, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0]
]
N = len(preferences[0]) # Number of projects
P = len(preferences) # Number of people
B = 100 # B large value
# Create the 'prob' variable to contain the problem data
prob = LpProblem("Project Assignment", LpMaximize)
# Variables
# D[i, j] = 1 if person i is assigned to project j and 0 otherwise. To be optimized
D = [[LpVariable("D[%i,%i]" % (i, j), 0, 1, LpInteger) for j in range(N)] for i in range(P)]
#Define auxiliary Z[N] for inequality
Z = [LpVariable("Z[%i]" % j, 0, 1, LpInteger) for j in range(N)]
#Define size penalty SP[N] variable as int for each project
SP1 = [LpVariable("SP[%i]" % j, 0, 1, LpInteger) for j in range(N)]
# Constraints
# Each person is assigned to exactly one project, use Add(sum(D[i, j]) == 1) for each person i
for i in range(P):
prob += lpSum(D[i][j] for j in range(N)) == 1
# Each project has not 1 person assigned. Use auxiliary variable z to express inequality
# https://math.stackexchange.com/questions/37075/how-can-not-equals-be-expressed-as-an-inequality-for-a-linear-programming-model
for j in range(N):
prob += lpSum(D[i][j] for i in range(P)) <= B * Z[j]
prob += lpSum(D[i][j] for i in range(P)) >= 2-B+B*Z[j]
#Add penalty the more the number of people is different from 3 for each project
for j in range(N):
prob += lpSum(D[i][j] for i in range(P)) - 3 <= SP1[j]
prob += lpSum(D[i][j] for i in range(P)) - 3 >= -SP1[j]
# Objective
# Maximize the total preference score
prob += lpSum(D[i][j] * preferences[i][j] for i in range(P) for j in range(N)) - lpSum(SP1[j] for j in range(N))
# Solve
status = prob.solve()
#print D.values()
for i in range(P):
for j in range(N):
print(D[i][j].value(), end=" ")
print()
#print Z.values()
print("Zeta values")
for j in range(N):
print(Z[j].value(), end=" ")
#Assert that sum(D[i][j]) over i is != 1 for each column j
for j in range(N):
assert sum(D[i][j].value() for i in range(P)) != 1