Implementing binary constraint in PuLP Objective Function - python

I'm trying to determine the maximum revenue that can be earned from a battery connected to the grid using linear programming. The battery can earn revenues in two markets, the energy market and the frequency market. My model is throwing an error when I include a binary constraint in the objective function (TypeError: Non-constant expressions cannot be multiplied).
My Objective function is:
N is the time horizon of the optimisation
is the energy price at time t
are the allocated discharge and charge power at time t
is the frequency price at time t
is the allocated frequency power at time t
The battery should only be active in one market (energy or frequency) at each time period t. So needs a constraint that looks something like this:
where is a binary variable that activates activity x.
Ok, so that's what I trying to achieve. I'm struggling to create such a constraint in pulp that essentially switches off participation in one of the markets if the value in the other is higher (all other constraints being met). In my battery class, I've created decision variables for each of the power activities and also for their on/off status.
self.charge = \
pulp.LpVariable.dicts(
"charging_power",
('c_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_charge_power_capacity,
cat='Continuous')
self.discharge = \
pulp.LpVariable.dicts(
"discharging_power",
('d_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_discharge_power_capacity,
cat='Continuous')
self.freq = \
pulp.LpVariable.dicts(
"freq_power",
('f_t_' + str(i) for i in range(0,time_horizon)),
lowBound=0, upBound=max_freq_power_capacity,
cat='Continuous')
self.charge_status = \
pulp.LpVariable.dicts(
"charge_status",
('c_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
self.discharge_status = \
pulp.LpVariable.dicts(
"discharge_status",
('d_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
self.freq_status = \
pulp.LpVariable.dicts(
"freq_status",
('ds3_status_t_' + str(i) for i in range(0,time_horizon)),
cat='Binary')
In my objective function, I included these binary variables.
self.model = pulp.LpProblem("Max Profit", pulp.LpMaximize)
self.model +=\
pulp.lpSum(
[self.charge['c_t_' + str(i)]*-1*prices[i] *
self.charge_status['c_status_t_' + str(i)] for i in range(0,self.time_horizon)]
+ [self.discharge['d_t_' + str(i)]*prices[i] *
self.discharge_status['d_status_t_' + str(i)] for i in range(0,self.time_horizon)]
+ [self.freq['f_t_' + str(i)]*freq_prices[i] *
self.freq_status['freq_status_t_' + str(i)] for i in range(0,self.time_horizon)]
)
The constraint for these binary variables, I set up as follows:
for hour_of_sim in range(1,self.time_horizon+1):
self.model += \
pulp.lpSum([self.charge_status['c_status_t_' + str(i)] for i in range(0,self.time_horizon)] +\
[self.discharge_status['d_status_t_' + str(i)] for i in range(0,self.time_horizon)] +\
[self.freq_status['freq_status_t_' + str(i)] for i in range(0,self.time_horizon)]
) <= 1
When I try to solve, I get a
TypeError: Non-constant expressions cannot be multiplied
on the objective function. Doesn't like my binary variables, runs if they are removed. There must be an alternative way of setting this up which is escaping me?

The comment is correct... you are violating "linearity" by multiplying 2 variables together. Fortunately, this is easy to linearize. You have a binary variable controlling the mode, so the key element you are looking for (google it) is a Big-M constraint, where you use the binary variable multiplied by a max value (or just something sufficiently large) to limit the other variable either to the max, or clamp it to zero.
An example below. I also re-arranged things a bit. You might find this style more readable. Two main things on style:
You are constantly re-creating the indices you are using which is really painful to read and error prone. Just make them and re-use them...and you don't need to get complicated with the index set values
You can easily double-index this model, which I think is more clear than making multiple sets of variables. You essentially have 2 sets you are working with: Time periods, and Op modes. Just make those sets and double index.
Example
# battery modes
import pulp
# some data
num_periods = 3
rate_limits = { 'energy' : 10,
'freq' : 20}
price = 2 # this could be a table or double-indexed table of [t, m] or ....
# SETS
M = rate_limits.keys() # modes. probably others... discharge?
T = range(num_periods) # the time periods
TM = {(t, m) for t in T for m in M}
model = pulp.LpProblem('Batts', pulp.LpMaximize)
# VARS
model.batt = pulp.LpVariable.dicts('batt_state', indexs=TM, lowBound=0, cat='Continuous')
model.op_mode = pulp.LpVariable.dicts('op_mode', indexs=TM, cat='Binary')
# Constraints
# only one op mode in each time period...
for t in T:
model += sum(model.op_mode[t, m] for m in M) <= 1
# Big-M constraint. limit rates for each rate, in each period.
# this does 2 things: it is equivalent to the upper bound parameter in the var declaration
# It is a Big-M type of constraint which uses the binary var as a control <-- key point
for t, m in TM:
model += model.batt[t, m] <= rate_limits[m] * model.op_mode[t, m]
# OBJ
model += sum(model.batt[t, m] * price for t, m in TM)
print(model)
# solve...
Yields:
Batts:
MAXIMIZE
2*batt_state_(0,_'energy') + 2*batt_state_(0,_'freq') + 2*batt_state_(1,_'energy') + 2*batt_state_(1,_'freq') + 2*batt_state_(2,_'energy') + 2*batt_state_(2,_'freq') + 0
SUBJECT TO
_C1: op_mode_(0,_'energy') + op_mode_(0,_'freq') <= 1
_C2: op_mode_(1,_'energy') + op_mode_(1,_'freq') <= 1
_C3: op_mode_(2,_'energy') + op_mode_(2,_'freq') <= 1
_C4: batt_state_(2,_'freq') - 20 op_mode_(2,_'freq') <= 0
_C5: batt_state_(2,_'energy') - 10 op_mode_(2,_'energy') <= 0
_C6: batt_state_(1,_'freq') - 20 op_mode_(1,_'freq') <= 0
_C7: batt_state_(1,_'energy') - 10 op_mode_(1,_'energy') <= 0
_C8: batt_state_(0,_'freq') - 20 op_mode_(0,_'freq') <= 0
_C9: batt_state_(0,_'energy') - 10 op_mode_(0,_'energy') <= 0
VARIABLES
batt_state_(0,_'energy') Continuous
batt_state_(0,_'freq') Continuous
batt_state_(1,_'energy') Continuous
batt_state_(1,_'freq') Continuous
batt_state_(2,_'energy') Continuous
batt_state_(2,_'freq') Continuous
0 <= op_mode_(0,_'energy') <= 1 Integer
0 <= op_mode_(0,_'freq') <= 1 Integer
0 <= op_mode_(1,_'energy') <= 1 Integer
0 <= op_mode_(1,_'freq') <= 1 Integer
0 <= op_mode_(2,_'energy') <= 1 Integer
0 <= op_mode_(2,_'freq') <= 1 Integer

Related

PuLP Optimization Traveling Salesman Problem - Calculating arrival time for each node

I'm solving the traveling salesman problem using PuLP optimizer on python. The code takes the time matrix as an ndarray and uses it to calculate the optimal route. My first version is running perfectly but i am facing some issue when i add a variable which calculates the time at which the vehicle reaches each point.
Version 1
import numpy as np
from pulp import *
time_matrix = np.array([[0,5,4,6,7,10],
[5,0,3,2,6,15],
[4,3,0,4,5,6],
[6,2,4,0,7,8],
[7,6,5,7,0,11],
[10,15,6,8,11,0]])
row,col = time_matrix.shape
problem = LpProblem('TravellingSalesmanProblem', LpMinimize)
# Decision variable X for truck route
decisionVariableX = LpVariable.dicts('decisionVariable_X', ((i, j) for i in range(row) for j in range(row)), lowBound=0, upBound=1, cat='Integer')
# subtours elimination
decisionVariableU = LpVariable.dicts('decisionVariable_U', (i for i in range(row)), lowBound=1, cat='Integer')
# Objective Function
problem += lpSum(time_matrix[i][j] * decisionVariableX[i, j] for i in range(row) for j in range(row))
# Constraint
for i in range(row):
problem += (decisionVariableX[i,i] == 0) # elimination of (1 to 1) route
problem += lpSum(decisionVariableX[i,j] for j in range(row))==1 # truck reaches all points once
problem += lpSum(decisionVariableX[j,i] for j in range(row)) ==1 #truck dispatches from all points once
for j in range(row):
if i != j and (i != 0 and j != 0):
problem += decisionVariableU[i] <= decisionVariableU[j] + row * (1 - decisionVariableX[i, j])-1 # sub-tour elimination for truck
status = problem.solve()
print(f"status: {problem.status}, {LpStatus[problem.status]}")
print(f"objective: {problem.objective.value()}")
for var in problem.variables():
if (problem.status == 1):
if (var.value() !=0):
print(f"{var.name}: {var.value()}")
In version 2 I add another variable decisionVariableT which stores the value of time at which the truck reaches each node. But adding this constraint makes the problem infeasible. Can someone help me in identifying whats wrong with the code?
TIA
Version 2 addition
# Decision variable T for truck arrival time
decisionVariableT = LpVariable.dicts('decisionVariable_T', (i for i in range(row)), lowBound=0, cat='Integer')
M=1000
# Calculating truck arrival time at each node
for i in range(row):
for j in range(row):
if (decisionVariableX[i,j]==1):
problem += decisionVariableT[j] == decisionVariableT[i] + time_matrix[i][j]
The result of Version 1:
status: 1, Optimal
objective: 33.0
decisionVariable_U_1: 5.0
decisionVariable_U_2: 2.0
decisionVariable_U_3: 4.0
decisionVariable_U_4: 1.0
decisionVariable_U_5: 3.0
decisionVariable_X_(0,_4): 1.0
decisionVariable_X_(1,_0): 1.0
decisionVariable_X_(2,_5): 1.0
decisionVariable_X_(3,_1): 1.0
decisionVariable_X_(4,_2): 1.0
decisionVariable_X_(5,_3): 1.0
The result of Version 2:
status: -1, Infeasible
objective: 0.0
Consider if your timing matrix had these values:
t[2,3] = 2
t[3,2] = 4
Because you are enumerating over all of the combos in your matrix, you are then making the (infeasible) statement that
t[2] >= t[3] + 4 + C
t[3] >= t[2] + 2 + C
Where C is a constant and the result of your big-M constraint in the case that neither (or both) are used.
Rearranged that is:
t[2] >= t[3] + 4
t[2] <= t[3] - 2
Edit... ideas on fix
As for a fix... You could either...
make a variable and keep a running sum over the rows only not the columns. In pseudocode:
problem += cumulative_time[0] >= sum(x[0,c] * t[0,c] for c in cols)
for r in rows[1:]:
prob += cumulative_time[r] >= cumulative_time[r-1] + <above, with 'r' instead of '0'>
Or
realize that it isn't necessary to treat cumulative time as a variable, because you can deduce it from your solution and just do the same construct over the result values when you are looping over them to print....

Pyomo: How to include a penalty in the objective function

I'm trying to minimize the cost of manufacturing a product with two machines. The cost of machine A is $30/product and cost of machine B is $40/product.
There are two constraints:
we must cover a demand of 50 products per month (x+y >= 50)
the cheap machine (A) can only manufacture 40 products per month (x<=40)
So I created the following Pyomo code:
from pyomo.environ import *
model = ConcreteModel()
model.x = Var(domain=NonNegativeReals)
model.y = Var(domain=NonNegativeReals)
def production_cost(m):
return 30*m.x + 40*m.y
# Objective
model.mycost = Objective(expr = production_cost, sense=minimize)
# Constraints
model.demand = Constraint(expr = model.x + model.y >= 50)
model.maxA = Constraint(expr = model.x <= 40)
# Let's solve it
results = SolverFactory('glpk').solve(model)
# Display the solution
print('Cost=', model.mycost())
print('x=', model.x())
print('y=', model.y())
It works ok, with the obvious solution x=40;y=10 (Cost = 1600)
However, if we start to use the machine B, there will be a fixed penalty of $300 over the cost.
I tried with
def production_cost(m):
if (m.y > 0):
return 30*m.x + 40*m.y + 300
else:
return 30*m.x + 40*m.y
But I get the following error message
Rule failed when generating expression for Objective mycost with index
None: PyomoException: Cannot convert non-constant Pyomo expression (0 <
y) to bool. This error is usually caused by using a Var, unit, or mutable
Param in a Boolean context such as an "if" statement, or when checking
container membership or equality. For example,
>>> m.x = Var() >>> if m.x >= 1: ... pass
and
>>> m.y = Var() >>> if m.y in [m.x, m.y]: ... pass
would both cause this exception.
I do not how to implement the condition to include the penalty into the objective function through the Pyomo code.
Since m.y is a Var, you cannot use the if statement with it. You can always use a binary variable using the Big M approach as Airsquid said it. This approach is usually not recommended, since it turns the problem from LP into a MILP, but it is effective.
You just need to create a new Binary Var:
model.bin_y = Var(domain=Binary)
Then constraint model.y to be zero if model.bin_y is zero, or else, be any value between its bounds. I use a bound of 100 here, but you can even use the demand:
model.bin_y_cons = Constraint(expr= model.y <= model.bin_y*100)
then, in your objective just apply the new fixed value of 300:
def production_cost(m):
return 30*m.x + 40*m.y + 300*model.bin_y
model.mycost = Objective(rule=production_cost, sense=minimize)

Barrier Option Pricing in Python

We have a barrier call option of European type with strike price K>0 and a barrier value
0 < b< S0 ,
where S_0 is the starting price.According to the contract, the times 0<t_1<...<t_k<T the price must be checked S(t_k)>b for every k.
Assuming the S(t) is described with the binomial option model with u=1.1 and d = 0.9,r=0.05,T=10, and t_1=2,t_2=4 and t-3=7 the times that the asset must be checked.Also consider the S_0=100,K=125 and the barrier b=60.
My attempt is the following :
# Initialise parameters
S0 = 100 # initial stock price
K = 125 # strike price
T = 10 # time to maturity in years
b = 60 # up-and-out barrier price/value
r = 0.05 # annual risk-free rate
N = 4 # number of time steps
u = 1.1 # up-factor in binomial models
d = 0.9 # ensure recombining tree
opttype = 'C' # Option Type 'C' or 'P'
def barrier_binomial(K,T,S0,b,r,N,u,d,opttype='C'):
#precompute values
dt = T/N
q = (1+r - d)/(u-d)
disc = np.exp(-r*dt)
# initialise asset prices at maturity
S = S0 * d**(np.arange(N,-1,-1)) * u**(np.arange(0,N+1,1))
# option payoff
if opttype == 'C':
C = np.maximum( S - K, 0 )
else:
C = np.maximum( K - S, 0 )
# check terminal condition payoff
C[S >= b] = 0
# backward recursion through the tree
for i in np.arange(N-1,-1,-1):
S = S0 * d**(np.arange(i,-1,-1)) * u**(np.arange(0,i+1,1))
C[:i+1] = disc * ( q * C[1:i+2] + (1-q) * C[0:i+1] )
C = C[:-1]
C[S >= H] = 0
return C[0]
barrier_binomial(K,T,S0,b,r,N,u,d,opttype='C')
I receive nothing because something is wrong and I don’t know what
But is it a simulation ?
Any help from someone ?
In your loop you are using C[S >= H] = 0, but your barrier param is defined as b. Also you are filling the array C with 0s only, so check the payoff condition. In general, I find it much easier looping through matrices when working with tree models.

Pyomo scheduling optimization problem with non-continuous objective function

I have the following optimization problem:
Assign a set of Users to a set of Shifts that minimizes labor cost. Each user has his own hourly wage but the caveat is that any hour worked above a certain overtime threshold needs to be counted with a wage multiplier. E.g. if the threshold if 5 hours and the shift is 8 hours then 5 hours would be paid with regular user wage and the remaining 3 with the wage multiplied by a predefined factor. And it carries over to shifts being worked later in the same date range (e.g. a week) so if a Monday shift may cause a shift on Friday to be counted for overtime.
Example:
threshold: 5
multiplier: 2
wage: 10,
shift duration: 8
cost = 5 * 10 + (8 - 5) * 10 * 2 = 110
I'm modelling my problem with pyomo library for python and I ran into an issue with
Evaluating Pyomo variables in a Boolean context, e.g.
Here is the complete example code that I'm trying to run:
import numpy
from datetime import datetime
from pyomo.environ import *
from pyomo.opt import SolverFactory
users = ['U1', 'U2', 'U3']
shifts = ['S1', 'S2'] # shifts in a chronological order
user_data = {
'U1': dict(wage=10),
'U2': dict(wage=20),
'U3': dict(wage=30),
}
shifts_data = {
'S1': dict(dtstart=datetime(2020, 2, 15, 9, 0), dtend=datetime(2020, 2, 15, 18, 0)),
'S2': dict(dtstart=datetime(2020, 2, 15, 19, 0), dtend=datetime(2020, 2, 15, 23, 0))
}
OVERTIME_THRESHOLD = 5 # hours
OVERTIME_MULTIPLIER = 2
model = ConcreteModel()
# (user, shift) binary pairs. If 1 then "user" works the given "shift"
model.assignments = Var(((user, shift) for user in users for shift in shifts), within=Binary, initialize=0)
def get_shift_hours(shift):
return (shifts_data[shift]['dtend'] - shifts_data[shift]['dtstart']).total_seconds() / 3600
def get_shift_cost(m, shift, shift_index, user):
shift_hours = get_shift_hours(shift)
all_hours_including_shift = sum(get_shift_hours(s) * m.assignments[user, s] for i, s in enumerate(shifts) if i <= shift_index)
# overtime hours are any hours above the OVERTIME_THRESHOLD threshold
ot_hours_including_shift = max(0, all_hours_including_shift - OVERTIME_THRESHOLD)
all_hours_excluding_shift = sum(get_shift_hours(s) * m.assignments[user, s] for i, s in enumerate(shifts) if i < shift_index)
ot_hours_excluding_shift = max(0, all_hours_excluding_shift - OVERTIME_THRESHOLD)
shift_ot_hours = ot_hours_including_shift - ot_hours_excluding_shift
shift_reg_hous = shift_hours - shift_ot_hours
return user_data[user]['wage'] * (shift_reg_hous + OVERTIME_MULTIPLIER * shift_ot_hours)
def obj_rule(m):
s = 0
# if a shift gets scheduled it has a negative impace on the objective function so it maximizes the number of scheduled shifts
s = s - sum(m.assignments[user, shift] * 1000000 for user in users for shift in shifts)
for user in users:
for shift_index, shift in enumerate(shifts):
# add the cost of a shift if the "user" was assigned to it (via the binary decision variable)
s = s + m.assignments[user, shift] * get_shift_cost(m, shift, shift_index, user)
return s
model.constraints = ConstraintList()
"""
Constraints that ensure the same user is not scheduled for overlapping shifts
"""
def shifts_overlap(shift_1, shift_2):
s1 = shifts_data[shift_1]
s2 = shifts_data[shift_2]
return s2['dtstart'] < s1['dtend'] and s2['dtend'] > s1['dtstart']
for shift_1 in shifts:
for shift_2 in shifts:
if shift_1 == shift_2 or not shifts_overlap(shift_1, shift_2):
continue
for user in users:
model.constraints.add(
1 >= model.assignments[user, shift_1] + model.assignments[user, shift_2]
)
"""
Constraints that a shift has only 1 assignee
"""
for shift in shifts:
model.constraints.add(
1 >= sum(model.assignments[user, shift] for user in users)
)
"""
End constraints
"""
model.obj = Objective(rule=obj_rule, sense=minimize)
opt = SolverFactory('cbc') # choose a solver
results = opt.solve(model) # solve the model with the selected solver
model.pprint()
I've been reading about disjunctions and piecewise constraints but I cannot find a way to apply these concepts to my problem. Any help would be much appreciated, thanks!
This can be modeled linearly and without binary variables.
Use something like:
totalHours = normalHours + overtimeHours
normalHours <= 40
cost = normalWage*normalHours + overtimePay*overtimeHours
We are lucky: overtime is more expensive so we will automatically first exhaust the normal hours.

How to deal with inconsistent pulp solution depending on specific inputs?

I have set a small script that describes a diet optimization solution in pulp. The particular integers are not really relevant, they are just macros from foods. The strange thing is that when one of protein_ratio, carb_ratio or fat_ratio is 0.1, then the problem becomes infeasible. For other combinations of these factors (which always should add up to 1) the problem has a solution. Is there any way to sort of relax the objective function so that the solution might have a small error margin? For example instead of giving you the grams that will lead to a 800 calorie meal, it would give you the grams that lead to a 810 calorie meal. This would still be acceptable. Here is the script:
from pulp import *
target_calories = 1500
protein_ratio = 0.4 #play around with this - 0.1 breaks it
carb_ratio = 0.4 #play around with this - 0.1 breaks it
fat_ratio = 0.2 #play around with this - 0.1 breaks it
problem = LpProblem("diet", sense = LpMinimize)
gramsOfMeat = LpVariable("gramsOfMeat", lowBound = 1)
gramsOfPasta = LpVariable("gramsOfPasta", lowBound = 1 )
gramsOfOil = LpVariable("gramsOfOil", lowBound = 1)
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories
totalprotein = gramsOfMeat*0.21 + gramsOfPasta*0.13 + gramsOfOil*0
totalcarb = gramsOfMeat*0 + gramsOfPasta*0.75 + gramsOfOil*0
totalfat = gramsOfMeat*0.05 + gramsOfPasta*0.015 + gramsOfOil*1
totalmacros = totalprotein + totalcarb + totalfat
problem += totalfat== fat_ratio*totalmacros
problem += totalcarb == carb_ratio*totalmacros
problem += totalprotein == protein_ratio*totalmacros
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories == 0
status = problem.solve()
print(status)
#assert status == pulp.LpStatusOptimal
#print(totalmacros)
print("Grams of meat: {}, grams of pasta: {}, grams of oil: {}, error: {}".format(value(gramsOfMeat), value(gramsOfPasta), value(gramsOfOil), value(problem.objective)))
You can add a penalty for violating the target. The idea would be to introduce two new decision variables, say under and over, and add constraints that say
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories <= under
problem += target_calories - (gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9) <= over
Then change your objective function to something like
problem += c_under * under + c_over * over
where c_under is the penalty per unit for being under the target and c_over is the penalty for being over. (These are parameters.) If you want to impose a hard bound on the over/under, you can add new constraints:
problem += under <= max_under
problem += over <= max_over
where max_under and max_over are the maximum allowable deviations (again, parameters).
One note: Your model is a little weird because it doesn't really have an objective function. Normally in the diet problem you want to minimize cost or maximize calories or something like that, and in general in linear programming you want to minimize or maximize something. In your model, you only have constraints. True, there is something that looks like an objective function --
problem += gramsOfMeat*1.29 + gramsOfPasta*3.655 + gramsOfOil*9 - target_calories
-- but since you have constrained this to equal 0, it doesn't really have any effect. There's certainly nothing incorrect about not having an objective function, but it's unusual, and I wanted to mention it in case this is not what you intended.

Categories

Resources