- from pyomo.environ import (
ConcreteModel, RangeSet, BooleanVar, LogicalConstraint, TransformationFactory, atleast, SolverFactory, Objective, Constraint, Var, land, Reference
) from pyomo.gdp import Disjunct, Disjunction from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary
# This is to make unicode comparison work in python 2.7. import sys if sys.version[0] == '2': reload(sys) sys.setdefaultencoding("utf-8")
To demonstrate modeling with disjunctions in Pyomo.GDP, we revisit the small example from the previous page <gdp-disjunctions-concept>
.
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.
m.unit1 = Disjunct()
m.unit1.inout = Constraint(expr=exp(m.x[2]) - 1 == m.x[1])
m.unit1.no_unit2_flow1 = Constraint(expr=m.x[3] == 0)
m.unit1.no_unit2_flow2 = Constraint(expr=m.x[4] == 0)
m.unit2 = Disjunct()
m.unit2.inout = Constraint(expr=exp(m.x[4] / 1.2) - 1 == m.x[3])
m.unit2.no_unit1_flow1 = Constraint(expr=m.x[1] == 0)
m.unit2.no_unit1_flow2 = Constraint(expr=m.x[2] == 0)
m.use_unit1or2 = Disjunction(expr=[m.unit1, m.unit2])
The indicator variables for each disjunct Y1 and Y2 are automatically generated by Pyomo.GDP, accessible via m.unit1.indicator_var
and m.unit2.indicator_var
.
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.
m.use1or2 = Disjunction(expr=[
# First disjunct
[exp(m.x[2])-1 == m.x[1],
m.x[3] == 0, m.x[4] == 0],
# Second disjunct
[exp(m.x[4]/1.2)-1 == m.x[3],
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,
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
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 Y1 ∨ (Y2 ⇒ Y3) rather than (Y1 ∨ Y2) ⇒ Y3
>>> 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 <gdp-reformulations>
.
Like Constraint
objects for algebraic expressions, LogicalConstraint
objects can be indexed. An example of this usage may be found below for the expression:
Yi + 1 ⇒ Yi, i ∈ {1, 2, …, n − 1}
>>> 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
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
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.
Y1 ∨ Y2 ⟹ Y3 ∧ ¬Y4 ∧ (Y5 ∨ Y6)
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])))
)
atleast(3, Y1, Y2 ∨ Y3, Y4 ⇒ Y5, Y6)
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]))
atleast(2, Y1, exactly(2, Y2, Y3, Y4), Y5, Y6)
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.
The following models all work and are equivalent for
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])