Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Logical Modeling and Generalized Disjunctive Programs

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:

iRyi=1 \sum_{i \in R} y_i = 1

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:

  • yAy_A: absorber

  • yMy_M: membrane

  • yCSy_{CS}: cryogenic separation

How to translate this logical statement into a linear constraint?

Click here to see answer

One option:

yA+yM+2yCS2 y_A + y_M + 2y_{CS} \leq 2

Another option:

yA+yCS1 y_A + y_{CS} \leq 1

yM+yCS1 y_M + y_{CS} \leq 1

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 (Qi=P1P2 ... PrQ_i = P_1 \lor P_2 \ \lor ... \lor \ P_r) into conjunctive normal form (Q1Q2 ... QsQ_1 \land Q_2 \ \land ... \land \ Q_s).

Symbolic Logic Notation

Logical OperationLogical Symbol
OR\text{OR}\lor
AND\text{AND}\land
IMPLICATION\text{IMPLICATION}\Rightarrow
NEGATION\text{NEGATION}¬\neg
EQUIVALENCE\text{EQUIVALENCE}\Leftrightarrow
EXCLUSIVE OR\text{EXCLUSIVE OR}\veebar

Vocabulary

Literal PiP_i is a selection or action and yiy_i is the associated binary (true/false) variable:

yi={1if Pi is true0otherwise y_i = \begin{cases} 1 & \text{if~} P_i \text{~is~true} \\ 0 & \text{otherwise} \end{cases}

Negation or complement ¬Pi\neg P_i implies 1yi1 - y_i.

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 \wedge . (The clause is true when all literals are true.)

Disjunctive clause: finite collection connected with \vee . (The clause is true when at least one literal is true.)

Example: Qi Q_i is a disjunctive clause, P1P2Pr P_1 \vee P_2 \vee \dots \vee P_r , such that y1++yr1 y_1 + \dots + y_r \geq 1 .

Need a system to convert logical statements into conjunctive normal form, e.g., Q1Q2...QnQ_1 \wedge Q_2 \wedge ... \wedge Q_n

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:

P1P2¬P1P2(15.18)P_{1} \Rightarrow P_{2} \Leftrightarrow \neg P_{1} \lor P_{2} \tag{15.18}
Click to further explore this example

Let’s enumerate to better understand this example. We will look at all possible outcomes for P1P_1 and P2P_2 and then assess if the left and right statements are true or false.

P1P_1P2P_2P1P2P_{1} \Rightarrow P_{2}¬P1P2\neg P_{1} \lor P_{2}
truetruetruetrue
truefalsefalsefalse
falsetruetruetrue
falsefalsetruetrue

Step 2: Distribute the negation by applying DeMorgan’s Theorem. Examples:

¬(P1P2)¬P1¬P2(15.19)\neg (P_{1} \land P_{2}) \Leftrightarrow \neg P_{1} \lor \neg P_{2}\tag{15.19}
Click to further explore this example

Let’s enumerate to better understand this example

P1P_1P2P_2¬(P1P2)\neg (P_{1} \land P_{2})¬P1¬P2\neg P_{1} \lor \neg P_{2}
truetruefalsefalse
truefalsetruetrue
falsetruetruetrue
falsefalsetruetrue

The example holds.

¬(P1P2)¬P1¬P2(15.20)\neg (P_{1} \lor P_{2}) \Leftrightarrow \neg P_{1} \land \neg P_{2}\tag{15.20}
Click to further explore this example

Let’s enumerate to better understand this example

P1P_1P2P_2¬(P1P2)\neg (P_{1} \lor P_{2})¬P1¬P2\neg P_{1} \land \neg P_{2}
truetruefalsefalse
truefalsefalsefalse
falsetruefalsefalse
falsefalsetruetrue

The example holds.

Step 3: Recursively distribute the OR\textbf{OR} over the AND\textbf{AND} by using the following equivalence. Example:

(P1P2)P3(P1P3)(P2P3)(15.21)(P_{1} \land P_{2}) \lor P_{3} \Leftrightarrow (P_{1} \lor P_{3}) \land (P_{2} \lor P_{3})\tag{15.21}
Click to further explore this example

Let’s enumerate to better understand this example

P1P_1P2P_2P3P_3(P1P2)P3(P_{1} \land P_{2}) \lor P_{3}(P1P3)(P2P3)(P_{1} \lor P_{3}) \land (P_{2} \lor P_{3})
truetruetruetruetrue
truetruefalsetruetrue
truefalsetruetruetrue
truefalsefalsefalsefalse
falsetruetruetruetrue
falsetruefalsefalsefalse
falsefalsetruetruetrue
falsefalsefalsefalsefalse

Again, the example holds.

Once in conjunctive normal form, we can apply the following rules:

Logical RelationCommentsBoolean ExpressionRepresentation as Linear Inequalities
Logical ORP1P_{1} \lor P2P_{2}  ..\lor \ .. \lor PrP_{r}y1+y2y_{1} + y_{2} + .. + + \ ..\ + yr1y_{r} \geq 1
Logical ANDP1P_{1} \land P2P_{2}  ..\land \ .. \land PrP_{r}y11y21..yr1y_{1} \geq 1\\ y_{2} \geq 1\\ .. \\ y_{r} \geq 1
ImplicationP1P2P_{1} \Rightarrow P_{2}¬P1P2\neg P_{1} \lor P_{2}1y1+y211-y_{1} + y_{2} \geq 1
EquivalenceP1P_{1} iff P2(P1P2)(P2P1)P_{2} (P_{1} \Rightarrow P_{2}) \land (P_{2} \Rightarrow P_{1})(¬P1P2)(\neg P_{1} \lor P_{2}) \land (¬P2P1)(\neg P_{2} \lor P_{1})y1=y2y_{1} = y_{2}
Exclusive ORExactly one of the variables is trueP1P2P_{1} \veebar P_{2}  .. \veebar \ .. \ \veebar PrP_{r}y1+y2y_{1} + y_{2} + .. ++ \ ..\ + yr=1y_{r} = 1

Example: Separation Sequence

Let’s revisit the example from the top of the notebook. Reformulate:

PAPM    ¬PCSP_A \lor P_M \implies \neg P_{CS}

Step 1:

¬(PAPM)¬PCS\neg (P_A \lor P_M) \lor \neg P_{CS}

Step 2:

(¬PA¬PM)¬PCS(\neg P_A \land \neg P_M) \lor \neg P_{CS}

Step 3:

(¬PA¬PCS)(¬PM¬PCS)(\neg P_A \lor \neg P_{CS}) \land (\neg P_M \lor \neg P_{CS})

Now substituting:

¬PA1yA,¬PCS1yCS \neg P_A \rightarrow 1 - y_A, \quad \neg P_{CS} \rightarrow 1 - y_{CS}

¬PM1yM,¬PCS1yCS \neg P_M \rightarrow 1 - y_M, \quad \neg P_{CS} \rightarrow 1 - y_{CS}

We get:

(1yA)+(1yCS)1 (1 - y_A) + (1 - y_{CS}) \geq 1

(1yM)+(1yCS)1 (1 - y_M) + (1 - y_{CS}) \geq 1

Rearrange:

yA+yCS1 y_A + y_{CS} \leq 1

yM+yCS1 y_M + y_{CS} \leq 1

Example: Assembling Components

If you use (parts 1 and 2) or part 3, then you must also use parts 4 or 5.

PiP_i true means “use part ii” (corresponding to yiy_i).

(P1P2)P3    (P4P5)(P_1 \land P_2) \lor P_3 \implies (P_4 \lor P_5)

Step 1:

¬((P1P2)P3)(P4P5)\neg ((P_1 \land P_2) \lor P_3) \lor (P_4 \lor P_5)

which simplifies to:

¬(P1P2)¬P3(P4P5)\neg (P_1 \land P_2) \land \neg P_3 \lor (P_4 \lor P_5)

Step 2:

(¬P1¬P2)¬P3(P4P5)(\neg P_1 \lor \neg P_2) \land \neg P_3 \lor (P_4 \lor P_5)

Step 3:

(¬P1¬P2P4P5)(¬P3P4P5)(\neg P_1 \lor \neg P_2 \lor P_4 \lor P_5) \land (\neg P_3 \lor P_4 \lor P_5)

We now have our statement in conjunctive normal form.

On the left of the \land is equivalent to:

(1y1)+(1y2)+y4+y51(1 - y_1) + (1 - y_2) + y_4 + y_5 \geq 1

Simplifying:

y1y2+y4+y51-y_1 - y_2 + y_4 + y_5 \geq -1

Rearranging:

y1+y2y4y51y_1 + y_2 - y_4 - y_5 \leq 1

On the right of the \land is equivalent to:

(1y3)+y4+y51(1 - y_3) + y_4 + y_5 \geq 1

Simplifying:

y3y4y50y_3 - y_4 - y_5 \leq 0

Modeling Disjunctions

When modeling disjunctions, we will have to represent logical constraints that involve continuous variables.

General Notation

iDR[Yiknik(x)0ck=μik]   ,   Ω(Y)=True\lor_{i \in D_{R}} \begin{bmatrix} Y_{ik}\\ n_{ik}(x)\leq0\\ c_{k} = \mu_{ik} \end{bmatrix} \ \ \ , \ \ \ \Omega(Y)= \text{True}

where:

NotationDefinition
\lorThe OR operator that connects a finite collection of disjunctive clauses
DRD_{R}The set of disjunctive terms
YikY_{ik}Boolean “indicator variable”
nik(x)0n_{ik}(x)\leq0Constraint enforced when YikY_{ik} is true
μik\mu_{ik}Parameter values when indicator is true
Ω(Y)\Omega(Y)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 PP must be between 5 and 10 atmospheres.

  • If reactor 2 is selected, then pressure PP must be between 20 and 30 atmospheres.

Linear Disjunction Form:

iD[Aixbi]\lor_{i \in D} \begin{bmatrix} A_{i}x \leq b_{i} \end{bmatrix}

Applied to the Reactor Problem:

[y1P10P5][y2P30P20]\begin{bmatrix} y_1\\ P \leq 10\\ -P \leq -5 \end{bmatrix} \lor \begin{bmatrix} y_2\\ P \leq 30\\ -P \leq -20 \end{bmatrix}


where y1y_{1} represents reactor 1 and y2y_{2} represents reactor 2.

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 model

Transform 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:

Aixbi+Mi(1yi) , iDA_{i}x \leq b_{i} + M_{i}(1-y_{i}) \ , \ \forall i \in D
iDyi=1\sum_{i \in D} y_{i} = 1
yi{0,1} , iDy_{i} \in \{0,1\} \ , \ \forall i \in D

Applied to the Reactor Problem:

P10+M1(1y1)P5+M1(1y1)P30+M2(1y2)P20+M2(1y2)y1+y2=1P \leq 10 + M_{1}(1-y_{1})\\ -P \leq -5 + M_{1}(1-y_{1})\\ P \leq 30 + M_{2}(1-y_{2})\\ -P \leq -20 + M_{2}(1-y_{2})\\ y_{1} + y_{2} = 1

When the yy’s are considered continuous variables, weak bounds for the objective function are formed for large values such as:

M1=100  and  M2=100M_{1} = 100 \ \ \text{and} \ \ M_{2} = 100

Main Idea: Considering the special case where hi(x)=Aixbi0,h_{i}(x) = A_{i}x - b_{i} \leq 0,
MiM_{i} is sufficiently large to relax hi(x)0h_{i}(x) \leq 0 when yi=0y_{i}=0

Key Takeaways:

  • If MM is too large, we can get a “weak relaxation” because integer programming algorithms need more iterations.

  • If MM 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:

x=iDzix = \sum_{i \in D} z_{i}
Aizibiyi , iDA_{i} z_{i} \leq b_{i}y_{i} \ , \ \forall i \in D
iDyi=1\sum_{i \in D} y_{i} = 1
0ziUyi , iD0 \leq z_{i} \leq Uy_{i} \ , \ \forall i \in D
yi{0,1} , iDy_{i} \in \{0,1\} \ , \ \forall i \in D

ziz_{i}: continuous variables separated into as many new variables as there are terms for the disjunctions.

Applied to the Reactor Problem:

P=P1+P2P110y1P230y2P15y1P220y2y1+y2=1P = P_{1} + P_{2} \\ P_{1} \leq 10y_{1}\\ P_{2} \leq 30y_{2} \\ -P_{1} \leq -5y_{1}\\ -P_{2} \leq -20y_{2}\\ y_{1} + y_{2} = 1

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

  1. How do we create a system to go from logical expressions to linear constraints?

  2. Are conjunctive or disjunctive clauses more common? Why might this be the case?

  3. If yi=0y_{i}=0 or yi=1y_{i}=1, what happens when a Big-M parameter is introduced in the general notation?

  4. When will the convex hull formulation simplify?

Click to see the ideas for the discussion questions
  1. Using conjunctive normal form.

  2. Disjunctive clauses because the clause is true when at least one literal is true which occurs more often.

  3. The inequality becomes unnecessary when yi=0y_{i}=0 and the inequality is applied when yi=1y_{i}=1.

  4. If the disjunction only has two terms and one of the terms requires the variable to take a value at 0.