I am trying to solve this problem using linear programming using Pulp in python.
We have mango packs with each having different number of mangoes.
We should be able to serve the demand using the minimum number of packets and if possible serve the whole bag.
# Packet Names and the count of mangoes in each packet.
mangoe_packs = {
"pack_1": 2,
"pack_2": 3,
"pack_3": 3,
"pack_4": 2
}
For example,
Based on the demand we should get the correct packets. Ie., if the demand is 2, we give the packet with 2 mangoes. If the demand is 5, we serve packets with 2 and 3 mangoes. If your demand is 2 and we don't have any packet with 2 mangoes we can serve packet with 3 mangoes. In this case, we will have one remnant mango. Our purpose is to have the least number of remnant mangoes while serving the demand.
# Packet Names and the count of mangoes in each packet.
mangoe_packs = {
"pack_1": 2,
"pack_2": 3,
"pack_3": 3,
"pack_4": 2
}
Based on the data provided above,
If the demand is 2, The solution is pack_2 (can be pack_4 also).
If the demand is 4, The solution is pack_2 + pack_4.
If the demand is 5, The solution is pack_1 + pack_2
I am new to Linear programming and stuck at the problem. Tried few solutions and they are not working.
I am unable to come up with the correct objective function and constraints to solve this problem. Need help with that. Thank you.
Here is the code I tried.
from pulp import *
prob = LpProblem("MangoPacks", LpMinimize)
# Number of Mangoes in each packet.
mangoe_packs = {
"pack_1": 2,
"pack_2": 3,
"pack_3": 3,
"pack_4": 2
}
# Define demand variable.
demand = LpVariable("Demand", lowBound=2, HighBound=2, cat="Integer")
pack_count = LpVariable.dicts("Packet Count",
((i, j) for i in mangoe_packs.values() for j in ingredients),
lowBound=0,
cat='Integer')
pulp += (
lpSum([
pack_count[(pack)]
for pack, mango_count in mangoe_packs.iteritems()])
)
pulp += lpSum([j], for pack, j in mangoe_packs.iteritems()]) == 350 * 0.05
status = prob.solve()
Thank you.
Here are some considerations:
The variables of the problem are whether or not a pack should be opened. These variables are thus either 0 or 1 (keep closed, or open).
The main objective of the problem is to minimise the number of remnant mangoes. Or otherwise put: to minimise the total number of mangoes that are in the opened packs. This is the sum of the values of the input dictionary, but only of those entries where the corresponding LP variable is 1. Of course, a multiplication (with 0 or 1) can be used here.
In case of a tie, the number of opened packs should be minimised. This is simply the sum of the above mentioned variables. In order to combine this into one, single objective, multiply the value of the first objective with the total number of packets and add the value of this second objective to it. That way you get the right order in competing solutions.
The only constraint is that the sum of the number of mangoes in the opened packs is at least the number given in the input.
So here is an implementation:
def optimise(mango_packs, mango_count):
pack_names = list(mango_packs.keys())
prob = LpProblem("MangoPacks", LpMinimize)
# variables: names of the mango packs. We can either open them or not (0/1)
lp_vars = LpVariable.dicts("Open", pack_names, 0, 1, "Integer")
# objective: minimise total count of mangoes in the selected packs (so to
# minimise remnants). In case of a tie, minimise the number of opened packs.
prob += (
lpSum([mango_packs[name]*lp_vars[name] for name in pack_names]) * len(mango_packs)
+ lpSum([lp_vars[name] for name in pack_names])
)
# constraint: the opened packs need to amount to a minimum number of mangoes
prob += lpSum([mango_packs[name]*lp_vars[name] for name in pack_names]) >= mango_count
prob.solve()
In order to visualise the result, you could add the following in the above function:
print("Status:", LpStatus[prob.status])
# Each of the variables is printed with it's resolved optimum value
for i, v in enumerate(prob.variables()):
print("{}? {}".format(v.name, ("no","yes")[int(v.varValue)]))
Call the function like this:
# Packet Names and the count of mangoes in each packet.
mango_packs = {
"pack_1": 10,
"pack_2": 2,
"pack_3": 2,
"pack_4": 2
}
optimise(mango_packs, 5)
Output (when you added those print statements)
Status: Optimal
Open_pack_1? no
Open_pack_2? yes
Open_pack_3? yes
Open_pack_4? yes
See it run here -- give it some time to temporarily install the pulp module.
Here is a simple model that minimizes the total number of remnant mangoes. Instead of specifying the exact packages available the model just specifies the number of packages available per size (here 5 of size 2 and 15 of size 4):
from pulp import *
# PROBLEM DATA:
demand = [3, 7, 2, 5, 9, 3, 2, 4, 7, 5] # demand per order
packages = [0, 5, 0, 15] # available packages of different sizes
O = range(len(demand))
P = range(len(packages))
# DECLARE PROBLEM OBJECT:
prob = LpProblem('Mango delivery', LpMinimize)
# VARIABLES
assigned = pulp.LpVariable.dicts('assigned',
((o, p) for o in O for p in P), 0, max(demand), cat='Integer') # number of packages of different sizes per order
supply = LpVariable.dicts('supply', O, 0, max(demand), cat='Integer') # supply per order
remnant = LpVariable.dicts('remnant', O, 0, len(packages)-1, cat='Integer') # extra delivery per order
# OBJECTIVE
prob += lpSum(remnant) # minimize the total extra delivery
# CONSTRAINTS
for o in O:
prob += supply[o] == lpSum([p*assigned[(o, p)] for p in P])
prob += remnant[o] == supply[o] - demand[o]
for p in P:
# don't use more packages than available
prob += packages[p] >= lpSum([assigned[(o, p)] for o in O])
# SOLVE & PRINT RESULTS
prob.solve()
print(LpStatus[prob.status])
print('obj = ' + str(value(prob.objective)))
print('#remnants = ' + str(sum(int(remnant[o].varValue) for o in O)))
print('demand = ' + str(demand))
print('supply = ' + str([int(supply[o].varValue) for o in O]))
print('remnant = ' + str([int(remnant[o].varValue) for o in O]))
If the demand cannot be fulfilled this model will be infeasible. Another option in this case would be to maximize the number of orders fulfilled with a penalty for remnant mangoes. Here is the adapted model:
from pulp import *
# PROBLEM DATA:
demand = [3, 7, 2, 5, 9, 3, 2, 4, 7, 5] # demand per order
packages = [0, 5, 0, 5] # available packages of different sizes
O = range(len(demand))
P = range(len(packages))
M = max(demand) # a big enough number
# DECLARE PROBLEM OBJECT:
prob = LpProblem('Mango delivery', LpMaximize)
# VARIABLES
assigned = pulp.LpVariable.dicts('assigned',
((o, p) for o in O for p in P), 0, max(demand), cat='Integer') # number of packages of different sizes per order
supply = LpVariable.dicts('supply', O, 0, max(demand), cat='Integer') # supply per order
remnant = LpVariable.dicts('remnant', O, 0, len(packages)-1, cat='Integer') # extra delivery per order
served = LpVariable.dicts('served', O, cat='Binary') # whether an order is served
diff = LpVariable.dicts('diff', O, -M, len(packages)-1, cat='Integer') # difference between demand and supply
# OBJECTIVE
# primary objective is serve orders, secondary to minimize remnants
prob += 100*lpSum(served) - lpSum(remnant) # maximize served orders with a penalty for remnants
# CONSTRAINTS
for o in O:
prob += supply[o] == lpSum([p*assigned[(o, p)] for p in P])
prob += diff[o] == supply[o] - demand[o]
for p in P:
# don't use more packages than available
prob += packages[p] >= lpSum([assigned[(o, p)] for o in O])
for o in O:
# an order is served if supply >= demand
# formulation adapted from https://cs.stackexchange.com/questions/69531/greater-than-condition-in-integer-linear-program-with-a-binary-variable
prob += M*served[o] >= diff[o] + 1
prob += M*(served[o]-1) <= diff[o]
prob += lpSum([assigned[(o, p)] for p in P]) <= M*served[o]
for o in O:
# if order is served then remnant is supply - demand
# otherwise remnant is zero
prob += remnant[o] >= diff[o]
prob += remnant[o] <= diff[o] + M*(1-served[o])
# SOLVE & PRINT RESULTS
prob.solve()
print(LpStatus[prob.status])
print('obj = ' + str(value(prob.objective)))
print('#served = ' + str(sum(int(served[o].varValue) for o in O)))
print('#remnants = ' + str(sum(int(remnant[o].varValue) for o in O)))
print('served = ' + str([int(served[o].varValue) for o in O]))
print('demand = ' + str(demand))
print('supply = ' + str([int(supply[o].varValue) for o in O]))
print('remnant = ' + str([int(remnant[o].varValue) for o in O]))
Related
I am trying to solve this problem using pulp :
This is my code, There is a problem, because the result should be to only keep the second Location :
# Import PuLP modeler functions
from pulp import *
# Set of locations J
Locations = ["A", "B","C"]
# Set of demands I
Demands = ["1", "2", "3", "4", "5"]
# Set of distances ij
dt = [ # Demands I
# 1 2 3 4 5
[2, 23, 30, 54, 1], # A Locations J
[3, 1, 2, 2, 3], # B
[50,65,80,90,100] # C distances are very long
]
# Max value to get covered
s = 5
# Theses binaries values should be generated by code from the dt array ... I write it down directly for simplification.
# Demand I is served by location J If distance is <= 5 ( 0 = KO , 1 = OK)
covered = [
[1,0,0,0,1],
[1,1,1,1,1] # This shows that we only need Location B , not A
[0,0,0,0,0] # This shows we can't use Location C, it's too far
]
# Creates the 'prob' variable to contain the problem data
prob = LpProblem("Set covering", LpMinimize)
# # Problem variables
J = LpVariable.dicts("location", Locations, cat='Binary')
# The distance data is made into a dictionary
distances = makeDict([Locations, Demands], covered, 0)
# The objective function
# Minimize J, which is the number of locations
prob += lpSum(J["A"]+J["B"]+J["C"])
# The constraint
# Is it covered or not ?
for w in Locations:
for b in Demands:
if(distances[w][b] > 0):
prob += int(distances[w][b]) * J[w] >= 1
# Or eventually this instead :
#for w in Locations:
# prob += (lpSum([distances[w][b] * J[w] for b in Demands]) >= 1)
# or that :
# prob += 1 * J["A"] >= 1
# prob += 1 * J["A"] >= 1
# prob += 1 * J["B"] >= 1
# prob += 1 * J["B"] >= 1
# prob += 1 * J["B"] >= 1
# prob += 1 * J["B"] >= 1
# prob += 1 * J["B"] >= 1
# The problem data is written to an .lp file
prob.writeLP("SetCovering.lp")
# The problem is solved using PuLP's choice of Solver
prob.solve()
# The status of the solution is printed to the screen
print("Status:", LpStatus[prob.status])
# Each of the variables is printed with it's resolved optimum value
for v in prob.variables():
print(v.name, "=", v.varValue)
# The optimised objective function value is printed to the screen
print("Total Locations = ", value(prob.objective))
# Show constraints
constraints = prob.constraints
print(constraints)
#Status: Optimal
#location_A = 1.0
#location_B = 1.0
#location_C = 0.0
#Total Locations = 2.0
The result should be :
location_A = 0.0
location_B = 1.0
location_C = 0.0
because location B covers all of our needs.
I wonder where is the problem, there is the maths code , I hope I wrote enough:
Thanks , it's nice if you have a solution, I have also tried lpSum with no luck
Edit : Modified the code a few, you can see 'optimal solution', but It's not the solution I want + Added a "Location_C"
EDIT : This is my new code, added a secondary continuous pulp dict for arcs(links) generation (ser_customer) . The solver should only pick Fac-2 in this case, because it's near all of the customers, and other facilities are way too far:
# Lists (sets / Array) of Customers and Facilities
Customer = [1,2,3,4,5]
Facility = ['Fac-1', 'Fac-2', 'Fac-3']
# Dictionary of distances in kms
distance = {'Fac-1' : {1 : 54, 2 : 76, 3 : 5, 4 : 76, 5 : 76},
'Fac-2' : {1 : 1, 2 : 3, 3 : 1, 4 : 8, 5 : 1},
'Fac-3' : {1 : 45, 2 : 23, 3 : 54, 4 : 87, 5 : 88}
}
# Setting the Problem
prob = LpProblem("pb", LpMinimize)
# Defining our Decision Variables
use_facility = LpVariable.dicts("Use Facility", Facility, 0, 1, LpBinary)
ser_customer = LpVariable.dicts("Service", [(i,j) for i in Customer for j in Facility], 0)
# Setting the Objective Function = Minimize amount of facilities and arcs
prob += lpSum(use_facility['Fac-1']+use_facility['Fac-2']+use_facility['Fac-3']) + lpSum(distance[j][i]*ser_customer[(i,j)] for j in Facility for i in Customer)
# Constraints,At least 1 arc must exist between facilities and customers
for i in Customer:
prob += lpSum(ser_customer[(i,j)] for j in Facility) >= 1
prob.solve()
# Print the solution of Decision Variables
for v in prob.variables():
print(v.name, "=", v.varValue)
# Print the solution of Binary Decision Variables
Tolerance = 0.0001
for j in Facility:
if use_facility[j].varValue > Tolerance:
print("Establish Facility at site = ", j)
The result seems to show good arcs(links), but there is no facility selection, I wonder if somebody have any idea, is there any way to force use_facility[index] to be > 0 , Is adding arcs decisions variables a good idea ? I have tried to moove the arcs as a constraint too instead of being into the objective function, with no luck. :
Service_(1,_'Fac_1') = 0.0
Service_(1,_'Fac_2') = 1.0
Service_(1,_'Fac_3') = 0.0
Service_(2,_'Fac_1') = 0.0
Service_(2,_'Fac_2') = 1.0
Service_(2,_'Fac_3') = 0.0
Service_(3,_'Fac_1') = 0.0
Service_(3,_'Fac_2') = 1.0
Service_(3,_'Fac_3') = 0.0
Service_(4,_'Fac_1') = 0.0
Service_(4,_'Fac_2') = 1.0
Service_(4,_'Fac_3') = 0.0
Service_(5,_'Fac_1') = 0.0
Service_(5,_'Fac_2') = 1.0
Service_(5,_'Fac_3') = 0.0
Use_Facility_Fac_1 = 0.0
Use_Facility_Fac_2 = 0.0
Use_Facility_Fac_3 = 0.0
I also have tried the AirSquid solution, ,I think I maybe miss sources decisions variables who should be minimized but don' t know how to do, I guess covered are arcs (links), anyway It is a good exercise, harder than a simple product mix, hi hi :
prob = LpProblem('source minimzer', LpMinimize)
dist_limit = 5
sources = ['A', 'B','C'] # the source locations
# note this is zero-indexed to work with the list indexes in dist dictionary...
destinations = list(range(5)) # the demand locations 0, 1, 2, 3, 4
dist = { 'A': [2, 23, 30, 54, 1],
'B': [3, 1, 2, 2, 3],
'C':[24,54,12,56,76]}
covered = LpVariable.dicts('covered', [(s, d) for s in sources for d in destinations], cat='Binary')
# The objective function
# Minimize the number of sources
prob += lpSum(covered[s, d])
# set up constraint to limit covered if the destination is "reachable"
for s in sources:
for d in destinations:
prob += covered[s, d] * dist[s][d] <= dist_limit
# add one more constraint to make sure that every destination is "covered"...
# The problem is solved using PuLP's choice of Solver
prob.solve()
# The status of the solution is printed to the screen
print("Status:", LpStatus[prob.status])
# The optimised objective function value is printed to the screen
print("Location Selection = ", prob.objective)
The solution displayed, while it should print "B" :
Status: Optimal
Total Locations = covered_('C',_4)
You are on the right track! A couple things will help...
First, you overlooked a key piece of information in your output in that the solver says your formulation is INFEASIBLE!
Status: Infeasible
So whatever came out in the variables is gibberish and you must figure that part out first.
So, why is it infeasible? Take a look at your constraint. You are trying to force the impossible if your distance value is zero this cannot be true:
prob += int(distances[w][b]) * J[w] >= 1
So, you need to reformulate! You are missing a concept here. You actually need 2 constraints for this problem.
You need to constrain the selection of a source-destination if the route is too long
You need to enforce that every destination is covered.
You also need a double-indexed decision variable. Why? Well, lets say that source 'A' covers destination 1, 2; and 'B' covers 2, 3, 4, 5.... You will be able to know that all the destinations are "covered" with one variable, but you will not know which sources were used, so you need to keep track of both to get the full picture.
Here is a start, along with a couple edits. I'd suggest the variable names source and destination as that is kinda standard. You do not have a specific demand in this particular problem, just the need for a connection. You might also want to use dictionaries more than nested lists, I think it is clearer. Below is an example start with the first constraint. Note the trick here in limiting the covered variable. If the distance is less than the limit, s, then this constraint is satisfiable. For instance, if the distance is 3:
3 * 1 <= s
Anyhow, here is a recommended start. The other constraint is not implemented. You will need to sum across all the sources to ensure the destination is "covered". Comment back if your are stuck.
prob = LpProblem('source minimzer', LpMinimize)
dist_limit = 5
sources = ['A', 'B'] # the source locations
# note this is zero-indexed to work with the list indexes in dist dictionary...
destinations = list(range(5)) # the demand locations 0, 1, 2, 3, 4
dist = { 'A': [2, 23, 30, 54, 1],
'B': [3, 1, 2, 2, 3]}
covered = LpVariable.dicts('covered', [(s, d) for s in sources for d in destinations], cat='Binary')
# set up constraint to limit covered if the destination is "reachable"
for s in sources:
for d in destinations:
prob += covered[s, d] * dist[s][d] <= dist_limit
# add one more constraint to make sure that every destination is "covered"...
My Pyomo model is trying to solve a task assignment problem where 4 workers needs to be assigned to 8 tasks, so that's 2 tasks per worker.
One of the objective function model.obj2 tries to minimize the sum of the types of materials used by each worker worker. The reason is because every truck transporting materials to the worker can only carry 1 type of material, so there is efficiency gains to minimize the total number of truck visits.
This is currently being done using len(set(...)) to find number of unique materials used by both tasks assigned to a worker, and sum() to add up this number for all 4 workers.
def obj_rule(m):
# Minimize the total costs
obj1 = sum(
costs[i][j] * model.x[w, t] for i, w in enumerate(W) for j, t in enumerate(T)
)
# Minimize the number of unique materials used per worker
obj2 = len(
set(
material
for w in W
for t in T
for material in materials_used[t]
if value(model.x[w, t]) == True
)
)
return 5 * obj1 + 2 * obj2
However, removing model.obj1 (for debugging purposes), such as
def obj_rule(m):
# Minimize the number of unique materials used per worker
obj2 = len(
set(
material
for w in W
for t in T
for material in materials_used[t]
if value(model.x[w, t]) == True
)
)
return obj2
results in the warning
WARNING: Constant objective detected, replacing with a placeholder to prevent
solver failure.
This might explain why model.obj2 does not seem to be minimized for in the initial code. The objective expression might have been converted into a scalar value?
Can I get some help to rewrite this objective function the proper way for Pyomo? Thank you!
Code to reproduce problem
from pyomo.environ import *
import numpy as np
# 4 workers X 8 tasks
costs = np.array(
[
# workerA
[1, 2, 3, 4, 5, 6, 7, 8],
[1, 2, 3, 4, 5, 6, 7, 8],
# workerB
[8, 7, 6, 5, 4, 3, 2, 1],
[8, 7, 6, 5, 4, 3, 2, 1],
# workerC
[1, 3, 5, 7, 9, 11, 13, 15],
[1, 3, 5, 7, 9, 11, 13, 15],
# workerD
[15, 13, 11, 9, 7, 5, 3, 1],
[15, 13, 11, 9, 7, 5, 3, 1],
]
)
# "stone", "wood", "marble", "steel", "concrete"
materials_used = {
"taskA": ["stone", "wood"],
"taskB": ["marble", "wood"],
"taskC": ["marble", "stone"],
"taskD": ["steel", "stone"],
"taskE": ["marble", "steel"],
"taskF": ["marble", "steel"],
"taskG": ["concrete", "marble"],
"taskH": ["concrete", "steel"],
}
W = [
"workerA1",
"workerA2",
"workerB1",
"workerB2",
"workerC1",
"workerC2",
"workerD1",
"workerD2",
]
T = ["taskA", "taskB", "taskC", "taskD", "taskE", "taskF", "taskG", "taskH"]
model = ConcreteModel()
model.x = Var(W, T, within=Binary, initialize=0)
def obj_rule(m):
# Minimize the total costs
# obj1 = sum(
# costs[i][j] * model.x[w, t] for i, w in enumerate(W) for j, t in enumerate(T)
# )
# Minimize the number of unique materials used per worker
obj2 = len(
set(
material
for w in W
for t in T
for material in materials_used[t]
if value(model.x[w, t]) == True
)
)
return obj2
# return 5 * obj1 + 2 * obj2
model.obj = Objective(
rule=obj_rule,
sense=minimize,
)
def all_t_assigned_rule(m, w):
return sum(m.x[w, t] for t in T) == 1
def all_w_assigned_rule(m, t):
return sum(m.x[w, t] for w in W) == 1
model.c1 = Constraint(W, rule=all_t_assigned_rule)
model.c2 = Constraint(T, rule=all_w_assigned_rule)
opt = SolverFactory("glpk")
results = opt.solve(model)
I think there are 2 things I can add here that might be your missing links...
First, as mentioned in comments, the stuff you feed into the model must be legal expressions that do not depend on the value of the variables at time of creation, so len() etc. are invalid. Solution: use binary variables for those types of counting things and then sum them over appropriate indices.
Second, you are indexing your first variable correctly, but you have a second variable you need to introduce, namely, the decision to send worker w some material matl. See my example below that introduces this variable and then uses a big-M constraint to link the two decisions together.... Specifically, ensure the model delivers a required material to a worker for a task in which it is required.
Code:
# worker-task-material assignement
# goal: assign workers to tasks, minimze cost of sending them materials
# individually with a weighted OBJ function
import pyomo.environ as pyo
# some data
tasks = list('ABCDEF')
matls = ['stone', 'steel', 'wood', 'concrete']
big_M = max(len(tasks), len(matls)) # an upper bound on the num of matls needed
# this could be expanded to show quantities required...
material_reqts = [
('A', 'stone'),
('A', 'steel'),
('A', 'wood'),
('B', 'stone'),
('B', 'concrete'),
('C', 'concrete'),
('D', 'steel'),
('D', 'concrete'),
('E', 'stone'),
('E', 'wood'),
('F', 'steel'),
('F', 'wood')]
# convert to dictionary for ease of ingestion...
matls_dict = {(task, matl) : 1 for (task, matl) in material_reqts}
workers = ['Homer', 'Marge', 'Flanders']
# a little delivery cost matrix of matl - worker
worker_matl_delivery_costs = [
[1, 2, 4, 5], # Homer
[2, 3, 1, 1], # Marge
[4, 4, 3, 2]] # Flanders
wm_dict = { (w, m) : worker_matl_delivery_costs[i][j]
for i, w in enumerate(workers)
for j, m in enumerate(matls)}
# salary matrix
worker_task_costs = [
[2.2, 3.5, 1.9, 4.0, 3.8, 2.1],
[1.5, 3.0, 2.9, 4.0, 2.5, 1.6],
[1.4, 4.0, 2.3, 4.4, 2.5, 1.8]]
wt_dict = { (w, t) : worker_task_costs[i][j]
for i, w in enumerate(workers)
for j, t in enumerate(tasks)}
# build model components...
m = pyo.ConcreteModel()
# SETS
m.W = pyo.Set(initialize=workers)
m.T = pyo.Set(initialize=tasks)
m.M = pyo.Set(initialize=matls)
# PARAMS
m.delivery_costs = pyo.Param(m.W, m.M, initialize=wm_dict)
m.salary_costs = pyo.Param(m.W, m.T, initialize=wt_dict)
# note: need a default here to "fill in" the non-requirements...
m.matl_reqts = pyo.Param(m.T, m.M, initialize=matls_dict, default=0)
# VARS
m.Assign = pyo.Var(m.W, m.T, domain=pyo.Binary) # assign worker to task decision
m.Deliver = pyo.Var(m.W, m.M, domain=pyo.Binary) # deliver material to worker decision
# build model
# OBJ
# some conveniences here.... we can make model expressions individually
# for clarity and then combine them in the obj.
# pyo.summation() is a nice convenience too! Could also be done w/generator
delivery = pyo.summation(m.delivery_costs, m.Deliver)
salary = pyo.summation(m.salary_costs, m.Assign)
w1, w2 = 0.5, 0.6 # some arbitrary weights...
m.OBJ = pyo.Objective(expr=w1 * delivery + w2 * salary)
# CONSTRAINTS
# each worker must do at least 2 tasks. (note: this is an extension of your reqt.
# in this in conjunction with constraint below will pair all 6 tasks (2 ea. to workers)
# if more tasks are added, they'll be covered, with a min of 2 each
def two_each(m, w):
return sum(m.Assign[w, t] for t in m.T) >= 2
m.C1 = pyo.Constraint(m.W, rule=two_each)
# each task must be done once...prevents tasks from being over-assigned
def task_coverage(m, t):
return sum(m.Assign[w, t] for w in m.W) >= 1
m.C2 = pyo.Constraint(m.T, rule=task_coverage)
# linking constraint.... must deliver materials for task to worker if assigned
# note this is a "for each worker" & "for each material" type of constraint...
def deliver_materials(m, w, matl):
return m.Deliver[w, matl] * big_M >= sum(m.Assign[w, t] * m.matl_reqts[t, matl]
for t in m.T)
m.C3 = pyo.Constraint(m.W, m.M, rule=deliver_materials)
solver = pyo.SolverFactory('glpk')
results = solver.solve(m)
print(results)
print('Assignment Plan:')
for (w, t) in m.Assign.index_set():
if m.Assign[w, t]:
print(f' Assign {w} to task {t}')
print('\nDelivery Plan:')
for w in m.W:
print(f' Deliver to {w}:')
print(' ', end='')
for matl in m.M:
if m.Deliver[w, matl]:
print(matl, end=', ')
print()
print()
Yields:
Problem:
- Name: unknown
Lower bound: 18.4
Upper bound: 18.4
Number of objectives: 1
Number of constraints: 22
Number of variables: 31
Number of nonzeros: 85
Sense: minimize
Solver:
- Status: ok
Termination condition: optimal
Statistics:
Branch and bound:
Number of bounded subproblems: 53
Number of created subproblems: 53
Error rc: 0
Time: 0.008975028991699219
Solution:
- number of solutions: 0
number of solutions displayed: 0
Assignment Plan:
Assign Homer to task A
Assign Homer to task F
Assign Marge to task B
Assign Marge to task E
Assign Flanders to task C
Assign Flanders to task D
Delivery Plan:
Deliver to Homer:
stone, steel, wood,
Deliver to Marge:
stone, wood, concrete,
Deliver to Flanders:
steel, concrete,
[Finished in 562ms]
I have a transportation problem with constraints on the load a robot can carry.
There are robots in different places in a warehouse.
And I am optimizing based on distances from robot to stations where the robot can collect some boxes.
The constraint is that a robot can carry 2 boxes IF the height of the first box is below a threshold.
Otherwise it can carry only a single box.
And every station has only 1 box to take.
I succeeded in planning for robots without the load constraint.
So that every robot can carry 2 boxes.
In the example below robot 1 gets station 2 and 3 and robot 2 gets station 1.
So this is fine.
But I am struggling on how to add the box_heights constraint which affects the loadable boxes on the robot.
How would I design this in pulp?
This is my test inout data:
optimizer costs: [[1, 2, 3], [4, 5, 6]]
optimizer boxes_to_take: {1: 1, 2: 1, 3: 1}
optimizer box_heights: {1: 1, 2: 2, 3: 1}
optimizer available_space: {1: 2, 2: 2}
optimizer station_ids: [1, 2, 3]
Costs explained:
# Stations
# 1 2 3
[1, 2, 3], # S1 Robots
[4, 5, 6], # S2
So the cost from S2 to Station 3 is 6.
And this is my script: ( copy - paste executable python 3.8 code)
import logging
import threading
import typing
from pulp import *
logger = logging.getLogger()
logger.level = logging.INFO
stream_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(stream_handler)
class TransportOptimizer:
def __init__(self):
logging.info(f"Initializing TransportOptimizer")
# Takes stations and robots to optimize and returns a new set of optimized stations
def optimize(self):
station_ids = [1, 2, 3]
robot_ids = [1, 2]
# Creates a dictionary for the number of units a robot can load
available_space = {}
for robot_id in robot_ids:
available_space[robot_id] = 2
# Creates a dictionary for the number of available boxes
boxes_to_take = {}
box_heights = {}
for station_id in station_ids:
boxes_to_take[station_id] = 1
box_heights[station_id] = 1 if station_id % 2 else 2
logging.debug(f"boxes_to_take: {boxes_to_take}")
# Creates a list of costs of each transportation path
costs = [ # Stations
# 1 2 3
[1, 2, 3], # S1 Robots
[4, 5, 6], # S2
]
logging.info(f"optimizer costs: {costs}")
logging.info(f"optimizer boxes_to_take: {boxes_to_take}")
logging.info(f"optimizer box_heights: {box_heights}")
logging.info(f"optimizer available_space: {available_space}")
logging.info(f"optimizer station_ids: {station_ids}")
# The cost data is made into a dictionary
costs = makeDict([robot_ids, station_ids], costs, 0)
# Creates the 'problem' variable to contain the problem data
problem = LpProblem("FullCostOptimizer", LpMinimize)
# Creates a list of tuples containing all the possible routes for transport
Routes = [(robot_id, station_id) for robot_id in robot_ids for station_id in station_ids]
# A dictionary called 'Vars' is created to contain the referenced variables(the routes)
vars = LpVariable.dicts("Route", (robot_ids, station_ids), 0, None, LpInteger)
# The objective function is added to 'problem' first
problem += (
lpSum([vars[robot_id][station_id] * costs[robot_id][station_id] for (robot_id, station_id) in Routes]),
"Sum_of_Transporting_Costs",
)
# The maximum loadable boxes constraints are added to problem for each robot
for robot_id in robot_ids:
problem += lpSum([vars[robot_id][station_id] for station_id in station_ids]) <= available_space[
robot_id], f"sum_of_boxes_robot {robot_id}_can_load"
# The minimum boxes constraints are added to problem for each station
for station_id in station_ids:
problem += lpSum([vars[robot_id][station_id] for robot_id in robot_ids]) >= boxes_to_take[
station_id], "sum_of_boxes_on_station_%s" % station_id
station_id = 1
robot_id = 1
logging.info(
f"robot {robot_id} and station {station_id}: {vars[robot_id][station_id]}, sum={lpSum([vars[robot_id][station_id] for station_id in station_ids]) <= available_space[robot_id]}")
logging.info(f"optimizer problem: {problem}")
# The problem is solved using PuLP's choice of Solver
problem.solve(PULP_CBC_CMD(msg=False))
# The status of the solution is printed to the screen
logging.debug(f"Status: {LpStatus[problem.status]}")
# Each of the variables is printed with it's resolved optimum value
for v in problem.variables():
logging.debug(f"{v.name} = {v.varValue}")
# The optimised objective function value is printed to the screen
logging.debug(f"Total Cost of Transportation = {value(problem.objective)}")
logging.debug(problem.sol_status)
logging.info(f"Total cost: {problem.objective.value()}")
logging.info(f"vars: {problem.variables()}")
for v in problem.variables():
route, robot, station = str.split(v.name, sep='_')
logging.info(f"robot {robot} to station {station} = {v.varValue} {'valid' if v.varValue == 1 else ''}")
if problem.sol_status != 1:
raise RuntimeError(f"Solver failed with status {problem.sol_status}")
if __name__ == '__main__':
optimizer = TransportOptimizer()
optimized_tasks = optimizer.optimize()
There are number of jobs to be assigned to number of resources each with a score (performance indicator) and cost. The resource assignment problem (RAP) objective is to maximize assignment scores considering the budget. Constraints: Each resource can handle at most one job and each job if it is filled should be done by one resource. Also, there is a limited budget to spend.
I have tackled the problem in two ways: CVXPY using gurobi solver and gurobi packages. My challenge is I can't program it in a memory-efficient way with cvxpy. There are hundreds of constraint list comprehensions! How can I can improve efficiency of my code in cvxpy? For example, is there a better way to define dictionary variables in cvxpy similar to gurobi?
ms is dictionary of format {('firstName lastName', 'job'), score_value}
cst is dictionary of format {('firstName lastName', 'job'), cost_value}
job is set of jobs
res is set of resources {'firstName lastName'}
G (or g in gurobi implementation) is a dictionary with jobs as keys and values of 0 or 1 whether that job is filled due to budget limit (0 if filled and 1 if not)
thanks
github link including codes and memory profiling comparison
gurobi implementation:
m = gp.Model("RAP")
assign = m.addVars(ms.keys(), vtype=GRB.BINARY, name="assign")
g = m.addVars(job, name="gap")
m.addConstrs((assign.sum("*", j) + g[j] == 1 for j in job), name="demand")
m.addConstrs((assign.sum(r, "*") <= 1 for r in res), name="supply")
m.addConstr(assign.prod(cst) <= budget, name="Budget")
job_gap_penalty = 101 # penatly of not filling a job
m.setObjective(assign.prod(ms) -job_gap_penalty*g.sum(), GRB.MAXIMIZE)
m.optimize()
cvxpy implenentation:
X = {}
for a in ms.keys():
X[a] = cp.Variable(boolean=True, name="assign")
G = {}
for g in job:
G[g] = cp.Variable(boolean=True, name="gap")
constraints = []
for j in job:
X_r = 0
for r in res:
X_r += X[r, j]
constraints += [
X_r + G[j] == 1
]
for r in res:
X_j = 0
for j in job:
X_j += X[r, j]
constraints += [
X_j <= 1
]
constraints += [
np.array(list(cst.values())) # np.array(list(X.values())) <= budget,
]
obj = cp.Maximize(np.array(list(ms.values())) # np.array(list(X.values()))
- job_gap_penalty * cp.sum(list(G.values())))
prob = cp.Problem(obj, constraints)
prob.solve(solver=cp.GUROBI, verbose=False)
Here is the memory profiling comparison:
memeory profiling for cvxpy
memory profiling for gurobi
Previously, I tried to solve thru defining dictionary variables similar to gurobi but at is not available in cvxpy, the code was not efficient when scaling up. But now I solved it thru matrix variables and then converting to dictionary variables which super fast!
assign_scores = np.array(list(ms.values())).reshape(len(res), len(job))
assign_cost = np.array(list(cst.values())).reshape(len(res), len(job))
# make a bool matrix variable with the shape of number of resources and jobs
x = cp.Variable(shape=(len(res), len(job)), boolean=True, name="assign")
# make a bool vector variable with the shape of number of jobs
g = cp.Variable(shape=(len(job), ), boolean=True, name="gap")
constraints = []
# each job can be assigned to at most one resource or remains unfilled due to budget cap
constraints += [cp.sum(x[:, j]) + g[j] == 1 for j in range(len(job))]
# each resource can be assigned to at most one job
constraints += [cp.sum(x[r, :]) <= 1 for r in range(len(res))]
# budget cap
constraints += [cp.sum(cp.multiply(assign_cost, x)) <= budget]
# pentalty if a job is not filled
job_gap_penalty=101
# objective is to maiximize performance score
obj = cp.Maximize(cp.sum(cp.multiply(assign_scores, x) - job_gap_penalty * cp.sum(g)))
prob = cp.Problem(obj, constraints)
prob.solve(solver=cp.GUROBI, verbose=True)
I'm currently working on a problem that requires a variation of the bin packing problem. My problem is different in that the number of bins is finite. I have three bins, the smallest one costs the least to put an object into, the medium bin is slightly more expensive than the small box, and the third box has theoretically unlimited capacity but is prohibitively more expensive to place an item into.
I was able to find a Python script online that solves the bin problem in a similar manner. My question is how can I rewrite the script to get closer to my original problem? The script in question uses identical bins.
I've included some lines at the very bottom to discuss how I would prefer the bin to look. Furthermore, is there a way to set up separate constraints for each bin? Thanks for all the help!
from openopt import *
N = 30 #Length of loan dataset
items = []
for i in range(N):
small_vm = {
'name': 'small%d' % i,
'cpu': 2,
'mem': 2048,
'disk': 20,
'n': 1
}
med_vm = {
'name': 'medium%d' % i,
'cpu': 4,
'mem': 4096,
'disk': 40,
'n': 1
}
large_vm = {
'name': 'large%d' % i,
'cpu': 8,
'mem': 8192,
'disk': 80,
'n': 1
}
items.append(small_vm)
items.append(med_vm)
items.append(large_vm)
bins = {
'cpu': 48*4, # 4.0 overcommit with cpu
'mem': 240000,
'disk': 2000,
}
p = BPP(items, bins, goal = 'min')
r = p.solve('glpk', iprint = 0)
print(r.xf)
print(r.values) # per each bin
print "total vms is " + str(len(items))
print "servers used is " + str(len(r.xf))
for i,s in enumerate(r.xf):
print "server " + str(i) + " has " + str(len(s)) + " vms"
##OP Interjection: Ideally my bins would look something like:
bin1 = {
'size': 10000,
'cost': 0.01*item_weight,
}
bin2 = {
'size': 20000,
'cost': 0.02*item_weight,
}
bin3 = {
'size': 100000,
'cost': 0.3*item_weight,
}
The variant of the bin packing problem with variable bin sizes you are describing is at least np-hard.
I do not know the package openopt, the project website seems to be down. Openopt seems to use GLPK to solve the problem as a mixed-integer program. You do not have direct access to the model formulation, since BPP() is an abstraction. You may need to modify the openopt package to add constraints for the individual bins.
It is generally easy to add the variable bin sizes as a constraint, extending this formulation you would need to add index i to capacity V, so that each bin has a different capacity.
I would recommend to look at some maintained libraries to model and solve this problem: There is the library PuLP, CyLP and SCIP. The latter is not free for commercial use I think though.
Since bin packing is a very common problem, I found an example for the PuLP library. It uses the CoinOR Solver by default I think, you can also use different commercial ones.
easy_install pulp
After installing PuLP, which should be possible with easy_install you can extend on this example.
I modified the example according to your problem:
from pulp import *
items = [("a", 5), ("b", 6), ("c", 7)]
itemCount = len(items)
maxBins = 3
binCapacity = [11, 15, 10]
binCost = [10, 30, 20]
y = pulp.LpVariable.dicts('BinUsed', range(maxBins), lowBound = 0, upBound = 1, cat = pulp.LpInteger)
possible_ItemInBin = [(itemTuple[0], binNum) for itemTuple in items for binNum in range(maxBins)]
x = pulp.LpVariable.dicts('itemInBin', possible_ItemInBin, lowBound = 0, upBound = 1, cat = pulp.LpInteger)
# Model formulation
prob = LpProblem("Bin Packing Problem", LpMinimize)
# Objective
prob += lpSum([binCost[i] * y[i] for i in range(maxBins)])
# Constraints
for j in items:
prob += lpSum([x[(j[0], i)] for i in range(maxBins)]) == 1
for i in range(maxBins):
prob += lpSum([items[j][1] * x[(items[j][0], i)] for j in range(itemCount)]) <= binCapacity[i]*y[i]
prob.solve()
print("Bins used: " + str(sum(([y[i].value() for i in range(maxBins)]))))
for i in x.keys():
if x[i].value() == 1:
print("Item {} is packed in bin {}.".format(*i))
This implementation has the strong advantage that you have complete control over your model formulation and you are not restricted by some layer of abstraction like BPP() in the case of openopt.