How do I generate PuLP variables and constrains without using exec?
Question:
I have written the following Python Code using the PuLP Library for solving the Knapsack Problem using the Integer Programming formulation. I am using strings to generate the LpVariable commands and add the constraints and then executing them with eval. Is there a way to do this without using eval?
from pulp import *
#Knapsack problem
items = input ('Enter the number of items :')
items = int(items)
#print('Enter %d items one by one')
print ('Enter {0} items profit one by one'.format(items))
obj = []
weight = []
knapweight = 0
for i in range(0,items):
print('Enter {0} item profit : '.format(i+1))
obj.append(input())
for i in range(0, items):
print('The profit at {0} is {1}'.format(i, obj[i]))
print ('nEnter {0} items weights one by one'.format(items))
for i in range(0, items):
print('Enter {0} item weight'.format(i+1))
weight.append(input())
for i in range(0, items):
print('The profit at {0} is {1}'.format(i, weight[i]))
print ('nEnter the weight of the knapsack :')
knapweight = input()
print ('The weight of the knapsack is : {0}'.format(knapweight))
#generating variables
for i in range(0, items):
str = 'x{0} = LpVariable("x{1}", cat='Binary')'.format(i+1,i+1)
print (str)
exec(str)
prob = LpProblem('Knapsack', LpMaximize)
print ('nDynamic Generaionn')
#weight constraint generation
str = "prob += "
for i in range(0, items):
if i == (items-1):
str = str + weight[i] + '*x{0}'.format(i+1)
else:
str = str + weight[i] + '*x{0}'.format(i+1) + '+'
str = str + '<=' + knapweight
exec(str)
print(str)
#objective function generation
str = "prob += "
for i in range(0, items):
if i == (items-1):
str = str + obj[i] + '*x{0}'.format(i+1)
else:
str = str + obj[i] + '*x{0}'.format(i+1) + '+'
exec(str)
print(str)
status = prob.solve()
print(LpStatus[status])
print ('nThe values of the variables : n')
for i in range(0, items):
print('x{0} = {1}'.format(i+1, value(eval('x{0}'.format(i+1)))))
Answers:
The key is recognizing that it’s okay to have an object — say one in a list or a dictionary — which you don’t have explicitly bound to a name. You can make an object, append it to a list, and only ever refer to it as some_list[2]
. Once you allow yourself that freedom, your code can become much simpler.
Here I’ve hardcoded the inputs, because that doesn’t matter:
from pulp import *
objs = [2,3,2,5,3]
weights = [1,2,2,1,3]
knapweight = 5
prob = LpProblem('Knapsack', LpMaximize)
xs = [LpVariable("x{}".format(i+1), cat="Binary") for i in range(len(objs))]
# add objective
total_prof = sum(x * obj for x,obj in zip(xs, objs))
prob += total_prof
# add constraint
total_weight = sum(x * w for x,w in zip(xs, weights))
prob += total_weight <= knapweight
status = prob.solve()
print(LpStatus[status])
print("Objective value:", value(prob.objective))
print ('nThe values of the variables : n')
for v in prob.variables():
print(v.name, "=", v.varValue)
which gives me
Optimal
Objective value: 10.0
The values of the variables :
x1 = 1.0
x2 = 1.0
x3 = 0.0
x4 = 1.0
x5 = 0.0
Here I’m building the LpVariables in the list comprehension
xs = [LpVariable("x{}".format(i+1), cat="Binary") for i in range(len(objs))]
where the objects then just live in the list xs
.
>>> xs
[x1, x2, x3, x4, x5]
>>> xs[3]
x4
>>> type(xs[3])
<class 'pulp.pulp.LpVariable'>
my pulp version:
from pulp import *
import numpy as np
prob = LpProblem("Knapsack_problem", LpMaximize)
cost = [2,3,2,6,1]
cap = [1,2,2,1,1]
cnt= len(cost);
knapweight = 5
x = LpVariable.dicts('x',range(cnt),lowBound=0,upBound=None,cat="Binary")
print(x)
prob += pulp.lpSum([x[i] * cost[i] for i in range(cnt)]), "obj"
prob += pulp.lpSum([x[i] * cap[i] for i in range(cnt)]) == knapweight, "c1"
prob.solve()
status = LpStatus[prob.status]
if status == "Optimal":
for v in prob.variables():
if v.varValue>0.0001:
print (v.name, "=", v.varValue)
print ("objective = %s" % value(prob.objective))
print ("_weights = %s" % ["x"+str(ix)+ "= "+str(w*v.varValue) for ix,w,v in zip(x, cap, prob.variables())])
or in GEKKO
from gekko import GEKKO
cost = [2,3,2,6,1]
w = [1,2,2,1,1]
cnt= len(cost);
knapweight = 5
m = GEKKO(remote = False)
# Gekko has an integer tolerance where it can find a solution within 1e-2 of the integer value. This can be adjusted with the solver option minlp_integer_tol
# increase tolerance
m.options.RTOL = 1e-10
m.options.OTOL = 1e-10
m.solver_options = ['minlp_gap_tol 1.0e-5',
'minlp_maximum_iterations 10000',
'minlp_max_iter_with_int_sol 10000',
'minlp_integer_tol 1e-8']
x = m.Array(m.Var,cnt,lb=0,ub=1,integer=True)
def f(x):
return sum([x[i] * cost[i] for i in range(cnt)])
m.Maximize(m.sum([cost[i]*x[i] for i in range(cnt)]))
y = m.Var()
m.Equation(y==m.sum([w[i]*x[i] for i in range(cnt)]))
m.Equation(y <= knapweight)
m.options.SOLVER = 1 # APOPT
m.solve()
##print(x)
value = int((m.options.objfcnval)*-1)
res = [int(x[i].value[0]) for i in range(cnt) ]
print("res_x: ", res)
print("y_total_weights: ", y.value[0])
I have written the following Python Code using the PuLP Library for solving the Knapsack Problem using the Integer Programming formulation. I am using strings to generate the LpVariable commands and add the constraints and then executing them with eval. Is there a way to do this without using eval?
from pulp import *
#Knapsack problem
items = input ('Enter the number of items :')
items = int(items)
#print('Enter %d items one by one')
print ('Enter {0} items profit one by one'.format(items))
obj = []
weight = []
knapweight = 0
for i in range(0,items):
print('Enter {0} item profit : '.format(i+1))
obj.append(input())
for i in range(0, items):
print('The profit at {0} is {1}'.format(i, obj[i]))
print ('nEnter {0} items weights one by one'.format(items))
for i in range(0, items):
print('Enter {0} item weight'.format(i+1))
weight.append(input())
for i in range(0, items):
print('The profit at {0} is {1}'.format(i, weight[i]))
print ('nEnter the weight of the knapsack :')
knapweight = input()
print ('The weight of the knapsack is : {0}'.format(knapweight))
#generating variables
for i in range(0, items):
str = 'x{0} = LpVariable("x{1}", cat='Binary')'.format(i+1,i+1)
print (str)
exec(str)
prob = LpProblem('Knapsack', LpMaximize)
print ('nDynamic Generaionn')
#weight constraint generation
str = "prob += "
for i in range(0, items):
if i == (items-1):
str = str + weight[i] + '*x{0}'.format(i+1)
else:
str = str + weight[i] + '*x{0}'.format(i+1) + '+'
str = str + '<=' + knapweight
exec(str)
print(str)
#objective function generation
str = "prob += "
for i in range(0, items):
if i == (items-1):
str = str + obj[i] + '*x{0}'.format(i+1)
else:
str = str + obj[i] + '*x{0}'.format(i+1) + '+'
exec(str)
print(str)
status = prob.solve()
print(LpStatus[status])
print ('nThe values of the variables : n')
for i in range(0, items):
print('x{0} = {1}'.format(i+1, value(eval('x{0}'.format(i+1)))))
The key is recognizing that it’s okay to have an object — say one in a list or a dictionary — which you don’t have explicitly bound to a name. You can make an object, append it to a list, and only ever refer to it as some_list[2]
. Once you allow yourself that freedom, your code can become much simpler.
Here I’ve hardcoded the inputs, because that doesn’t matter:
from pulp import *
objs = [2,3,2,5,3]
weights = [1,2,2,1,3]
knapweight = 5
prob = LpProblem('Knapsack', LpMaximize)
xs = [LpVariable("x{}".format(i+1), cat="Binary") for i in range(len(objs))]
# add objective
total_prof = sum(x * obj for x,obj in zip(xs, objs))
prob += total_prof
# add constraint
total_weight = sum(x * w for x,w in zip(xs, weights))
prob += total_weight <= knapweight
status = prob.solve()
print(LpStatus[status])
print("Objective value:", value(prob.objective))
print ('nThe values of the variables : n')
for v in prob.variables():
print(v.name, "=", v.varValue)
which gives me
Optimal
Objective value: 10.0
The values of the variables :
x1 = 1.0
x2 = 1.0
x3 = 0.0
x4 = 1.0
x5 = 0.0
Here I’m building the LpVariables in the list comprehension
xs = [LpVariable("x{}".format(i+1), cat="Binary") for i in range(len(objs))]
where the objects then just live in the list xs
.
>>> xs
[x1, x2, x3, x4, x5]
>>> xs[3]
x4
>>> type(xs[3])
<class 'pulp.pulp.LpVariable'>
my pulp version:
from pulp import *
import numpy as np
prob = LpProblem("Knapsack_problem", LpMaximize)
cost = [2,3,2,6,1]
cap = [1,2,2,1,1]
cnt= len(cost);
knapweight = 5
x = LpVariable.dicts('x',range(cnt),lowBound=0,upBound=None,cat="Binary")
print(x)
prob += pulp.lpSum([x[i] * cost[i] for i in range(cnt)]), "obj"
prob += pulp.lpSum([x[i] * cap[i] for i in range(cnt)]) == knapweight, "c1"
prob.solve()
status = LpStatus[prob.status]
if status == "Optimal":
for v in prob.variables():
if v.varValue>0.0001:
print (v.name, "=", v.varValue)
print ("objective = %s" % value(prob.objective))
print ("_weights = %s" % ["x"+str(ix)+ "= "+str(w*v.varValue) for ix,w,v in zip(x, cap, prob.variables())])
or in GEKKO
from gekko import GEKKO
cost = [2,3,2,6,1]
w = [1,2,2,1,1]
cnt= len(cost);
knapweight = 5
m = GEKKO(remote = False)
# Gekko has an integer tolerance where it can find a solution within 1e-2 of the integer value. This can be adjusted with the solver option minlp_integer_tol
# increase tolerance
m.options.RTOL = 1e-10
m.options.OTOL = 1e-10
m.solver_options = ['minlp_gap_tol 1.0e-5',
'minlp_maximum_iterations 10000',
'minlp_max_iter_with_int_sol 10000',
'minlp_integer_tol 1e-8']
x = m.Array(m.Var,cnt,lb=0,ub=1,integer=True)
def f(x):
return sum([x[i] * cost[i] for i in range(cnt)])
m.Maximize(m.sum([cost[i]*x[i] for i in range(cnt)]))
y = m.Var()
m.Equation(y==m.sum([w[i]*x[i] for i in range(cnt)]))
m.Equation(y <= knapweight)
m.options.SOLVER = 1 # APOPT
m.solve()
##print(x)
value = int((m.options.objfcnval)*-1)
res = [int(x[i].value[0]) for i in range(cnt) ]
print("res_x: ", res)
print("y_total_weights: ", y.value[0])