Prepared by: Prof. Alexander Dowling (adowling@nd.edu), Hailey Lynch (hlynch@nd.edu, 2023)
Introduction and Learning Objectives¶
This notebook introduces generalized disjunctive programs through an example in Pyomo.GDP. Students will learn concepts related to Logical Modeling and Modeling Disjunctions in this notebook. These techniques will be applied to the Reactor Problem and then implemented into Pyomo. Critical thinking discussion questions will be included to connect concepts from CBE 60499.
Import Modules¶
# Imports
import sys
if "google.colab" in sys.modules:
!wget "https://raw.githubusercontent.com/ndcbe/optimization/main/notebooks/helper.py"
import helper
helper.easy_install()
else:
sys.path.insert(0, "../")
import helper
helper.set_plotting_style()
milp_solver = "appsi_highs"Motivating Example: Separation¶
The following excerpts are from Section 15.7 in Biegler, Grossmann, and Westerberg (1997).
From Integer Programs, we saw that modeling “choose only 1 item” is straightforward:
Let’s consider something more complex:
“If the absorber to recover the product is selected or the membrane is selected, then do not use cryogenic distillation.”
Yes/No binary decisions:
: absorber
: membrane
: cryogenic separation
How to translate this logical statement into a linear constraint?
Click here to see answer
One option:
Another option:
They are equivalent, but the latter is “tighter”; it constrains more of the feasible space. (Think of 3D visualization.)
We seek a formal system to go from logical statements to linear constraints.
Logical Modeling¶
First we will look at important logic notation that is commonly used in logical modeling. This will enable us to convert logical expressions such as disjunctive clauses () into conjunctive normal form ().
Symbolic Logic Notation¶
| Logical Operation | Logical Symbol |
|---|---|
Vocabulary¶
Literal is a selection or action and is the associated binary (true/false) variable:
Negation or complement implies .
Conjunctive normal form: sequence of clauses connected by AND operators
Atom or atomic formula: no deeper structure (e.g., no connectives or subformulas)
Literal: atom or its negation
Conjunctive clause: finite collection of literals connected with . (The clause is true when all literals are true.)
Disjunctive clause: finite collection connected with . (The clause is true when at least one literal is true.)
Example: is a disjunctive clause, , such that .
Need a system to convert logical statements into conjunctive normal form, e.g.,
Logical Statements and Conjunctive Normal Form¶
The following excerpts are from Section 15.7 in Biegler, Grossmann, and Westerberg (1997).
The three step procedure below shows how to convert logical statements into conjunctive normal form:
Step 1: Replace the implication by its equivalent disjunction. Example:
Click to further explore this example
Let’s enumerate to better understand this example. We will look at all possible outcomes for and and then assess if the left and right statements are true or false.
| true | true | true | true |
| true | false | false | false |
| false | true | true | true |
| false | false | true | true |
Step 2: Distribute the negation by applying DeMorgan’s Theorem. Examples:
Click to further explore this example
Let’s enumerate to better understand this example
| true | true | false | false |
| true | false | true | true |
| false | true | true | true |
| false | false | true | true |
The example holds.
Click to further explore this example
Let’s enumerate to better understand this example
| true | true | false | false |
| true | false | false | false |
| false | true | false | false |
| false | false | true | true |
The example holds.
Step 3: Recursively distribute the over the by using the following equivalence. Example:
Click to further explore this example
Let’s enumerate to better understand this example
| true | true | true | true | true |
| true | true | false | true | true |
| true | false | true | true | true |
| true | false | false | false | false |
| false | true | true | true | true |
| false | true | false | false | false |
| false | false | true | true | true |
| false | false | false | false | false |
Again, the example holds.
Once in conjunctive normal form, we can apply the following rules:
| Logical Relation | Comments | Boolean Expression | Representation as Linear Inequalities |
|---|---|---|---|
| Logical OR | |||
| Logical AND | |||
| Implication | |||
| Equivalence | iff | ||
| Exclusive OR | Exactly one of the variables is true |
Example: Separation Sequence¶
Let’s revisit the example from the top of the notebook. Reformulate:
Step 1:
Step 2:
Step 3:
Now substituting:
We get:
Rearrange:
Example: Assembling Components¶
If you use (parts 1 and 2) or part 3, then you must also use parts 4 or 5.
true means “use part ” (corresponding to ).
Step 1:
which simplifies to:
Step 2:
Step 3:
We now have our statement in conjunctive normal form.
On the left of the is equivalent to:
Simplifying:
Rearranging:
On the right of the is equivalent to:
Simplifying:
Modeling Disjunctions¶
When modeling disjunctions, we will have to represent logical constraints that involve continuous variables.
General Notation¶
where:
| Notation | Definition |
|---|---|
| The OR operator that connects a finite collection of disjunctive clauses | |
| The set of disjunctive terms | |
| Boolean “indicator variable” | |
| Constraint enforced when is true | |
| Parameter values when indicator is true | |
| Additional logical constraints |
Example: The Reactor Problem¶
The following excerpts are from Section 15.8 in Biegler, Grossmann, and Westerberg (1997).
This modeling disjunctions example involves selecting between two reactors:
If reactor 1 is selected, then pressure must be between 5 and 10 atmospheres.
If reactor 2 is selected, then pressure must be between 20 and 30 atmospheres.
Linear Disjunction Form:
Define Model in Pyomo with GDP¶
First we will define the model (including disjunctions) for the Reactor Problem in Pyomo.
"""
Instead of using
# import pyomo.environ as pyo
We can import specific functions/objects
"""
from pyomo.environ import (
check_optimal_termination,
ConcreteModel,
Param,
Set,
SolverFactory,
TransformationFactory,
Var,
)def create_model():
"""
Build the reactor problem model.
Return:
model: Pyomo model
"""
## Model
model = ConcreteModel(name="Selecting a reactor")
## Sets
# Initialized for reactor 1 (1) and reactor 2 (2)
model.reactors = Set(initialize=[1, 2])
## Parameters
# Initialized with a dictionary where the keys are 1 and 2 (the reactors)
# for the minimum and maximum pressure values (atm)
model.min_pressure = Param(model.reactors, initialize={1: 5, 2: 20})
model.max_pressure = Param(model.reactors, initialize={1: 10, 2: 30})
## Variables
# Reactor pressure bounded between the lower bound (5 atm) and upper bound (30 atm)
model.P = Var(bounds=(5, 30), doc="Reactor pressure (atm)")
## Adding an objective for the example
@model.Objective()
def objective(b):
return b.P
## Disjunction
# ONE disjunction over the reactors. Each DISJUNCT is itself a list -- the
# two pressure bounds that hold together when that reactor is selected.
# Note: Pyomo.GDP by default treats the disjunction as a xor (choose only one)
# https://pyomo.readthedocs.io/en/latest/modeling_extensions/gdp/modeling.html
@model.Disjunction(
doc="Select exactly one reactor; each brings its own pressure window"
)
def pressure_bounds(b):
return [
[b.P <= b.max_pressure[r], b.P >= b.min_pressure[r]] for r in b.reactors
]
return modelTransform and Solve with Big-M Relaxation¶
The following excerpts are from Section 15.8 in Biegler, Grossmann, and Westerberg (1997).
Use “Big-M” constraints to convert linear disjunctions into mixed-integer constraints to represent logic with continuous variables.
Big-M Relaxation Approach¶
General Notation:
Applied to the Reactor Problem:
When the ’s are considered continuous variables, weak bounds for the objective function are formed for large values such as:
Main Idea:
Considering the special case where
is sufficiently large to relax when
Key Takeaways:
If is too large, we can get a “weak relaxation” because integer programming algorithms need more iterations.
If is too small, we can get unintended bounds.
Big-M is the best to use if the problem is small.
Big-M Implementation in Pyomo¶
First we will create and print the model.
# Creating the model
model = create_model()
# Printing the model
model.pprint()1 Set Declarations
reactors : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 2 : {1, 2}
2 Param Declarations
max_pressure : Size=2, Index=reactors, Domain=Any, Default=None, Mutable=False
Key : Value
1 : 10
2 : 30
min_pressure : Size=2, Index=reactors, Domain=Any, Default=None, Mutable=False
Key : Value
1 : 5
2 : 20
1 Var Declarations
P : Reactor pressure (atm)
Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 5 : None : 30 : False : True : Reals
1 Objective Declarations
objective : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : minimize : P
1 Disjunct Declarations
pressure_bounds_disjuncts : Size=2, Index=Any, Active=True
pressure_bounds_disjuncts[0] : Active=True
1 Var Declarations
binary_indicator_var : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : None : 1 : False : True : Binary
1 Constraint Declarations
constraint : Size=2, Index={1, 2}, Active=True
Key : Lower : Body : Upper : Active
1 : -Inf : P : 10.0 : True
2 : 5.0 : P : +Inf : True
1 BooleanVar Declarations
indicator_var : Size=1, Index=None
Key : Value : Fixed : Stale
None : None : False : True
1 LogicalConstraint Declarations
propositions : Size=0, Index={}, Active=True
Key : Body : Active
4 Declarations: indicator_var binary_indicator_var constraint propositions
pressure_bounds_disjuncts[1] : Active=True
1 Var Declarations
binary_indicator_var : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : None : 1 : False : True : Binary
1 Constraint Declarations
constraint : Size=2, Index={1, 2}, Active=True
Key : Lower : Body : Upper : Active
1 : -Inf : P : 30.0 : True
2 : 20.0 : P : +Inf : True
1 BooleanVar Declarations
indicator_var : Size=1, Index=None
Key : Value : Fixed : Stale
None : None : False : True
1 LogicalConstraint Declarations
propositions : Size=0, Index={}, Active=True
Key : Body : Active
4 Declarations: indicator_var binary_indicator_var constraint propositions
1 Disjunction Declarations
pressure_bounds : Select exactly one reactor; each brings its own pressure window
Size=1, Index=None, Active=True
Key : Disjuncts : Active : XOR
None : ['pressure_bounds_disjuncts[0]', 'pressure_bounds_disjuncts[1]'] : True : True
7 Declarations: reactors min_pressure max_pressure P objective pressure_bounds pressure_bounds_disjuncts
Next, let’s transform using Big-M and print the model again.
# Applying Big-M relaxation to the model
# Add your solution here
# Printing
model.pprint()Click to see the solution to the activity
TransformationFactory("gdp.bigm").apply_to(model)Finally, we’ll solve the model and examine the solution.
# Solve and print the solution
results = SolverFactory(milp_solver).solve(model, tee=True)
assert check_optimal_termination(results), (
f"Solve failed: status={results.solver.status}, "
f"termination={results.solver.termination_condition}"
)
model.P.display()P : Reactor pressure (atm)
Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 5 : 5.0 : 30 : False : False : Reals
Transform and Solve with Convex Hull Relaxation¶
The following excerpts are from Section 15.8 in Biegler, Grossmann, and Westerberg (1997).
Convex hull can be used if we don’t want to implement Big-M parameters. This approach requires separating the continuous variables into its components.
Convex Hull Relaxation Approach¶
General Notation:
: continuous variables separated into as many new variables as there are terms for the disjunctions.
Applied to the Reactor Problem:
Key Takeaways:
(+) Constraints do not require Big-M parameters which produce a tight linear programming relaxation.
(--) A larger number of variables and constraints is required.
Convex hull is better to use over Big-M if the problem is large.
Convex Hull Implementation in Pyomo¶
We will repeat the procedure above but using Convex Hull now.
# Creating the model
model = create_model()
# Applying convex hull relaxation to the model
# Add your solution here
# Solve and print the solution
results = SolverFactory(milp_solver).solve(model, tee=True)
assert check_optimal_termination(results), (
f"Solve failed: status={results.solver.status}, "
f"termination={results.solver.termination_condition}"
)
model.P.display()Click to see the solution to the activity
TransformationFactory("gdp.hull").apply_to(model)Discussion Questions¶
How do we create a system to go from logical expressions to linear constraints?
Are conjunctive or disjunctive clauses more common? Why might this be the case?
If or , what happens when a Big-M parameter is introduced in the general notation?
When will the convex hull formulation simplify?
Click to see the ideas for the discussion questions
Using conjunctive normal form.
Disjunctive clauses because the clause is true when at least one literal is true which occurs more often.
The inequality becomes unnecessary when and the inequality is applied when .
If the disjunction only has two terms and one of the terms requires the variable to take a value at 0.