Linearize Pyomo model with BigM Constraint
Question:
I may have answered my own question posted below with the following.
I believe the construction is now appropriately linear and returns a feasible solution. Though I would appreciate any feedback as I am new to this.
In replacement of the model.bigM_cons
I outlined below, I have now implemented the following which creates a new variable (model.new
) to represent the product of model.auction*model.volume
Relevant constraints are then applied to model.new
and the new variable is also used in the decision function:
New BigM Constraint
model.m = Param(initialize = 100)
model.new = Var(model.WEEK_PROD_flat, within = NonNegativeIntegers)
# force new to be 0 if auction is 0
# force new to be between 0 and M if auction is 1
def c1_rule(model,w,p):
return model.new[w,p] <= model.auction[w,p]*model.m
model.c1 = Constraint(model.WEEK_PROD_flat, rule = c1_rule)
# force new to always be <= volume
def c2_rule(model,w,p):
return(model.new[w,p] <= model.volume[w,p])
model.c2 = Constraint(model.WEEK_PROD_flat, rule = c2_rule)
# force new to equal volume if auction = 1
def c3_rule(model,w,p):
return model.new[w,p] >= model.volume[w,p]-(1-model.auction[w,p])*model.m
model.c3 = Constraint(model.WEEK_PROD_flat, rule = c3_rule)
Original Post
I am constructing a model that involves 2 decision variables indexed over a sparse set of (week, product) indices (model.WEEK_PROD_flat
):
model.volume
is an non-negative integer variable that quantifies the amount of product to be sold.
model.auction
is a binary variable that indicates if a particular (week, product) sale should occur.
As it stands, my problem is non-linear as several constraints and the objective function involve the product of model.auction * model.volume
. Effectively, model.auction
is being used to turn on/off model.volume
.
I understand that I need to create a bigM constraint to avoid the multiplication of decision variables and to ensure that my problem formulation is linear.
I am trying to formulate a constraint that forces model.volume
to equal zero if model.auction
is equal to zero and assign a value to model.volume
only if model.auction
is equal to 1.
My current formulation of this constraint looks like this:
model.bigM = Param(initialize = 100) # define bigM value
def bigM_rule(model,w,p):
return model.volume[w,p] <= model.auction[w,p] * model.bigM
model.bigM_cons = Constraint(model.WEEK_PROD_flat, rule = bigM_rule)
When applied in a simple MWE (below), things seem to function correctly. However, when the new constraint is incorporated into my complete model, an infeasible solution is returned.
It should be noted that this complete model, when utilising the non-linear constraint and objective function DID reach a feasible solution (though I am aware this is unlikely to be a global max). The infeasible output is only returned when applying the bigM constraint and so leads me to believe that I may formulated it incorrectly?
I would appreciate some insight into where I may have gone wrong here.
Complete MWE
weekly_products = {
1: ['Q24'],
2: ['Q24', 'J24'],
3: ['Q24', 'J24','F24'],
4: ['J24', 'F24'],
5: ['F24']
}
product_weeks = {'Q24': [1, 2, 3],
'J24': [2, 3, 4],
'F24': [3, 4, 5]}
prices = {(1, 'Q24'):43.42,
(2, 'Q24'):43.73,
(2, 'J24'):24.89,
(3, 'Q24'):44.03,
(3, 'J24'):25.54,
(3, 'F24'):43.10,
(4, 'J24'):26.15,
(4, 'F24'):43.45,
(5, 'F24'):43.77}
from pyomo.environ import *
model = ConcreteModel()
# define Sets
model.WEEKS = Set(initialize = [1,2,3,4,5])
model.PRODS = Set(initialize = ['Q24','J24','F24'])
model.WEEK_PROD = Set(model.WEEKS, initialize=weekly_products)
model.WEEK_PROD_flat = Set(initialize=[(w, p) for w in model.WEEKS for p in model.WEEK_PROD[w]])
model.PROD_WEEK = Set(model.PRODS, initialize = product_weeks)
# deine Vars
model.volume = Var(model.WEEK_PROD_flat, within = NonNegativeIntegers, bounds = (0,60))
model.auction = Var(model.WEEK_PROD_flat, within = Binary)
# Define Params
model.price = Param(model.WEEK_PROD_flat, initialize = prices)
model.weekMax = Param(initialize = 1)
model.prodMax = Param(initialize = 3)
model.bigM = Param(initialize = 100)
# Define Cons
def weekMax_rule(model,i):
return sum(model.auction[i,j] for j in model.WEEK_PROD[i]) <= model.weekMax
model.weekMax_const = Constraint(model.WEEKS, rule = weekMax_rule)
def prodMax_rule(model,j):
return sum(model.auction[i,j] for i in model.PROD_WEEK[j]) <=model.prodMax
model.prodMax_const = Constraint(model.PRODS, rule = prodMax_rule)
def bigM_rule(model,w,p):
return model.volume[w,p] <= model.auction[w,p] * model.bigM
model.bigM_cons= Constraint(model.WEEK_PROD_flat, rule = bigM_rule)
# Objective function
def objective_rule(model):
return sum(model.volume[w,p] * model.price[w,p] for p in model.PRODS for w in model.PROD_WEEK[p])
model.maximiseRev = Objective(rule = objective_rule, sense = maximize)
optimizer = SolverFactory('scip')
results = optimizer.solve(model)
model.display()
Answers:
In replacement of the model.bigM_cons
I outlined below, I have now implemented the following which creates a new variable (model.new
) to represent the product of model.auction*model.volume
Relevant constraints are then applied to model.new
and the new variable is also used in the decision function:
New BigM Constraint
model.m = Param(initialize = 100)
model.new = Var(model.WEEK_PROD_flat, within = NonNegativeIntegers)
# force new to be 0 if auction is 0
# force new to be between 0 and M if auction is 1
def c1_rule(model,w,p):
return model.new[w,p] <= model.auction[w,p]*model.m
model.c1 = Constraint(model.WEEK_PROD_flat, rule = c1_rule)
# force new to always be <= volume
def c2_rule(model,w,p):
return(model.new[w,p] <= model.volume[w,p])
model.c2 = Constraint(model.WEEK_PROD_flat, rule = c2_rule)
# force new to equal volume if auction = 1
def c3_rule(model,w,p):
return model.new[w,p] >= model.volume[w,p]-(1-model.auction[w,p])*model.m
model.c3 = Constraint(model.WEEK_PROD_flat, rule = c3_rule)
I may have answered my own question posted below with the following.
I believe the construction is now appropriately linear and returns a feasible solution. Though I would appreciate any feedback as I am new to this.
In replacement of the model.bigM_cons
I outlined below, I have now implemented the following which creates a new variable (model.new
) to represent the product of model.auction*model.volume
Relevant constraints are then applied to model.new
and the new variable is also used in the decision function:
New BigM Constraint
model.m = Param(initialize = 100)
model.new = Var(model.WEEK_PROD_flat, within = NonNegativeIntegers)
# force new to be 0 if auction is 0
# force new to be between 0 and M if auction is 1
def c1_rule(model,w,p):
return model.new[w,p] <= model.auction[w,p]*model.m
model.c1 = Constraint(model.WEEK_PROD_flat, rule = c1_rule)
# force new to always be <= volume
def c2_rule(model,w,p):
return(model.new[w,p] <= model.volume[w,p])
model.c2 = Constraint(model.WEEK_PROD_flat, rule = c2_rule)
# force new to equal volume if auction = 1
def c3_rule(model,w,p):
return model.new[w,p] >= model.volume[w,p]-(1-model.auction[w,p])*model.m
model.c3 = Constraint(model.WEEK_PROD_flat, rule = c3_rule)
Original Post
I am constructing a model that involves 2 decision variables indexed over a sparse set of (week, product) indices (model.WEEK_PROD_flat
):
model.volume
is an non-negative integer variable that quantifies the amount of product to be sold.
model.auction
is a binary variable that indicates if a particular (week, product) sale should occur.
As it stands, my problem is non-linear as several constraints and the objective function involve the product of model.auction * model.volume
. Effectively, model.auction
is being used to turn on/off model.volume
.
I understand that I need to create a bigM constraint to avoid the multiplication of decision variables and to ensure that my problem formulation is linear.
I am trying to formulate a constraint that forces model.volume
to equal zero if model.auction
is equal to zero and assign a value to model.volume
only if model.auction
is equal to 1.
My current formulation of this constraint looks like this:
model.bigM = Param(initialize = 100) # define bigM value
def bigM_rule(model,w,p):
return model.volume[w,p] <= model.auction[w,p] * model.bigM
model.bigM_cons = Constraint(model.WEEK_PROD_flat, rule = bigM_rule)
When applied in a simple MWE (below), things seem to function correctly. However, when the new constraint is incorporated into my complete model, an infeasible solution is returned.
It should be noted that this complete model, when utilising the non-linear constraint and objective function DID reach a feasible solution (though I am aware this is unlikely to be a global max). The infeasible output is only returned when applying the bigM constraint and so leads me to believe that I may formulated it incorrectly?
I would appreciate some insight into where I may have gone wrong here.
Complete MWE
weekly_products = {
1: ['Q24'],
2: ['Q24', 'J24'],
3: ['Q24', 'J24','F24'],
4: ['J24', 'F24'],
5: ['F24']
}
product_weeks = {'Q24': [1, 2, 3],
'J24': [2, 3, 4],
'F24': [3, 4, 5]}
prices = {(1, 'Q24'):43.42,
(2, 'Q24'):43.73,
(2, 'J24'):24.89,
(3, 'Q24'):44.03,
(3, 'J24'):25.54,
(3, 'F24'):43.10,
(4, 'J24'):26.15,
(4, 'F24'):43.45,
(5, 'F24'):43.77}
from pyomo.environ import *
model = ConcreteModel()
# define Sets
model.WEEKS = Set(initialize = [1,2,3,4,5])
model.PRODS = Set(initialize = ['Q24','J24','F24'])
model.WEEK_PROD = Set(model.WEEKS, initialize=weekly_products)
model.WEEK_PROD_flat = Set(initialize=[(w, p) for w in model.WEEKS for p in model.WEEK_PROD[w]])
model.PROD_WEEK = Set(model.PRODS, initialize = product_weeks)
# deine Vars
model.volume = Var(model.WEEK_PROD_flat, within = NonNegativeIntegers, bounds = (0,60))
model.auction = Var(model.WEEK_PROD_flat, within = Binary)
# Define Params
model.price = Param(model.WEEK_PROD_flat, initialize = prices)
model.weekMax = Param(initialize = 1)
model.prodMax = Param(initialize = 3)
model.bigM = Param(initialize = 100)
# Define Cons
def weekMax_rule(model,i):
return sum(model.auction[i,j] for j in model.WEEK_PROD[i]) <= model.weekMax
model.weekMax_const = Constraint(model.WEEKS, rule = weekMax_rule)
def prodMax_rule(model,j):
return sum(model.auction[i,j] for i in model.PROD_WEEK[j]) <=model.prodMax
model.prodMax_const = Constraint(model.PRODS, rule = prodMax_rule)
def bigM_rule(model,w,p):
return model.volume[w,p] <= model.auction[w,p] * model.bigM
model.bigM_cons= Constraint(model.WEEK_PROD_flat, rule = bigM_rule)
# Objective function
def objective_rule(model):
return sum(model.volume[w,p] * model.price[w,p] for p in model.PRODS for w in model.PROD_WEEK[p])
model.maximiseRev = Objective(rule = objective_rule, sense = maximize)
optimizer = SolverFactory('scip')
results = optimizer.solve(model)
model.display()
In replacement of the model.bigM_cons
I outlined below, I have now implemented the following which creates a new variable (model.new
) to represent the product of model.auction*model.volume
Relevant constraints are then applied to model.new
and the new variable is also used in the decision function:
New BigM Constraint
model.m = Param(initialize = 100)
model.new = Var(model.WEEK_PROD_flat, within = NonNegativeIntegers)
# force new to be 0 if auction is 0
# force new to be between 0 and M if auction is 1
def c1_rule(model,w,p):
return model.new[w,p] <= model.auction[w,p]*model.m
model.c1 = Constraint(model.WEEK_PROD_flat, rule = c1_rule)
# force new to always be <= volume
def c2_rule(model,w,p):
return(model.new[w,p] <= model.volume[w,p])
model.c2 = Constraint(model.WEEK_PROD_flat, rule = c2_rule)
# force new to equal volume if auction = 1
def c3_rule(model,w,p):
return model.new[w,p] >= model.volume[w,p]-(1-model.auction[w,p])*model.m
model.c3 = Constraint(model.WEEK_PROD_flat, rule = c3_rule)