Python Linear Programming Binary exclusive or choose only one group
Question:
I am trying to identify the designers with best cost to finish the complete dress.
The Conditions:
-
Need to get complete dress
-
Either Paul1 or Paul2 can be selected but not both even though the
combination of these two could be the best cost.
Dress
John
Chris
Paul1
Paul2
Shirt
6.33
6.62
6.37
6.52
Trouser
6.76
6.62
6.37
6.76
Jacket
6.84
6.62
6.34
6.20
Here is the result of the model from Python and Pulp:
MINIMIZE
6.84 * x_0 + 6.62 * x_1 + 6.37 * x_10 + 6.76 * x_11 + 6.34 * x_2 + 6.2 * x_3 + 6.33 * x_4 + 6.62 * x_5 + 6.37 * x_6 + 6.76 * x_7 + 6.76 * x_8 + 6.62 * x_9 + 0.0
SUBJECT TO
C1: x_0 + x_1 + x_2 + x_3 = 1
C2: x_4 + x_5 + x_6 + x_7 = 1
C3: x_10 + x_11 + x_8 + x_9 = 1
I couldn’t figure it out the condition to choose either Paul1 or Paul2 but not both.
Any help would be greatly appreciated.
Note: This is continuous to the question: Linear Programming Binary Variable Grouping
Answers:
Standard way to choose either paul1 or paul2 isn’t introduce 2 binary variable indexed over y[p1]
& y[p2]
. Then following constraints
y[p1]<= sum(x[p1,c] over c)<=3y[p1]
y[p2]<= sum(x[p2,c] over c)<=3y[p2]
y[p1] + y[p2]<= 1
where c
represents {shirt, trouser, jacket}
.
The above constraints choose either p1
or p2
for all or none of the clothing items.
You need better variable and constraint names, and you have only one constraint (the exclusive vendor selection per row):
import pandas as pd
import pulp
def make_assignment(suffix: str) -> pulp.LpVariable:
return pulp.LpVariable(name='asn_' + suffix, cat=pulp.LpBinary)
def constrain_rows(row: pd.Series) -> pd.Series:
names = (row.name + '_' + row.index).to_series(index=row.index)
vars = names.apply(make_assignment)
prob.addConstraint(name='excl_' + row.name, constraint=1 == pulp.lpSum(vars))
return vars
df = pd.DataFrame({
'John': (6.33, 6.76, 6.84),
'Chris': (6.62, 6.62, 6.62),
'Paul1': (6.37, 6.37, 6.34),
'Paul2': (6.52, 6.76, 6.20),
}, index=pd.Index(name='Dress', data=('Shirt', 'Trouser', 'Jacket')))
prob = pulp.LpProblem(name='dress_vendors', sense=pulp.LpMinimize)
assigns = df.apply(func=constrain_rows, axis=1)
prob.objective = pulp.lpDot(assigns.values.ravel(), df.values.ravel())
print(prob)
prob.solve()
_, used = assigns.applymap(pulp.LpVariable.value).values.nonzero()
for dress, vendor in zip(df.index, df.columns[used]):
print(f'{dress} bought from {vendor}')
print(f'Dress total is ${prob.objective.value():.2f}')
dress_vendors:
MINIMIZE
6.62*asn_Jacket_Chris + 6.84*asn_Jacket_John + 6.34*asn_Jacket_Paul1 + 6.2*asn_Jacket_Paul2 + 6.62*asn_Shirt_Chris + 6.33*asn_Shirt_John + 6.37*asn_Shirt_Paul1 + 6.52*asn_Shirt_Paul2 + 6.62*asn_Trouser_Chris + 6.76*asn_Trouser_John + 6.37*asn_Trouser_Paul1 + 6.76*asn_Trouser_Paul2 + 0.0
SUBJECT TO
excl_Shirt: asn_Shirt_Chris + asn_Shirt_John + asn_Shirt_Paul1
+ asn_Shirt_Paul2 = 1
excl_Trouser: asn_Trouser_Chris + asn_Trouser_John + asn_Trouser_Paul1
+ asn_Trouser_Paul2 = 1
excl_Jacket: asn_Jacket_Chris + asn_Jacket_John + asn_Jacket_Paul1
+ asn_Jacket_Paul2 = 1
VARIABLES
0 <= asn_Jacket_Chris <= 1 Integer
0 <= asn_Jacket_John <= 1 Integer
0 <= asn_Jacket_Paul1 <= 1 Integer
0 <= asn_Jacket_Paul2 <= 1 Integer
0 <= asn_Shirt_Chris <= 1 Integer
0 <= asn_Shirt_John <= 1 Integer
0 <= asn_Shirt_Paul1 <= 1 Integer
0 <= asn_Shirt_Paul2 <= 1 Integer
0 <= asn_Trouser_Chris <= 1 Integer
0 <= asn_Trouser_John <= 1 Integer
0 <= asn_Trouser_Paul1 <= 1 Integer
0 <= asn_Trouser_Paul2 <= 1 Integer
Welcome to the CBC MILP Solver
Version: 2.10.3
Build Date: Dec 15 2019
command line - .venv/lib/python3.10/site-packages/pulp/solverdir/cbc/linux/64/cbc /tmp/1eacd290c9f44c86a43b0722be3104de-pulp.mps timeMode elapsed branch printingOptions all solution /tmp/1eacd290c9f44c86a43b0722be3104de-pulp.sol (default strategy 1)
At line 2 NAME MODEL
At line 3 ROWS
At line 8 COLUMNS
At line 57 RHS
At line 61 BOUNDS
At line 74 ENDATA
Problem MODEL has 3 rows, 12 columns and 12 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Continuous objective value is 18.9 - 0.00 seconds
Cgl0004I processed model has 0 rows, 0 columns (0 integer (0 of which binary)) and 0 elements
Cbc3007W No integer variables - nothing to do
Cuts at root node changed objective from 18.9 to -1.79769e+308
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Result - Optimal solution found
Objective value: 18.90000000
Enumerated nodes: 0
Total iterations: 0
Time (CPU seconds): 0.00
Time (Wallclock seconds): 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
Shirt bought from John
Trouser bought from Paul1
Jacket bought from Paul2
Dress total is $18.90
I am trying to identify the designers with best cost to finish the complete dress.
The Conditions:
-
Need to get complete dress
-
Either Paul1 or Paul2 can be selected but not both even though the
combination of these two could be the best cost.
Dress | John | Chris | Paul1 | Paul2 |
---|---|---|---|---|
Shirt | 6.33 | 6.62 | 6.37 | 6.52 |
Trouser | 6.76 | 6.62 | 6.37 | 6.76 |
Jacket | 6.84 | 6.62 | 6.34 | 6.20 |
Here is the result of the model from Python and Pulp:
MINIMIZE
6.84 * x_0 + 6.62 * x_1 + 6.37 * x_10 + 6.76 * x_11 + 6.34 * x_2 + 6.2 * x_3 + 6.33 * x_4 + 6.62 * x_5 + 6.37 * x_6 + 6.76 * x_7 + 6.76 * x_8 + 6.62 * x_9 + 0.0
SUBJECT TO
C1: x_0 + x_1 + x_2 + x_3 = 1
C2: x_4 + x_5 + x_6 + x_7 = 1
C3: x_10 + x_11 + x_8 + x_9 = 1
I couldn’t figure it out the condition to choose either Paul1 or Paul2 but not both.
Any help would be greatly appreciated.
Note: This is continuous to the question: Linear Programming Binary Variable Grouping
Standard way to choose either paul1 or paul2 isn’t introduce 2 binary variable indexed over y[p1]
& y[p2]
. Then following constraints
y[p1]<= sum(x[p1,c] over c)<=3y[p1]
y[p2]<= sum(x[p2,c] over c)<=3y[p2]
y[p1] + y[p2]<= 1
where c
represents {shirt, trouser, jacket}
.
The above constraints choose either p1
or p2
for all or none of the clothing items.
You need better variable and constraint names, and you have only one constraint (the exclusive vendor selection per row):
import pandas as pd
import pulp
def make_assignment(suffix: str) -> pulp.LpVariable:
return pulp.LpVariable(name='asn_' + suffix, cat=pulp.LpBinary)
def constrain_rows(row: pd.Series) -> pd.Series:
names = (row.name + '_' + row.index).to_series(index=row.index)
vars = names.apply(make_assignment)
prob.addConstraint(name='excl_' + row.name, constraint=1 == pulp.lpSum(vars))
return vars
df = pd.DataFrame({
'John': (6.33, 6.76, 6.84),
'Chris': (6.62, 6.62, 6.62),
'Paul1': (6.37, 6.37, 6.34),
'Paul2': (6.52, 6.76, 6.20),
}, index=pd.Index(name='Dress', data=('Shirt', 'Trouser', 'Jacket')))
prob = pulp.LpProblem(name='dress_vendors', sense=pulp.LpMinimize)
assigns = df.apply(func=constrain_rows, axis=1)
prob.objective = pulp.lpDot(assigns.values.ravel(), df.values.ravel())
print(prob)
prob.solve()
_, used = assigns.applymap(pulp.LpVariable.value).values.nonzero()
for dress, vendor in zip(df.index, df.columns[used]):
print(f'{dress} bought from {vendor}')
print(f'Dress total is ${prob.objective.value():.2f}')
dress_vendors:
MINIMIZE
6.62*asn_Jacket_Chris + 6.84*asn_Jacket_John + 6.34*asn_Jacket_Paul1 + 6.2*asn_Jacket_Paul2 + 6.62*asn_Shirt_Chris + 6.33*asn_Shirt_John + 6.37*asn_Shirt_Paul1 + 6.52*asn_Shirt_Paul2 + 6.62*asn_Trouser_Chris + 6.76*asn_Trouser_John + 6.37*asn_Trouser_Paul1 + 6.76*asn_Trouser_Paul2 + 0.0
SUBJECT TO
excl_Shirt: asn_Shirt_Chris + asn_Shirt_John + asn_Shirt_Paul1
+ asn_Shirt_Paul2 = 1
excl_Trouser: asn_Trouser_Chris + asn_Trouser_John + asn_Trouser_Paul1
+ asn_Trouser_Paul2 = 1
excl_Jacket: asn_Jacket_Chris + asn_Jacket_John + asn_Jacket_Paul1
+ asn_Jacket_Paul2 = 1
VARIABLES
0 <= asn_Jacket_Chris <= 1 Integer
0 <= asn_Jacket_John <= 1 Integer
0 <= asn_Jacket_Paul1 <= 1 Integer
0 <= asn_Jacket_Paul2 <= 1 Integer
0 <= asn_Shirt_Chris <= 1 Integer
0 <= asn_Shirt_John <= 1 Integer
0 <= asn_Shirt_Paul1 <= 1 Integer
0 <= asn_Shirt_Paul2 <= 1 Integer
0 <= asn_Trouser_Chris <= 1 Integer
0 <= asn_Trouser_John <= 1 Integer
0 <= asn_Trouser_Paul1 <= 1 Integer
0 <= asn_Trouser_Paul2 <= 1 Integer
Welcome to the CBC MILP Solver
Version: 2.10.3
Build Date: Dec 15 2019
command line - .venv/lib/python3.10/site-packages/pulp/solverdir/cbc/linux/64/cbc /tmp/1eacd290c9f44c86a43b0722be3104de-pulp.mps timeMode elapsed branch printingOptions all solution /tmp/1eacd290c9f44c86a43b0722be3104de-pulp.sol (default strategy 1)
At line 2 NAME MODEL
At line 3 ROWS
At line 8 COLUMNS
At line 57 RHS
At line 61 BOUNDS
At line 74 ENDATA
Problem MODEL has 3 rows, 12 columns and 12 elements
Coin0008I MODEL read with 0 errors
Option for timeMode changed from cpu to elapsed
Continuous objective value is 18.9 - 0.00 seconds
Cgl0004I processed model has 0 rows, 0 columns (0 integer (0 of which binary)) and 0 elements
Cbc3007W No integer variables - nothing to do
Cuts at root node changed objective from 18.9 to -1.79769e+308
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
ZeroHalf was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Result - Optimal solution found
Objective value: 18.90000000
Enumerated nodes: 0
Total iterations: 0
Time (CPU seconds): 0.00
Time (Wallclock seconds): 0.00
Option for printingOptions changed from normal to all
Total time (CPU seconds): 0.00 (Wallclock seconds): 0.00
Shirt bought from John
Trouser bought from Paul1
Jacket bought from Paul2
Dress total is $18.90