I am working on a production allocation problem, whereby sales orders have to be allocated over three production plants.
I am using PuLP in Python, and this works fine until I try to make the constraints "elastic". The elasticity is needed when the total of the sales orders is larger than the total capacity, and I have to increase the capacity of the plants, although with a "penalty". (Since producing in overtime increases the cost).
I got stuck in the lack of user-friendly documentation on this aspect of PuLP (sorry to say so) and encounter several difficulties. For applying "makeElasticSubProblem", I need to give the constraint a name. However, a) When I use the traditional syntax of prob += lpSum([alloc[s][l]for s in so]) <= cap_dict[l], 'Myname'
, and when I use Myname in the next line, Python returns a message that Myname is not defined. b) when I use the following syntax: Myname = LpConstraintlpSum([alloc[s][l]for s in so]), sense = 1, rhs = cap_dict[l])
, the name is accepted, but the definition of the elasticity that follows for the name, seems to be "ignored" by the script.
Could someone give me a hint about what I am doing wrong ? Thanks!
Here follows a simplified version of my code (working with Python 3.8.2 64-bit):
from pulp import *
# define the locations and their production capacity
#---------------------------------------------------
location = ['locA', 'locB', 'locC']
capacity = [90, 60, 20]
cap_dict = dict(zip(location, capacity))
# define the sales orders (so) and their volumes (demand)
#--------------------------------------------------------
so = ['s1', 's2', 's3', 's4', 's5']
demand = [20,10,15,8,5]
order_dict = dict(zip(so, demand))
# define the problem
#-------------------
prob = LpProblem("Production_planning",LpMaximize)
# define the decision variables
#------------------------------
alloc = LpVariable.dicts("Alloc", (so, location), cat='Integer')
# set the objective function
#---------------------------
prob += lpSum(alloc[s][l] for s in so for l in location)
# define the constraints
#-----------------------
# 1) allocations should be positive
for s in so:
for l in location:
prob += (alloc[s][l] >= 0)
# 2) allocation limited by the capacity of the location
for l in location:
prob += lpSum([alloc[s][l] for s in so]) <= cap_dict[l]
# 3) Location A should receive > 50% of all the allocations
prob += (alloc[s]['locA'] >= lpSum(alloc[s][l] for l in location)/2)
# 4) allocation limited by the amount ordered
for s in so:
prob += lpSum([alloc[s][l] for l in location]) <= order_dict[s]
# solve the optimization problem
#-------------------------------
prob.solve()
print("Status :", LpStatus[prob.status])
for v in prob.variables():
if "Alloc_" in v.name:
if v.varValue > 0:
print(v.name, v.varValue)
After the following definition of the capacity constraints, the specification of the 20% upward elasticity of the constraint for location C is simply ignored (the program allocates to location C any large amount if I increase the sales orders, without any limit):
# capacity constraints:
#---------------------
# same definition for locations A and B
for l in ['locA', 'locB']:
prob += lpSum([alloc[s][l] for s in so]) <= cap_dict[l]
# different and specific definition for location C, where an elastic capacity increase needs to be introduced:
cap_locC = LpConstraint(lpSum([alloc[s][l]for s in so]), sense = 1, rhs = cap_dict['locC'])
elastic_cap_locC = cap_locC.makeElasticSubProblem(penalty = 100, proportionFreeBoundList = [0,0.2])
I prefer to do this explicitly. That makes it less of a black box.
Your constraint looks like
sum(s, a[s,l]) <= cap[l]
Now add a variable overcap[l]
with bounds [0,20%*cap[l]]
and write:
sum(s, a[s,l]) <= cap[l] + overcap[l]
and add a penalty to the objective (maximization so use -):
.... - sum(l,penalty[l]*overcap[l])
Finally, report nonzero values of overcap[l]
to the user.