Modeling in Pyomo.GDP
Disjunctions
To demonstrate modeling with disjunctions in Pyomo.GDP, we revisit the small example from the previous page.
Explicit syntax: more descriptive
Pyomo.GDP explicit syntax (see below) provides more clarity in the declaration of each modeling object, and gives the user explicit control over the Disjunct
names.
Assuming the ConcreteModel
object m
and variables have been defined, lines 1 and 5 declare the Disjunct
objects corresponding to selection of unit 1 and 2, respectively.
Lines 2 and 6 define the input-output relations for each unit, and lines 3-4 and 7-8 enforce zero flow through the unit that is not selected.
Finally, line 9 declares the logical disjunction between the two disjunctive terms.
1m.unit1 = Disjunct()
2m.unit1.inout = Constraint(expr=exp(m.x[2]) - 1 == m.x[1])
3m.unit1.no_unit2_flow1 = Constraint(expr=m.x[3] == 0)
4m.unit1.no_unit2_flow2 = Constraint(expr=m.x[4] == 0)
5m.unit2 = Disjunct()
6m.unit2.inout = Constraint(expr=exp(m.x[4] / 1.2) - 1 == m.x[3])
7m.unit2.no_unit1_flow1 = Constraint(expr=m.x[1] == 0)
8m.unit2.no_unit1_flow2 = Constraint(expr=m.x[2] == 0)
9m.use_unit1or2 = Disjunction(expr=[m.unit1, m.unit2])
The indicator variables for each disjunct \(Y_1\) and \(Y_2\) are automatically generated by Pyomo.GDP, accessible via m.unit1.indicator_var
and m.unit2.indicator_var
.
Compact syntax: more concise
For more advanced users, a compact syntax is also available below, taking advantage of the ability to declare disjuncts and constraints implicitly.
When the Disjunction
object constructor is passed a list of lists, the outer list defines the disjuncts and the inner list defines the constraint expressions associated with the respective disjunct.
1m.use1or2 = Disjunction(expr=[
2 # First disjunct
3 [exp(m.x[2])-1 == m.x[1],
4 m.x[3] == 0, m.x[4] == 0],
5 # Second disjunct
6 [exp(m.x[4]/1.2)-1 == m.x[3],
7 m.x[1] == 0, m.x[2] == 0]])
Note
By default, Pyomo.GDP Disjunction
objects enforce an implicit “exactly one” relationship among the selection of the disjuncts (generalization of exclusive-OR).
That is, exactly one of the Disjunct
indicator variables should take a True
value.
This can be seen as an implicit logical proposition, in our example, \(Y_1 \underline{\lor} Y_2\).
Logical Propositions
Pyomo.GDP also supports the use of logical propositions through the use of the BooleanVar
and LogicalConstraint
objects.
The BooleanVar
object in Pyomo represents Boolean variables, analogous to Var
for numeric variables.
BooleanVar
can be indexed over a Pyomo Set
, as below:
>>> m = ConcreteModel()
>>> m.my_set = RangeSet(4)
>>> m.Y = BooleanVar(m.my_set)
>>> m.Y.display()
Y : Size=4, Index=my_set
Key : Value : Fixed : Stale
1 : None : False : True
2 : None : False : True
3 : None : False : True
4 : None : False : True
Using these Boolean variables, we can define LogicalConstraint
objects, analogous to algebraic Constraint
objects.
>>> m.p = LogicalConstraint(expr=m.Y[1].implies(land(m.Y[2], m.Y[3])).lor(m.Y[4]))
>>> m.p.pprint()
p : Size=1, Index=None, Active=True
Key : Body : Active
None : (Y[1] --> Y[2] ∧ Y[3]) ∨ Y[4] : True
Supported Logical Operators
Pyomo.GDP logical expression system supported operators and their usage are listed in the table below.
Operator | Operator | Method | Function |
---|---|---|---|
Conjunction | Y[1].land(Y[2]) |
land(Y[1],Y[2]) |
|
Disjunction | Y[1].lor(Y[2]) |
lor(Y[1],Y[2]) |
|
Negation | ~Y[1] |
lnot(Y[1]) |
|
Exclusive OR | Y[1].xor(Y[2]) |
xor(Y[1], Y[2]) |
|
Implication | Y[1].implies(Y[2]) |
implies(Y[1], Y[2]) |
|
Equivalence | Y[1].equivalent_to(Y[2]) |
equivalent(Y[1], Y[2]) |
In addition, the following constraint-programming-inspired operators are provided: exactly
, atmost
, and atleast
.
These predicates enforce, respectively, that exactly, at most, or at least N of their BooleanVar
arguments are True
.
Usage:
atleast(3, Y[1], Y[2], Y[3])
atmost(3, Y)
exactly(3, Y)
Note
We omit support for most infix operators, e.g. Y[1] >> Y[2]
, due to concerns about non-intuitive Python operator precedence.
That is Y[1] | Y[2] >> Y[3]
would translate to \(Y_1 \lor (Y_2 \Rightarrow Y_3)\) rather than \((Y_1 \lor Y_2) \Rightarrow Y_3\)
>>> m = ConcreteModel()
>>> m.my_set = RangeSet(4)
>>> m.Y = BooleanVar(m.my_set)
>>> m.p = LogicalConstraint(expr=atleast(3, m.Y))
>>> TransformationFactory('core.logical_to_linear').apply_to(m)
>>> m.logic_to_linear.transformed_constraints.pprint() # constraint auto-generated by transformation
transformed_constraints : Size=1, Index=logic_to_linear.transformed_constraints_index, Active=True
Key : Lower : Body : Upper : Active
1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True
>>> m.p.pprint()
p : Size=1, Index=None, Active=False
Key : Body : Active
None : atleast(3: [Y[1], Y[2], Y[3], Y[4]]) : False
We elaborate on the logical_to_linear
transformation on the next page.
Indexed logical constraints
Like Constraint
objects for algebraic expressions, LogicalConstraint
objects can be indexed.
An example of this usage may be found below for the expression:
>>> m = ConcreteModel()
>>> n = 5
>>> m.I = RangeSet(n)
>>> m.Y = BooleanVar(m.I)
>>> @m.LogicalConstraint(m.I)
... def p(m, i):
... return m.Y[i+1].implies(m.Y[i]) if i < n else Constraint.Skip
>>> m.p.pprint()
p : Size=4, Index=I, Active=True
Key : Body : Active
1 : Y[2] --> Y[1] : True
2 : Y[3] --> Y[2] : True
3 : Y[4] --> Y[3] : True
4 : Y[5] --> Y[4] : True
Integration with Disjunctions
Note
Historically, the indicator_var
on Disjunct
objects was
implemented as a binary Var
. Beginning in Pyomo 6.0, that has
been changed to the more mathematically correct BooleanVar
, with
the associated binary variable available as
binary_indicator_var
.
The logical expression system is designed to augment the previously
introduced Disjunct
and Disjunction
components. Mathematically,
the disjunct indicator variable is Boolean, and can be used directly in
logical propositions.
Here, we demonstrate this capability with a toy example:
>>> m = ConcreteModel()
>>> m.s = RangeSet(4)
>>> m.ds = RangeSet(2)
>>> m.d = Disjunct(m.s)
>>> m.djn = Disjunction(m.ds)
>>> m.djn[1] = [m.d[1], m.d[2]]
>>> m.djn[2] = [m.d[3], m.d[4]]
>>> m.x = Var(bounds=(-2, 10))
>>> m.d[1].c = Constraint(expr=m.x >= 2)
>>> m.d[2].c = Constraint(expr=m.x >= 3)
>>> m.d[3].c = Constraint(expr=m.x <= 8)
>>> m.d[4].c = Constraint(expr=m.x == 2.5)
>>> m.o = Objective(expr=m.x)
>>> # Add the logical proposition
>>> m.p = LogicalConstraint(
... expr=m.d[1].indicator_var.implies(m.d[4].indicator_var))
>>> # Note: the implicit XOR enforced by m.djn[1] and m.djn[2] still apply
>>> # Apply the Big-M reformulation: It will convert the logical
>>> # propositions to algebraic expressions.
>>> TransformationFactory('gdp.bigm').apply_to(m)
>>> # Before solve, Boolean vars have no value
>>> Reference(m.d[:].indicator_var).display()
IndexedBooleanVar : Size=4, Index=s, ReferenceTo=d[:].indicator_var
Key : Value : Fixed : Stale
1 : None : False : True
2 : None : False : True
3 : None : False : True
4 : None : False : True
>>> # Solve the reformulated model
>>> run_data = SolverFactory('glpk').solve(m)
>>> Reference(m.d[:].indicator_var).display()
IndexedBooleanVar : Size=4, Index=s, ReferenceTo=d[:].indicator_var
Key : Value : Fixed : Stale
1 : True : False : False
2 : False : False : False
3 : False : False : False
4 : True : False : False
Advanced LogicalConstraint Examples
Support for complex nested expressions is a key benefit of the logical expression system. Below are examples of expressions that we support, and with some, an explanation of their implementation.
Composition of standard operators
m.p = LogicalConstraint(expr=lor(m.Y[1], m.Y[2]).implies(
land(m.Y[3], ~m.Y[4], m.Y[5].lor(m.Y[6])))
)
Expressions within CP-type operators
Here, augmented variables may be automatically added to the model as follows:
m.p = LogicalConstraint(
expr=atleast(3, m.Y[1], Or(m.Y[2], m.Y[3]), m.Y[4].implies(m.Y[5]), m.Y[6]))
Nested CP-style operators
Here, we again need to add augmented variables:
However, we also need to further interpret the second statement as a disjunction:
or equivalently,
m.p = LogicalConstraint(
expr=atleast(2, m.Y[1], exactly(2, m.Y[2], m.Y[3], m.Y[4]), m.Y[5], m.Y[6]))
In the logical_to_linear
transformation, we automatically convert these special disjunctions to linear form using a Big M reformulation.
Additional Examples
The following models all work and are equivalent for \(\left[x = 0\right] \underline{\lor} \left[y = 0\right]\):
Option 1: Rule-based construction
>>> from pyomo.environ import *
>>> from pyomo.gdp import *
>>> model = ConcreteModel()
>>> model.x = Var()
>>> model.y = Var()
>>> # Two conditions
>>> def _d(disjunct, flag):
... model = disjunct.model()
... if flag:
... # x == 0
... disjunct.c = Constraint(expr=model.x == 0)
... else:
... # y == 0
... disjunct.c = Constraint(expr=model.y == 0)
>>> model.d = Disjunct([0,1], rule=_d)
>>> # Define the disjunction
>>> def _c(model):
... return [model.d[0], model.d[1]]
>>> model.c = Disjunction(rule=_c)
Option 2: Explicit disjuncts
>>> from pyomo.environ import *
>>> from pyomo.gdp import *
>>> model = ConcreteModel()
>>> model.x = Var()
>>> model.y = Var()
>>> model.fix_x = Disjunct()
>>> model.fix_x.c = Constraint(expr=model.x == 0)
>>> model.fix_y = Disjunct()
>>> model.fix_y.c = Constraint(expr=model.y == 0)
>>> model.c = Disjunction(expr=[model.fix_x, model.fix_y])
Option 3: Implicit disjuncts (disjunction rule returns a list of
expressions or a list of lists of expressions)
>>> from pyomo.environ import *
>>> from pyomo.gdp import *
>>> model = ConcreteModel()
>>> model.x = Var()
>>> model.y = Var()
>>> model.c = Disjunction(expr=[model.x == 0, model.y == 0])