I am new to Pyomo and used Pulp for my model creation till now. I wanted to understand the procedural equivalent in Pyomo of steps I used to follow in Pulp to extend a constraint.
Pulp Implementation:
con = pulp.LpConstraintVar(name="Cons1", sense=pulp.LpConstraintEQ, rhs=200)
model.addConstraint(con.constraint)
In pulp this line of code will essentially create a empty constraint for me, which will look like this (I know will be infeasible if run at this stage, but won't run the code until cons is complete):
Cons1: 0 == 200
I can then add variables to this constraint based on my requirement. For example if we consider x
is a variable to be added here, I can do this operation:
con.addVariable(x, 2)
It will result in me having extended (updated) the previous constraint to:
Cons1: 2x + 0 == 200
I can keep on calling addVariable to add as many variables I want inside multiple parts of code and it will work.
I wanted to understand how can I do similar thing in Pyomo, as this gives me better control over my constraint as compared to normal syntax->
model.Cons1= Constraint(expr = model.x*2 == 200)
You can use a named expression (Expression
) to facilitate this:
>>> model = ConcreteModel()
>>> model.x = Var()
>>> model.Cons1_body = Expression(expr=0)
>>> model.Cons1 = Constraint(expr=model.Cons1_body == 200)
>>> model.Cons1_body += model.x * 2
>>> model.pprint()
1 Var Declarations
x : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : None : None : None : False : True : Reals
1 Expression Declarations
Cons1_body : Size=1, Index=None
Key : Expression
None : 2*x
1 Constraint Declarations
Cons1 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : 200.0 : Cons1_body : 200.0 : True
3 Declarations: x Cons1_body Cons1
The complexity here arises from the philosophy in Pyomo that expressions should be immutable - that is, once created, the structure of tree that represents the expression graph can't be changed. This means that actions like replacing variables or adding terms requires creating a (partially) new expression graph. The way this impacts Constraints is that Pyomo doesn't really have a native column generation API. Instead, you would replace the Constraint expression with a new expression:
model.Cons1 = Constraint(expr = model.x*2 == 200)
model.Cons1 = model.Cons1.expr.arg(0) + model.y * 3 == model.Cons1.expr.arg(1)
The named expression (Expression
) component provides a structured way to break the expression immutability: named expressions behave like "pointers" when they are used in expressions, and the pointer is allowed to be changed without rebuilding the expression tree(s) that the named expression appear in.
Some notes:
Pyomo doesn't (easily) support defining a constraint without any variables. Attempting to create Constraint(expr=0 == 200)
will result in an exception:
ERROR: Rule failed when generating expression for Constraint Cons1 with index
None: ValueError: Invalid constraint expression. The constraint expression
resolved to a trivial Boolean (False) instead of a Pyomo object. Please modify
your rule to return Constraint.Infeasible instead of False.
You can get something similar to your "empty" constraint using the tuple notation, Constraint(expr=(200, 0, 200))
:
>>> model.Cons2 = Constraint(expr=(200, 0, 200))
>>> model.Cons2.pprint()
Cons2 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : 200.0 : 0.0 : 200.0 : True
Be careful with the "equality" form of the tuple notation, as Pyomo has no information to know if the "0" or the "200" should be the constraint body, and which should be the rhs, although the .expr.arg(0)
and .expr.arg(1)
are well-defined:
>>> model.Cons3 = Constraint(expr=(0, 200))
>>> model.Cons3.pprint()
Cons3 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : 0.0 : 200.0 : 0.0 : True
>>> model.Cons3.expr.arg(0)
0
>>> model.Cons3.expr.arg(1)
200