Prepared by: Prof. Alexander Dowling (adowling@nd.edu), Hailey Lynch (hlynch@nd.edu, 2023)
Introduction and Learning Objectives¶
Translate logical statements into binary constraints, model continuous alternatives with Pyomo.GDP, and compare Big-M and hull reformulations. The separator and parts examples develop the logic; the reactor example shows how bounds affect the reformulation. Full derivations and reproducible diagnostics supplement the lecture activities.
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).

Black circles mark the common binary vertices; squares mark the two extra aggregate vertices. The slice exposes the printed fractional witness.
If the absorber is selected or the membrane is selected, do not use cryogenic separation. Let indicate these selections. The OR is inclusive: selecting both absorber and membrane is permitted.
Translate each way the antecedent can be true first:
Adding the two rows gives the aggregate:
The pair implies the aggregate even for fractional variables. Conversely, for binary variables the aggregate enforces both rows: if , it forces ; if , it imposes no further restriction. Thus both formulations describe the same binary choices.
They differ on the unit cube . At , the aggregate holds with equality, but the first pairwise row reads and fails. The pair therefore has a strictly tighter LP relaxation. The all-zero choice remains feasible: the implication does not require installing a separator.
Source: Biegler, Grossmann, and Westerberg (1997), §15.7, pp. 514–517, Example 15.4.
Logical Modeling¶
An atomic statement can represent a selection, such as “use the absorber.” Logical connectives combine these statements. Conjunctive normal form separates the resulting formula into disjunctive clauses, each of which becomes one linear inequality.
Symbolic Logic Notation¶
| Logical Operation | Logical Symbol |
|---|---|
Vocabulary¶
An atom has no logical connective inside it. Associate the binary variable with a true atom and with a false atom. A literal is an atom or its negation ; their numerical truth values are and .
A disjunctive clause is an OR of finitely many literals. It is true when at least one literal is true. A conjunctive clause is an AND of literals; all must be true. Conjunctive normal form (CNF) is an AND of disjunctive clauses, not an arbitrary sequence of clauses.
For example,
is CNF and translates to
More generally, if a clause has positive-literal indices and negative-literal indices , its row is
Each literal is 0 or 1, so this sum is at least one exactly when the clause is true. Retain binary domains: this equivalence concerns truth assignments, not fractional truth values.

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, translate each clause separately. The following dictionary also handles common direct relations.
| Relation | Boolean statement | Binary representation |
|---|---|---|
| Inclusive OR | ||
| AND | for every | |
| Implication | ||
| Equivalence | ||
| Two-input exclusive OR | ||
| Exactly one of statements | exactly one is true | |
| At most one | at most one is true |
A chain of ordinary two-input XORs is a parity test: for three true inputs, is true. It is not the exactly-one relation for three or more inputs. In GDP below, “exclusive selection” means exactly one selected indicator, as in Pyomo’s default disjunction semantics.
Source: BGW, Table 15.1, p. 515, and Eqs. (15.18)–(15.21), pp. 515–516. The exactly-one row above states the intended cardinality directly rather than interpreting a repeated XOR as cardinality.
Derived AND- and OR-implication rows¶
For , implication removal and De Morgan give . Its single clause is , or .
For , the same first steps give . Distributing yields . The two rows are and , equivalently and . Adding them gives the weaker aggregate on fractional variables. The separator example substitutes the consequence and thus .
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 (parts 1 and 2) or part 3 is used, then parts 4 or 5 must also be used. Here means “use part ,” and both ORs are inclusive.
Step 1: replace the implication. Apply with the whole antecedent and :
Step 2: move the negations inward. De Morgan’s laws first negate the outer OR and then the inner AND:
Step 3: distribute OR over AND. With , distribute into :
Translate the two clauses separately:
Equivalently,
The first row has , not , in its negative terms. For example, parts 1 and 2 without 4 or 5 must be rejected even when part 3 is absent. Source: BGW, Example 15.5, Eqs. (15.28)–(15.33), pp. 516–517.

Modeling Disjunctions¶
When modeling disjunctions, we will have to represent logical constraints that involve continuous variables.
General Notation¶
A generalized disjunctive program combines an objective, global algebraic constraints, disjunctions, and logical constraints. One common form is
Here indexes disjunctions and indexes the alternatives in disjunction . The cost variable is assigned the given cost when term is selected. Each selected term enforces all its constraints together. Global rows hold regardless of the choice. The Boolean indicators are distinct from their algebraic binary representatives .
The displayed denotes inclusive OR. Our reactor and packing models additionally impose exactly one selected indicator per disjunction, which is Pyomo’s default. A false indicator removes that term’s requirements; it does not assert the negation of every inequality in the term. Thus more than one alternative’s algebraic inequalities can happen to hold even though only one indicator is selected.
Sources: Bynum et al. (2021), §11.1, Eqs. (11.13)–(11.17), p. 173; Pyomo’s modeling documentation. Pyomo exposes indicator_var as Boolean and binary_indicator_var as its associated binary. Use BooleanVar for logic and a Var(domain=Binary) for numeric binary algebra.
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.
A bounded linear disjunction has term sets
Selecting a term enforces all of its rows. With exactly-one selection, eliminating the indicators leaves the union . The same continuous union can also be represented by inclusive selection; overlapping terms can then have several true indicators.
Applied to the reactor problem:
The continuous feasible set is atm. We minimize to obtain a simple test objective. The analytic Big-M illustration below uses the declared common box . The canonical Pyomo builder uses the stronger valid bound . This difference affects automatic Big-M values and LP bounds; it does not change the original disjunctive feasible set.

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 add_pressure_windows(model):
# One exactly-one disjunction, with both pressure rows in each term.
# GDP creates the indicators; reformulation supplies the algebraic rows.
@model.Disjunction(
doc="Select exactly one reactor; each brings its own pressure window"
)
def pressure_bounds(b):
# Two inner lists: one simultaneous pressure window per reactor.
return [
[b.P <= b.max_pressure[r], b.P >= b.min_pressure[r]] for r in b.reactors
]
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).
# These bounds are what gdp.bigm reads to compute M per row -- here 20, 0, 0, 15.
# Automatic M estimation needs finite expression bounds; valid supplied M values
# are an alternative. Hull requires finite bounds on variables in its disjuncts.
model.P = Var(bounds=(5, 30), doc="Reactor pressure (atm)")
## Adding an objective for the example
@model.Objective()
def objective(b):
return b.P
add_pressure_windows(model)
return modelPyomo creates the Boolean and associated binary indicators when the disjuncts are declared, as the pre-transformation model printout shows. Reformulation then supplies the algebraic selection and constraint rows.
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¶
For each row in term , choose a nonnegative constant large enough that the row imposes no restriction on the relevant off-state domain:
When , the original row is enforced. When , a valid deactivates it. A conservative choice is
A common constant for all rows of a term can be the maximum of their row constants. Bounds or constraints known to hold in the other selected terms may yield smaller valid constants. State which off-state domain was used.
For the reactor, common constants give
Over the entire box , maximum row violations are , so deactivates every row over that box. These are the smallest common per-term constants for that deactivation policy. They are not the smallest constants that can preserve this particular union: when term 2 is off, term 1 supplies , so already suffices.
A too-small constant may remove feasible points. For example, wrongly caps when reactor 2 is selected, cutting out its valid pressures above 20. An unnecessarily large constant can weaken the LP relaxation and worsen scaling; it does not guarantee a particular iteration count. Neither problem size alone nor a blanket preference selects the better reformulation.
Source: BGW, §15.8, Eqs. (15.43)–(15.44), pp. 519–520. The two printing errors in (15.44) are corrected above: the fourth row starts with -20, and the selection row is .

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).
The linear hull construction disaggregates each continuous variable into one scaled copy per term. The copies express a convex combination of feasible points, giving the convex hull of one bounded linear disjunction when the indicators are relaxed. This avoids choosing Big-M constants, at the cost of additional variables and rows.
Convex Hull Relaxation Approach¶

This analytic illustration uses the manual box-wide constants, not the automatic transformation of the builder with P bounded below by 5. The hatched difference is entirely at the lower boundary.
For the nonempty bounded linear term sets defined above, introduce and impose
The bounds are signed: need not be zero. For example, a valid selected value would be lost if were incorrectly imposed. At , the scaled bounds force . The row alone need not do so; allows any nonnegative .
Why the relaxed formulation gives the hull. Replace binary domains by . For any positive weight , divide by that weight to obtain . The zero-weight copies vanish by the scaled bounds. Hence is a convex combination of points in the union. Conversely, combine any convex combination’s contributions from the same term, which is convex; assigning its total weight to and its weighted point to satisfies every row. This proves both inclusions.
With binary weights, exactly one copy remains and equals , proving exactness for the original union. The bounded linear assumptions matter: nonlinear hull reformulation needs additional theory. Also, intersecting separately convexified disjunctions with global constraints need not give the convex hull of the entire GDP feasible set.
Reactor hull:
Here each term already bounds its copy on both sides, forcing an off-copy to zero. With , , the relaxed pressure interval is
Every value in this interval is a sum of points in and . Thus these projected bounds are necessary and sufficient. Minimizing gives 5.
For the manual , model, the corresponding lower bound is and the upper bound is . The lower lines meet at , , whereas the hull’s lower bound there is . At , the respective intervals are and . The upper boundaries agree; the lower boundaries explain the gap.
Sources: BGW, Eqs. (15.45)–(15.46), pp. 520–521; Balas (1985). Pyomo’s hull transformation requires finite lower and upper bounds on the variables appearing in disjuncts, including when automatic Big-M estimation could instead be replaced by supplied valid constants.

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¶
Why does CNF allow one linear row per clause?
Does the separator implication require selecting any separator?
What changes when an indicator is zero in Big-M? Does it assert the opposite inequality?
Is hull always strictly stronger than a valid Big-M formulation?
Under what conditions can the zero-term hull be simplified?
Answers
Each clause is an OR of literals. Its sum of binary truth values must be at least one; an AND of clauses requires every such row.
No. The all-zero selection makes the antecedent false and satisfies the implication. Add a separate selection requirement if the process needs one.
A valid constant makes the term’s row nonrestrictive on the relevant off-state domain. The false indicator does not enforce the negation of that row.
No. It can be stronger, or the LP relaxations can agree. The following diagnostics show equality for this notebook’s automatically bounded reactor model.
The particular two-term on/off construction below eliminates the off-copy. This is not a theorem that every two-term GDP simplifies in the same way.
Check the model and its relaxations¶
The following calculation constructs fresh copies of the same builder used above, inspects Pyomo’s signed lower/upper M values, and solves their MIP and LP versions. It also checks a pressure cap of 19: the correct model must return 10, not 19. Fixing must be infeasible. Both checks detect an error that the minimum would miss.
For this builder’s box , automatic row magnitudes are . Substitution yields , the same projection as the hull. A zero M means the bound is already globally implied. This example therefore illustrates equality, rather than a strict automatic-Big-M/hull gap. The manual illustration deliberately uses a different deactivation policy.
import json
import pyomo.environ as pyo
from pyomo.gdp import Disjunction
import pyomo
def model_size(m):
variables = list(m.component_data_objects(pyo.Var, active=True))
rows = list(m.component_data_objects(pyo.Constraint, active=True))
return {"continuous_variables": sum(not v.is_binary() for v in variables),
"binary_variables": sum(v.is_binary() for v in variables),
"equalities": sum(c.equality for c in rows),
"inequalities": sum(not c.equality for c in rows)}
def reactor_check(transform, relax=False, cap=None, fixed_pressure=None):
m = create_model()
if cap is not None:
m.cap = pyo.Constraint(expr=m.P <= cap)
m.objective.sense = pyo.maximize
if fixed_pressure is not None:
m.P.fix(fixed_pressure)
reformulation = pyo.TransformationFactory(transform)
reformulation.apply_to(m)
size = model_size(m)
M_values = None
if transform == "gdp.bigm":
M_values = {c.name: list(pair) for c, pair in
reformulation.get_all_M_values_by_constraint(m).items()}
if relax:
pyo.TransformationFactory("core.relax_integer_vars").apply_to(m)
solver = pyo.SolverFactory(milp_solver)
solved = solver.solve(m, load_solutions=False)
termination = str(solved.solver.termination_condition)
pressure = None
if pyo.check_optimal_termination(solved):
m.solutions.load_from(solved)
pressure = pyo.value(m.P)
return {"transformation": transform, "LP": relax, "cap": cap,
"fixed_pressure": fixed_pressure, "termination": termination,
"pressure": pressure, "size_before_relaxing": size, "signed_M": M_values}
reactor_diagnostics = [reactor_check(t, relax=r)
for t in ("gdp.bigm", "gdp.hull") for r in (False, True)]
reactor_diagnostics += [reactor_check("gdp.bigm", cap=19),
reactor_check("gdp.bigm", fixed_pressure=15)]
assert all(abs(row["pressure"] - 5) < 1e-8 for row in reactor_diagnostics[:4])
assert abs(reactor_diagnostics[4]["pressure"] - 10) < 1e-8
assert reactor_diagnostics[5]["termination"] == "infeasible"
print("Pyomo", pyomo.__version__)
print(json.dumps(reactor_diagnostics, indent=2))Pyomo 6.10.1
[
{
"transformation": "gdp.bigm",
"LP": false,
"cap": null,
"fixed_pressure": null,
"termination": "optimal",
"pressure": 5.0,
"size_before_relaxing": {
"continuous_variables": 1,
"binary_variables": 2,
"equalities": 1,
"inequalities": 4
},
"signed_M": {
"pressure_bounds_disjuncts[0].constraint[1]": [
null,
20.0
],
"pressure_bounds_disjuncts[0].constraint[2]": [
0.0,
null
],
"pressure_bounds_disjuncts[1].constraint[1]": [
null,
0.0
],
"pressure_bounds_disjuncts[1].constraint[2]": [
-15.0,
null
]
}
},
{
"transformation": "gdp.bigm",
"LP": true,
"cap": null,
"fixed_pressure": null,
"termination": "optimal",
"pressure": 5.0,
"size_before_relaxing": {
"continuous_variables": 1,
"binary_variables": 2,
"equalities": 1,
"inequalities": 4
},
"signed_M": {
"pressure_bounds_disjuncts[0].constraint[1]": [
null,
20.0
],
"pressure_bounds_disjuncts[0].constraint[2]": [
0.0,
null
],
"pressure_bounds_disjuncts[1].constraint[1]": [
null,
0.0
],
"pressure_bounds_disjuncts[1].constraint[2]": [
-15.0,
null
]
}
},
{
"transformation": "gdp.hull",
"LP": false,
"cap": null,
"fixed_pressure": null,
"termination": "optimal",
"pressure": 5.0,
"size_before_relaxing": {
"continuous_variables": 3,
"binary_variables": 2,
"equalities": 2,
"inequalities": 8
},
"signed_M": null
},
{
"transformation": "gdp.hull",
"LP": true,
"cap": null,
"fixed_pressure": null,
"termination": "optimal",
"pressure": 5.0,
"size_before_relaxing": {
"continuous_variables": 3,
"binary_variables": 2,
"equalities": 2,
"inequalities": 8
},
"signed_M": null
},
{
"transformation": "gdp.bigm",
"LP": false,
"cap": 19,
"fixed_pressure": null,
"termination": "optimal",
"pressure": 10.0,
"size_before_relaxing": {
"continuous_variables": 1,
"binary_variables": 2,
"equalities": 1,
"inequalities": 5
},
"signed_M": {
"pressure_bounds_disjuncts[0].constraint[1]": [
null,
20.0
],
"pressure_bounds_disjuncts[0].constraint[2]": [
0.0,
null
],
"pressure_bounds_disjuncts[1].constraint[1]": [
null,
0.0
],
"pressure_bounds_disjuncts[1].constraint[2]": [
-15.0,
null
]
}
},
{
"transformation": "gdp.bigm",
"LP": false,
"cap": null,
"fixed_pressure": 15,
"termination": "infeasible",
"pressure": null,
"size_before_relaxing": {
"continuous_variables": 1,
"binary_variables": 2,
"equalities": 1,
"inequalities": 4
},
"signed_M": {
"pressure_bounds_disjuncts[0].constraint[1]": [
null,
20.0
],
"pressure_bounds_disjuncts[0].constraint[2]": [
0.0,
null
],
"pressure_bounds_disjuncts[1].constraint[1]": [
null,
0.0
],
"pressure_bounds_disjuncts[1].constraint[2]": [
-15.0,
null
]
}
}
]
A test that rejects the toothless indexed model¶
The incorrect formulation below creates two independent exactly-one choices between overlapping half-lines. At , each pair has a satisfied alternative. Even exclusive indicator selection cannot repair a wrongly grouped model: the unselected half-line is deactivated, rather than negated.
wrong = pyo.ConcreteModel()
wrong.P = pyo.Var(bounds=(5, 30))
@wrong.Disjunction([1, 2])
def pressure_bounds(b, r):
lo, hi = {1: (5, 10), 2: (20, 30)}[r]
return [b.P <= hi, b.P >= lo]
wrong.cap = pyo.Constraint(expr=wrong.P <= 19)
wrong.objective = pyo.Objective(expr=wrong.P, sense=pyo.maximize)
pyo.TransformationFactory("gdp.bigm").apply_to(wrong)
wrong_result = pyo.SolverFactory(milp_solver).solve(wrong)
assert pyo.check_optimal_termination(wrong_result)
assert abs(pyo.value(wrong.P) - 19) < 1e-8
wrong.P.fix(15)
wrong_result = pyo.SolverFactory(milp_solver).solve(wrong)
assert pyo.check_optimal_termination(wrong_result)
print("Incorrect indexed model: max P with P <= 19 is 19; P = 15 is feasible.")Incorrect indexed model: max P with P <= 19 is 19; P = 15 is feasible.
Reproduce the manual LP bounds¶
The following LPs use the same four reactor rows with explicit nonnegative row constants. Their continuous variables are , with and . Keeping the box and constant policy in the output makes the comparison reproducible.
For , row-specific box maxima improve the LP bound from 2.5 to 4. Using the other branch to justify common constants gives the hull projection and bound 5. The canonical automatic constants also give 5. These choices are all valid for integer selections; their fractional behavior differs.
from scipy.optimize import linprog
manual_lp_bounds = []
for label, lower, M in (
("box [0,30], common M=20", 0, (20,20,20,20)),
("box [0,30], row-specific box maxima", 0, (20,5,0,20)),
("box [0,30], other-branch common M", 0, (20,20,15,15)),
("box [5,30], automatic row magnitudes", 5, (20,0,0,15)),
):
u1,l1,u2,l2 = M
# Columns are (P,t): upper/lower reactor-1 rows, then reactor-2 rows.
A = [[1,u1],[-1,l1],[1,-u2],[-1,-l2]]
b = [10+u1,-5+l1,30,-20]
lp = linprog([1,0], A_ub=A, b_ub=b, bounds=[(lower,30),(0,1)], method="highs")
assert lp.success
manual_lp_bounds.append({"case": label, "row_M": list(M),
"minimum_P": float(lp.fun), "t": float(lp.x[1])})
assert all(abs(row["minimum_P"]-expected)<1e-8
for row,expected in zip(manual_lp_bounds,(2.5,4,5,5)))
print(json.dumps(manual_lp_bounds, indent=2))[
{
"case": "box [0,30], common M=20",
"row_M": [
20,
20,
20,
20
],
"minimum_P": 2.5,
"t": 0.875
},
{
"case": "box [0,30], row-specific box maxima",
"row_M": [
20,
5,
0,
20
],
"minimum_P": 4.0,
"t": 0.8
},
{
"case": "box [0,30], other-branch common M",
"row_M": [
20,
20,
15,
15
],
"minimum_P": 5.0,
"t": 1.0
},
{
"case": "box [5,30], automatic row magnitudes",
"row_M": [
20,
0,
0,
15
],
"minimum_P": 5.0,
"t": 1.0
}
]
Formulation size and choice¶
For one disjunction with terms, continuous components, and inequalities in term , count the algebraic core as follows. These are numbers of declared components, not a Jacobian rank or degrees-of-freedom calculation.
| Component | Common-per-term Big-M | Linear hull |
|---|---|---|
| Continuous variables | ||
| Binary indicators | ||
| Equalities | 1 selection row | 1 selection row and linking rows |
| Term inequalities |
This table excludes domain bounds and scaled-copy bound rows. The explicit general hull also has scaled bound inequalities, although some can be redundant or subsumed by term rows. Global constraints add to both formulations. Pyomo can eliminate or reuse components, so compare the actual counts printed above under a stated convention rather than asserting they must equal the simplified table.
Tighter relaxations can improve lower bounds for minimization, while larger models cost more per node. Bounds, formulation structure, presolve, objective, and solver all matter. Compare actual relaxations and solve evidence; “Big-M for small problems, hull for large problems” is not a valid selection rule. This notebook reports optimality and mathematical bounds rather than treating an iteration count as a universal performance result.
Source: BGW, §15.8, p. 521. Nonlinear convex hull theory is treated by Grossmann & Lee (2003); it does not follow from the bounded linear proof above.
A two-term on/off simplification¶
Consider or . The hull copies satisfy and , so eliminate both copies to obtain
At these are the original on-term rows; at the scaled bounds force zero. Relaxing represents convex combinations with the zero point by the same proof as above. The signed bounds are essential in general. Merely having two alternatives, without an explicit zero off-term, does not justify this elimination. Source: BGW, Eqs. (15.47)–(15.48), p. 521.
Cost assignment and logical scope¶
When exactly one indicator is selected, the disjunctive assignments are equivalently expressed by . The equality takes the selected alternative’s cost. With several selected alternatives this expression instead adds their costs, so it must not be substituted into an inclusive-cost model without checking the intended semantics.
A separate complementarity warning concerns NLP solution methods. Biegler (2010), §11.3, p. 337, Eq. (11.18), gives : the product condition and the nonnegativity bounds leave only or . The book warns that this disconnected feasible set can cause convergence difficulties and advises against using such MPCC constructions for exclusive choices. This is a warning about formulation and solution behavior, not a proof that complementarity cannot represent those two points. Inclusive alternatives can sometimes be modeled successfully with MPCCs, but the construction and its assumptions are problem dependent. The present lecture uses explicit binary/GDP selection instead. See MINLP Algorithms for the later discrete-continuous algorithm material.
Further reading and applications¶
The CNF procedure here follows BGW §15.7, pp. 515–516. BGW attribute its formalization to Clocksin & Mellish (1981). Raman & Grossmann (1991) connect logical inference with MILP process-synthesis modeling; the 1993 paper by Raman & Grossmann (1993) develops symbolic integration of logic, including CNF/DNF representations, in mixed-integer methods. These historical notes explain the relationship between the logic and algebraic models; they do not add another conversion algorithm to learn.
Start with Balas (1985) for disjunctive relaxations, Raman & Grossmann (1994) for the logic-based GDP framework, and Grossmann & Ruiz (2012) for formulations and alternative MINLP algorithms. Grossmann (2002) and Trespalacios & Grossmann (2014) are broader reviews; Kronqvist et al. (2026) give a historical overview. Grossmann & Trespalacios (2013) explain systematic discrete-continuous modeling, and Chen & Grossmann (2019) discuss model variants and hierarchy. Perez & Grossmann (2024) extend GDP to hierarchical structures and first-order logic. These are extension readings, not prerequisites for the bounded linear hull proof.
Representative applications include power/desalination superstructures (Mussati et al. (2008)), thermally linked distillation (Caballero & Grossmann (2001)), production scheduling (Castro & Grossmann (2012)), maintenance scheduling (Castro et al. (2014)), solvent-mixture design (Jonuzaj et al. (2016)), modular process synthesis (Chen & Grossmann (2019)), and discrete-continuous energy-system model predictive control (Bhattacharya et al. (2021)).
Nonlinear solution methods are developed by Lee & Grossmann (2005); Türkay & Grossmann (1996) treat discontinuous investment costs and multiple size regions. The current Pyomo modeling ecosystem is described by Chen et al. (2022).
Textbooks and software¶
Biegler, Grossmann, and Westerberg (1997), Systematic Methods of Chemical Process Design, Chapter 15, §§15.7–15.8, pp. 514–521. Bynum et al. (2021), Pyomo—Optimization Modeling in Python, 3rd ed., Chapter 11, pp. 173–179, DOI. Biegler (2010), Nonlinear Programming: Concepts, Algorithms, and Applications, §11.3, p. 337 (complementarity warning). Postek et al. (2025), Hands-On Mathematical Optimization with Python, §3.4: Production model using disjunctions. Current Pyomo.GDP modeling and reformulation documentation supplement the book’s syntax; diagnostics above ran with Pyomo 6.10.1.
- Balas, E. (1985). Disjunctive Programming and a Hierarchy of Relaxations for Discrete Optimization Problems. SIAM Journal on Algebraic and Discrete Methods, 6(3), 466–486. 10.1137/0606047
- Grossmann, I. E., & Lee, S. (2003). Generalized Convex Disjunctive Programming: Nonlinear Convex Hull Relaxation. Computational Optimization and Applications, 26(1), 83–100. 10.1023/A:1025154322278
- Clocksin, W. F., & Mellish, C. S. (1981). Programming in Prolog. Springer-Verlag.
- Raman, R., & Grossmann, I. E. (1991). Relation between MILP Modelling and Logical Inference for Chemical Process Synthesis. Computers & Chemical Engineering, 15(2), 73–84. 10.1016/0098-1354(91)87007-V
- Raman, R., & Grossmann, I. E. (1993). Symbolic Integration of Logic in Mixed-Integer Linear Programming Techniques for Process Synthesis. Computers & Chemical Engineering, 17(9), 909–927. 10.1016/0098-1354(93)80073-V
- Raman, R., & Grossmann, I. E. (1994). Modelling and Computational Techniques for Logic Based Integer Programming. Computers & Chemical Engineering, 18(7), 563–578. 10.1016/0098-1354(93)E0010-7
- Grossmann, I. E., & Ruiz, J. P. (2012). Generalized Disjunctive Programming: A Framework for Formulation and Alternative Algorithms for MINLP Optimization. In J. Lee & S. Leyffer (Eds.), Mixed Integer Nonlinear Programming (Vol. 154, pp. 93–115). Springer. 10.1007/978-1-4614-1927-3_4
- Grossmann, I. E. (2002). Review of Nonlinear Mixed-Integer and Disjunctive Programming Techniques. Optimization and Engineering, 3, 227–252. 10.1023/A:1021039126272
- Trespalacios, F., & Grossmann, I. E. (2014). Review of Mixed-Integer Nonlinear and Generalized Disjunctive Programming Methods. Chemie Ingenieur Technik, 86(7), 991–1012. 10.1002/cite.201400037
- Kronqvist, J., Neira, D. E. B., & Grossmann, I. E. (2026). 50 Years of Mixed-Integer Nonlinear and Disjunctive Programming. European Journal of Operational Research, 331, 687–705. 10.1016/j.ejor.2025.07.016
- Grossmann, I. E., & Trespalacios, F. (2013). Systematic Modeling of Discrete-Continuous Optimization Models through Generalized Disjunctive Programming. AIChE Journal, 59(9), 3276–3295. 10.1002/aic.14088
- Chen, Q., & Grossmann, I. E. (2019). Modern Modeling Paradigms Using Generalized Disjunctive Programming. Processes, 7(11), 839. 10.3390/pr7110839
- Perez, H. D., & Grossmann, I. E. (2024). Extensions to Generalized Disjunctive Programming: Hierarchical Structures and First-Order Logic. Optimization and Engineering, 25(2), 959–998. 10.1007/s11081-023-09831-x
- Mussati, S. F., Barttfeld, M., Aguirre, P. A., & Scenna, N. J. (2008). A Disjunctive Programming Model for Superstructure Optimization of Power and Desalting Plants. Desalination, 222, 457–465. 10.1016/j.desal.2007.01.162
- Caballero, J. A., & Grossmann, I. E. (2001). Generalized Disjunctive Programming Model for the Optimal Synthesis of Thermally Linked Distillation Columns. Industrial & Engineering Chemistry Research, 40(10), 2260–2274. 10.1021/ie000761a