Constraint formulation in Pyomo with 3d-indexed varaibles - python

I have a problem with formulating constraints for 3d indexed variables with Pyomo:
I have the following variable list:
1 Var Declarations
E : Size=6, Index=N
Key : Lower : Value : Upper : Fixed : Stale : Domain
(0, 0, 2) : 0 : 10 : None : False : False : NonNegativeReals
(0, 0, 3) : 0 : 10 : None : False : False : NonNegativeReals
(0, 2, 0) : 0 : 10 : None : False : False : NonNegativeReals
(0, 3, 0) : 0 : 10 : None : False : False : NonNegativeReals
(1, 3, 1) : 0 : 10 : None : False : False : NonNegativeReals
(1, 4, 1) : 0 : 10 : None : False : False : NonNegativeReals
they are essentially values in a 3D array (2x5x5) which can take a value other then 0. I try to optimize them, with some constraints:
- the sum value of the variables in a row should be maximized
- the sum value of the variables in a column should take a certain value
My question is regarded to both constraints: I tried to formulate the first constraint like this:
def max_perf_rule(model, a, b):
return sum(model.E[a,b,c] for c in range(5) if (a,b,c) in model.N) <= H[a,b]
model.max_perf = Constraint(range(2), range(5), rule = max_perf_rule)
, where model.N= [(0, 0, 2), (0, 0, 3), (0, 2, 0), (0, 3, 0), (1, 3, 1), (1, 4, 1)]
The variables are originally given by model.N (list of 3d tuples) but I need the two "range(2)" and "range(5)" as inputs in this constraint in order to be able to refer to the proper row.
No matter what I try I cannot create the desired constraints. They should look something like this:
max_perf : Size=10, Index=max_perf_index, Active=True
(0, 0) : E[0,0,2] + E[0,0,3] : <= 0.0
(0, 2) : E[0,2,0] : <= 2688.0
(0, 3) : E[0,3,0] : <= 896.0
(1, 3) : E[1,3,1] : <= 448.0
(1, 4) : E[1,4,1] : <= 9999999.0
...but I keep getting the following error:
"ERROR: Constructing component 'max_perf' from data=None failed: ValueError:
Invalid constraint expression. The constraint expression resolved to a
trivial Boolean (True) instead of a Pyomo object. Please modify your rule
to return Constraint.Feasible instead of True.
Error thrown for Constraint 'max_perf[0,1]'"
I have no idea what this is; I even tried to reproduce the situation with a dictionary instead of model.E and it worked nicely.
Do you have any solution for the problem?
Thanks in advance!

The problem is that with the way you set up your indexing sets, not every combination of range(2) x range(5) x range(5) appears in model.N. For some combinations the sum in the constraint rule will not have any terms and therefore evaluate to a constant value of 0. The easiest way to get around this would be to add a check in your constraint rule to make sure the sum is not empty:
def max_perf_rule(model, a, b):
temp = sum(model.E[a,b,c] for c in range(5) if (a,b,c) in model.N)
if type(temp) is int: # This will be true if the sum was empty
return Constraint.Skip
return temp <= H[a,b]
model.max_perf = Constraint(range(2), range(5), rule = max_perf_rule)

Related

Create different restrictions with a single rule

I am having trouble creating multiple constraints with a single rule. The problem is the following:
I have "n" decision variables in my problem and I have "m" constraints formed by the sum of some of the decision variables, if for example (n=7) and (m=3), the constraints could be the following:
n1+n2+n3+n4=m1
n3+n4+n5=m2
n5+n6+n7=m3
The index of the decision variables I have it in a list and m value of each constraint I have it in a dictionary.
N = [1,2,3,4,5,6,7]
M = {1:20,2:12,3:18}
I define the variables
model.N = Var(N, within=NonNegativeReals)
The variables that I have to add in each constraint I have them stored in a dictionary, so that the key of each element of the dictionary represents the constraint m and the value is a list formed by the decision variables that are part of the constraint.
d = {1: [1,2,3,4], \
2: [3,4,5], \
3: [5,6,7],}
I have tried two different codes. The first one is shown below
def node_limit_rule (model,m):
# We extract the list that defines each constraint.
constraint_list = d.values()
for element in constraint_list:
ConcreteConstraint=element
return sum(model.N[cc] for cc in ConcreteConstrain) <= d[cc] for cc in ConcreteConstrains)
model.node_limit = Constraint(mydataWFLP.M, rule=node_limit_rule)
Using this code what I get is always the first constraint repeated m times
Key : Lower : Body : Upper : Active
1 : -Inf : N[1] + N[2] + N[3] + N[4] : 20.0 : True
2 : -Inf : N[1] + N[2] + N[3] + N[4] : 20.0 : True
3 : -Inf : N[1] + N[2] + N[3] + N[4] : 20.0 : True
When in fact it should be as follows
Key : Lower : Body : Upper : Active
1 : -Inf : N[1] + N[2] + N[3] + N[4] : 20.0 : True
2 : -Inf : N[3] + N[4] + N[5] : 12.0 : True
3 : -Inf : N[5] + N[6] + N[7] : 18.0 : True
What am I doing wrong? How can I fix it?
The second code I have tried is the following
constraint_list = d.values()
for element in constraint_list:
ConcreteConstraint=element
for m in M:
def node_limit_rule (model):
return sum(model.N[cc] for cc in ConcreteConstrain) <= d[cc] for cc in ConcreteConstrains)
model.node_limit = Constraint(mydataWFLP.M,rule=node_limit_rule)
But in this case I get only get one equation. Specifically the third one
Key : Lower : Body : Upper : Active
None : -Inf : N[5] + N[6] + N[7] : 18 : True
I think I should store each iteration of the for loop in a variable. Something similar to what is shown below but I don't know how to do it.
model.node_limit_1
model.node_limit_2
model.node_limit_3

How to understand this Pyomo Constraint (rewriting the constraint)

I am new to Pyomo.
I wrote this constraint in Pyomo written below as shown in this equation:
model.amount_of_energy_con = pe.ConstraintList()
for t in model.time:
for b in model.boats:
for s in model.chargers:
lhs = model.charge_energy[b, t, s]
rhs = model.c_rating[s] * model.boat_battery_capacity * model.boats_availability[b][t] * model.charging[b, t, s]
model.amount_of_energy_con.add(expr= (lhs <= rhs))
For the Constraint above, I think my constraint should be something like this in the model object
Key : Lower : Body : Upper : Active
1 : -Inf : charge_energy[1,0,SC] : 15.75*charging[1,0,SC] : True
2 : -Inf : charge_energy[1,0,FC] : 126*charging[1,0,FC] : True
But I was getting this below using model.amount_of_energy_con.pprint()
Key : Lower : Body : Upper : Active
1 : -Inf : charge_energy[1,0,SC] - 15.75*charging[1,0,SC] : 0.0 : True
2 : -Inf : charge_energy[1,0,FC] - 126*charging[1,0,FC] : 0.0 : True
Note: The 0 in the equation was added as bounds when setting up the model.charge_energy Variable model.charge_energy = pe.Var(model.boats, model.time, model.chargers, bounds=(0, None)) and I still don't understand why my Lower is -Inf.
What am I doing wrong?

Logical OR in pyomo to force consecutive assignment

I want to optimise my model that finds the highest possible overlap (assignment) of items (probes) against a sequence (sequence). I have the starting and end positions of all items and can thus build up my model as follows:
import pyomo
import pyomo.environ as pe
import pyomo.opt as po
sequence = [0, 1, 2, 3, 4, 5]
probes = ["a", "b", "c"]
probe_starts = {"a": 0, "b": 2, "c": 3}
probe_ends = {"a": 2, "b": 4, "c": 5}
# Model definition
model = pe.ConcreteModel()
model.sequence = pe.Set(initialize=sequence)
model.probes = pe.Set(initialize=probes)
model.starts = pe.Param(model.probes, initialize=probe_starts)
model.ends = pe.Param(model.probes, initialize=probe_ends)
model.assignment = pe.Var(model.sequence, model.probes, domain=pe.Binary)
# Objective
expr = sum([model.assignment[j, i] for j in model.sequence for i in model.probes])
model.objective = pe.Objective(expr=expr, sense=pe.maximize)
I now have the following three constraints:
Items can only bind one at a time (no overlapping items)
Because items have a start and end position they are limited in only getting assigned after their respective starts and ends
If items are assigned, they have to bind in their entirety spanning from their start to their end
# One probe per sequence position
model.one_probe_bound = pe.ConstraintList()
for s in model.sequence:
model.one_probe_bound.add(sum(model.assignment[s, p] for p in model.probes) <= 1)
# No assignment before/after start/end
model.define_length = pe.ConstraintList()
for s in model.sequence:
for p in model.probes:
if s < model.starts[p]:
model.define_length.add(model.assignment[s, p] == 0)
if s > model.ends[p]:
model.define_length.add(model.assignment[s, p] == 0)
Both of the constraints above work without issue but I can't find a way to input the logical or from my third condition. I tried to use the disjunction as described in this stackoverflow answer:
# Only allow full assignment or none
def disjunct_rule(b, p, i):
m = b.model()
expr = sum(m.assignment[s, p] for s in m.sequence)
if i:
return expr == m.ends[p] - m.starts[p]
else:
return expr == 0
def disjunction_rule(m, p):
return [m.disjunct[p, i] for i in [True, False]]
def xfrm(m):
pe.TransformationFactory("gdp.bigm").apply_to(m)
model.disjunct = pyomo.gdp.Disjunct(model.probes, [True, False], rule=disjunct_rule)
model.disjunction = pyomo.gdp.Disjunction(model.probes, rule=disjunction_rule)
model.xfrm = pe.BuildAction(rule=xfrm)
Looking at the matrix representation of model.assignment with sequence along the columns and probes along the rows, I get the following:
array([[1, 1, 1, 0, 0, 0],
[0, 0, 0, 1, 1, 0],
[0, 0, 0, 0, 0, 1]])
As can be seen above, I get assignments that aren't spanning the entire length of the item (e.g. c / 3rd item is only assigned at the last position whereas it should have to bind at the two previous ones too. The only valid solution I can see in this toy example is the following:
array([[1, 1, 1, 0, 0, 0],
[0, 0, 0, 0, 0, 0],
[0, 0, 0, 1, 1, 1]])
Where items a and c are selected in their entirety and b isn't selected at all. This way we have all constraints matched. The solver I used was glpk. Thanks in advance for any suggestions.
Here is a cut at this....
The other way I mentioned would be to introduce another binary variable for each point in sequence and control that (somehow) via the assignment of probes. The method below should be much more efficient than that.
Try this out with a larger dataset... The current only has 1 feasible solution. Also, I assumed that better solutions would use fewer probes, so I re-wrote the objective to that.
The basis of this solution is the constraint that you must connect to the start once (constraint 1) and the end (constraint 2) once. And any intermediate connections must be consistent (constraint 3).
I used some sub-setting in a few spots where needed.
Code:
# model to make contiguous connections across a sequence
# using as few connections (probes) as possible
import pyomo
import pyomo.environ as pe
sequence = [0, 1, 2, 3, 4, 5]
probes = ["a", "b", "c"]
probe_starts = {"a": 0, "b": 2, "c": 3}
probe_ends = {"a": 2, "b": 4, "c": 5}
# Model definition
model = pe.ConcreteModel()
model.sequence = pe.Set(initialize=sequence)
model.probes = pe.Set(initialize=probes)
model.starts = pe.Param(model.probes, initialize=probe_starts)
model.ends = pe.Param(model.probes, initialize=probe_ends)
model.assign = pe.Var(model.probes, domain=pe.Binary) # 1 if probe p is used...
# Objective
obj = sum(model.assign[p] for p in model.probes) # use as few as possible...?
model.objective = pe.Objective(expr=obj, sense=pe.minimize)
# Constraints
# must connect start once
model.C1 = pe.Constraint(expr=sum(model.assign[p] for p in model.probes
if model.starts[p] == sequence[0]) == 1)
# must connect end once
model.C2 = pe.Constraint(expr=sum(model.assign[p] for p in model.probes
if model.ends[p] == sequence[-1]) == 1)
# must connect any intermediate connections...
# if probe p1 is selected, must select an eligible p2 follow on
def connect(model, p1):
# create subset on the fly of legal follow-on connections
# assumption here that sequence is a sequential list of ints by using the "+1"
p2s = [p for p in model.probes if model.starts[p] == model.ends[p1] + 1]
if not p2s:
return pe.Constraint.Skip
return sum(model.assign[p2] for p2 in p2s) == model.assign[p1]
non_completing_probes = [p for p in model.probes if model.ends[p] != sequence[-1]]
model.C3 = pe.Constraint(non_completing_probes, rule=connect)
solver = pe.SolverFactory('glpk')
result = solver.solve(model)
print(result)
model.display()
Yields:
Problem:
- Name: unknown
Lower bound: 2.0
Upper bound: 2.0
Number of objectives: 1
Number of constraints: 4
Number of variables: 4
Number of nonzeros: 5
Sense: minimize
Solver:
- Status: ok
Termination condition: optimal
Statistics:
Branch and bound:
Number of bounded subproblems: 0
Number of created subproblems: 0
Error rc: 0
Time: 0.006771087646484375
Solution:
- number of solutions: 0
number of solutions displayed: 0
Model unknown
Variables:
assign : Size=3, Index=probes
Key : Lower : Value : Upper : Fixed : Stale : Domain
a : 0 : 1.0 : 1 : False : False : Binary
b : 0 : 0.0 : 1 : False : False : Binary
c : 0 : 1.0 : 1 : False : False : Binary
Objectives:
objective : Size=1, Index=None, Active=True
Key : Active : Value
None : True : 2.0
Constraints:
C1 : Size=1
Key : Lower : Body : Upper
None : 1.0 : 1.0 : 1.0
C2 : Size=1
Key : Lower : Body : Upper
None : 1.0 : 1.0 : 1.0
C3 : Size=1
Key : Lower : Body : Upper
a : 0.0 : 0.0 : 0.0

Pyomo assignes floats to Integer domain

Having a simplified problem: I'd like to assign an instance ID anywhere in in the instance_map. The goal is to have the ID's uniquely distributed over the instance_map, so each ID should occur exatcly once.
Pyomo in turn raises that this task is unfeasbile and most surprisingly started assigning with floats in an integer domain. Here's the code
import pyomo.environ as pe
model = pe.ConcreteModel()
model.rows = pe.RangeSet(1, 2)
model.cols = pe.RangeSet(1, 5)
model.instances = pe.RangeSet(1, 5)
model.n_instances = pe.Var(initialize=5)
model.n_cols = pe.Var(initialize=5)
model.n_rows = pe.Var(initialize=2)
model.instances_map = pe.Var(model.rows, model.cols, within=pe.Integers, initialize=0, bounds=(0, model.n_instances.value))
def unique_instances_check(model, instance):
if instance == 0:
return sum(model.instances_map[i,j] == instance for i in model.rows for j in model.cols) >= 0
else:
return sum(model.instances_map[i,j] == instance for i in model.rows for j in model.cols) == 1
model.C1 = pe.Constraint(model.instances, rule=unique_instances_check)
def objective(model):
return sum(model.instances_map[1,j] for j in model.cols)
model.obj = pe.Objective(rule=objective, sense=pe.minimize)
opt = pe.SolverFactory("ipopt").solve(model)
model.instances_map.pprint()
When running it I get the the following output for the last code line
WARNING: Loading a SolverResults object with a warning status into
model.name="unknown";
- termination condition: infeasible
- message from solver: Ipopt 3.14.5\x3a Converged to a locally
infeasible point. Problem may be infeasible.
instances_map : Size=10, Index=instances_map_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
(1, 1) : 0 : 5.000000049983252 : 5 : False : False : Integers
(1, 2) : 0 : 5.000000049983252 : 5 : False : False : Integers
(1, 3) : 0 : 5.000000049983252 : 5 : False : False : Integers
(1, 4) : 0 : 5.000000049983252 : 5 : False : False : Integers
(1, 5) : 0 : 5.000000049983252 : 5 : False : False : Integers
(2, 1) : 0 : 5.000000049983252 : 5 : False : False : Integers
(2, 2) : 0 : 5.000000049983252 : 5 : False : False : Integers
(2, 3) : 0 : 5.000000049983252 : 5 : False : False : Integers
(2, 4) : 0 : 5.000000049983252 : 5 : False : False : Integers
(2, 5) : 0 : 5.000000049983252 : 5 : False : False : Integers
I was expecting many 0 assignments but 1,2,3,4,5 only once.
I'm honestly not sure where to go from here
First, your problem is not converging to a feasible point, so there is no guarantee that the returned solution respects any constraints or bounds.
More importantly, ipopt is a continuous interior point solver and ignores discrete domains. If you look at the solver output (by adding tee=True' to the solve call), you should see:
==> Warning: Treating __ binary and __ integer variables as continous.
at the top of the solver output log.

PYOMO ERROR: KeyError: "Index '0' is not valid for indexed component 'y' "

This is the hole Error:
ERROR: Rule failed when generating expression for objective FObj: KeyError:
"Index '4' is not valid for indexed component 'y'"
ERROR: Constructing component 'FObj' from data=None failed:
KeyError: "Index '4' is not valid for indexed component 'y'"
I've proved everything, checking every RangeSet and it's ok so I don't why it doesn't work well. Thanks for reading this, if anyone could help...
from pyomo.environ import *
from pyomo.opt import SolverFactory
from pyomo.core.base.PyomoModel import AbstractModel
from pyomo.core.base.constraint import Constraint
from pyomo.core.base.set import RangeSet
#import pyomo.dae
import numpy as np
import logging
logging.getLogger('pyomo.core').setLevel(logging.ERROR)
model = AbstractModel()
model.personas = RangeSet(0, 29)
model.sabados = RangeSet(0,3)
model.y = Var(model.personas,model.sabados, within = Binary)
def ObjFunction(model):
return sum(model.y[i][s] for i in model.personas for s in model.sabados)
model.FObj= Objective(rule=ObjFunction, sense = maximize)
Problem discovered. I think you must have just changed the model type to Abstract as when I change it back to Concrete the problem with y shows up.
You are indexing model.y with double indexing (Python standard). Pyomo ... for whatever reason ... uses comma separated indices for multiple indexing. Note the change in my code below. If this is a head-hurter, I've built models and put the indices in a tuple just to keep myself sane. Such as: model.y[(i, s)] which is unnecessary, but works and makes it look more distinct for pyomo.
Couple other notes...
I removed some of the unnecceary imports. One was causing some kind
of warning.
I chopped down your indices just to see a smaller printout
from pyomo.environ import *
from pyomo.opt import SolverFactory
#from pyomo.core.base.PyomoModel import AbstractModel
#from pyomo.core.base.constraint import Constraint
#from pyomo.core.base.set import RangeSet
#import pyomo.dae
import numpy as np
import logging
#logging.getLogger('pyomo.core').setLevel(logging.ERROR)
model = ConcreteModel()
model.personas = RangeSet(0, 3)
model.sabados = RangeSet(0,2)
model.y = Var(model.personas,model.sabados, within = Binary)
def ObjFunction(model):
return sum(model.y[i,s] for i in model.personas for s in model.sabados)
model.FObj= Objective(rule=ObjFunction, sense = maximize)
model.pprint()
Yields:
1 Set Declarations
y_index : Dim=0, Dimen=2, Size=12, Domain=None, Ordered=True, Bounds=None
Virtual
2 RangeSet Declarations
personas : Dim=0, Dimen=1, Size=4, Domain=Integers, Ordered=True, Bounds=(0, 3)
Virtual
sabados : Dim=0, Dimen=1, Size=3, Domain=Integers, Ordered=True, Bounds=(0, 2)
Virtual
1 Var Declarations
y : Size=12, Index=y_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
(0, 0) : 0 : None : 1 : False : True : Binary
(0, 1) : 0 : None : 1 : False : True : Binary
(0, 2) : 0 : None : 1 : False : True : Binary
(1, 0) : 0 : None : 1 : False : True : Binary
(1, 1) : 0 : None : 1 : False : True : Binary
(1, 2) : 0 : None : 1 : False : True : Binary
(2, 0) : 0 : None : 1 : False : True : Binary
(2, 1) : 0 : None : 1 : False : True : Binary
(2, 2) : 0 : None : 1 : False : True : Binary
(3, 0) : 0 : None : 1 : False : True : Binary
(3, 1) : 0 : None : 1 : False : True : Binary
(3, 2) : 0 : None : 1 : False : True : Binary
1 Objective Declarations
FObj : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : maximize : y[0,0] + y[0,1] + y[0,2] + y[1,0] + y[1,1] + y[1,2] + y[2,0] + y[2,1] + y[2,2] + y[3,0] + y[3,1] + y[3,2]
5 Declarations: personas sabados y_index y FObj
[Finished in 2.6s]

Categories