0
# Generalized Disjunctive Programming
1
2
Components for modeling logical relationships and disjunctive constraints in optimization problems. GDP enables the formulation of problems with logical decision-making and alternative process paths through disjuncts and disjunctions.
3
4
## Capabilities
5
6
### Disjunct Components
7
8
Components for defining disjunctive blocks containing variables, constraints, and logical relationships.
9
10
```python { .api }
11
class Disjunct:
12
"""
13
Disjunct component for GDP modeling.
14
15
A disjunct represents a logical block that is either active or inactive.
16
When active, all constraints within the disjunct must be satisfied.
17
"""
18
def __init__(self, *args, **kwargs): ...
19
20
def activate(self):
21
"""Activate this disjunct."""
22
23
def deactivate(self):
24
"""Deactivate this disjunct."""
25
26
def is_active(self):
27
"""Check if disjunct is active."""
28
29
@property
30
def indicator_var(self):
31
"""Get the binary indicator variable for this disjunct."""
32
33
class DisjunctData:
34
"""
35
Data container for disjunct components.
36
37
Stores the actual constraint and variable data associated with
38
a specific disjunct instance.
39
"""
40
def __init__(self, component=None): ...
41
42
def activate(self): ...
43
def deactivate(self): ...
44
def is_active(self): ...
45
```
46
47
### Disjunction Components
48
49
Components for defining logical disjunctions (OR relationships) between disjuncts.
50
51
```python { .api }
52
class Disjunction:
53
"""
54
Disjunction component representing logical OR relationships.
55
56
A disjunction requires that exactly one of its constituent
57
disjuncts must be active (true).
58
"""
59
def __init__(self, *args, **kwargs): ...
60
61
def activate(self):
62
"""Activate this disjunction."""
63
64
def deactivate(self):
65
"""Deactivate this disjunction."""
66
67
def is_active(self):
68
"""Check if disjunction is active."""
69
70
class DisjunctionData:
71
"""
72
Data container for disjunction components.
73
74
Stores the disjunct references and logical relationships
75
for a specific disjunction instance.
76
"""
77
def __init__(self, component=None): ...
78
79
def activate(self): ...
80
def deactivate(self): ...
81
def is_active(self): ...
82
```
83
84
### GDP Error Handling
85
86
Exception class for GDP-specific errors and validation issues.
87
88
```python { .api }
89
class GDP_Error(Exception):
90
"""GDP-specific error class for logical modeling issues."""
91
def __init__(self, message): ...
92
```
93
94
## Usage Examples
95
96
### Basic Disjunctive Model
97
98
```python
99
from pyomo.environ import *
100
from pyomo.gdp import Disjunct, Disjunction
101
102
model = ConcreteModel()
103
104
# Decision variables
105
model.x = Var(bounds=(0, 10))
106
model.y = Var(bounds=(0, 10))
107
108
# Create disjuncts for alternative process paths
109
model.path1 = Disjunct()
110
model.path2 = Disjunct()
111
112
# Path 1: High-temperature, low-pressure process
113
model.path1.temp_constraint = Constraint(expr=model.x >= 8)
114
model.path1.pressure_constraint = Constraint(expr=model.y <= 3)
115
model.path1.cost = Var(bounds=(0, 100))
116
model.path1.cost_def = Constraint(expr=model.path1.cost == 2*model.x + model.y)
117
118
# Path 2: Low-temperature, high-pressure process
119
model.path2.temp_constraint = Constraint(expr=model.x <= 5)
120
model.path2.pressure_constraint = Constraint(expr=model.y >= 6)
121
model.path2.cost = Var(bounds=(0, 100))
122
model.path2.cost_def = Constraint(expr=model.path2.cost == model.x + 3*model.y)
123
124
# Disjunction: exactly one path must be chosen
125
model.path_choice = Disjunction(expr=[model.path1, model.path2])
126
127
# Objective: minimize total cost
128
model.obj = Objective(
129
expr=model.path1.cost + model.path2.cost,
130
sense=minimize
131
)
132
```
133
134
### Indexed Disjuncts and Disjunctions
135
136
```python
137
from pyomo.environ import *
138
from pyomo.gdp import Disjunct, Disjunction
139
140
model = ConcreteModel()
141
142
# Sets for indexing
143
model.UNITS = Set(initialize=[1, 2, 3])
144
model.MODES = Set(initialize=['low', 'med', 'high'])
145
146
# Variables
147
model.flow = Var(model.UNITS, bounds=(0, 100))
148
model.operating = Var(model.UNITS, domain=Binary)
149
150
# Indexed disjuncts for operating modes
151
model.mode_disjunct = Disjunct(model.UNITS, model.MODES)
152
153
# Define constraints for each mode
154
for unit in model.UNITS:
155
# Low mode: 0-30% capacity
156
model.mode_disjunct[unit, 'low'].flow_lower = Constraint(
157
expr=model.flow[unit] >= 0
158
)
159
model.mode_disjunct[unit, 'low'].flow_upper = Constraint(
160
expr=model.flow[unit] <= 30
161
)
162
163
# Medium mode: 30-70% capacity
164
model.mode_disjunct[unit, 'med'].flow_lower = Constraint(
165
expr=model.flow[unit] >= 30
166
)
167
model.mode_disjunct[unit, 'med'].flow_upper = Constraint(
168
expr=model.flow[unit] <= 70
169
)
170
171
# High mode: 70-100% capacity
172
model.mode_disjunct[unit, 'high'].flow_lower = Constraint(
173
expr=model.flow[unit] >= 70
174
)
175
model.mode_disjunct[unit, 'high'].flow_upper = Constraint(
176
expr=model.flow[unit] <= 100
177
)
178
179
# Each unit must operate in exactly one mode
180
def mode_disjunction_rule(model, unit):
181
return [model.mode_disjunct[unit, mode] for mode in model.MODES]
182
183
model.mode_choice = Disjunction(model.UNITS, rule=mode_disjunction_rule)
184
```
185
186
### Hierarchical Disjunctions
187
188
```python
189
from pyomo.environ import *
190
from pyomo.gdp import Disjunct, Disjunction
191
192
model = ConcreteModel()
193
194
# Variables
195
model.x = Var(bounds=(0, 20))
196
model.y = Var(bounds=(0, 20))
197
198
# Top-level choice: Process A or Process B
199
model.process_A = Disjunct()
200
model.process_B = Disjunct()
201
202
# Within Process A: two sub-options
203
model.process_A.option_A1 = Disjunct()
204
model.process_A.option_A2 = Disjunct()
205
206
# Process A, Option 1
207
model.process_A.option_A1.con1 = Constraint(expr=model.x <= 8)
208
model.process_A.option_A1.con2 = Constraint(expr=model.y >= 5)
209
210
# Process A, Option 2
211
model.process_A.option_A2.con1 = Constraint(expr=model.x >= 12)
212
model.process_A.option_A2.con2 = Constraint(expr=model.y <= 10)
213
214
# Process A must choose one sub-option
215
model.process_A.sub_choice = Disjunction(
216
expr=[model.process_A.option_A1, model.process_A.option_A2]
217
)
218
219
# Process B (single option)
220
model.process_B.con1 = Constraint(expr=model.x + model.y <= 15)
221
model.process_B.con2 = Constraint(expr=model.x - model.y >= 2)
222
223
# Top-level disjunction
224
model.main_choice = Disjunction(expr=[model.process_A, model.process_B])
225
226
# Objective
227
model.obj = Objective(expr=model.x + model.y, sense=maximize)
228
```
229
230
### Solving GDP Models
231
232
```python
233
from pyomo.environ import *
234
from pyomo.gdp import Disjunct, Disjunction
235
import pyomo.contrib.gdpopt as gdpopt
236
237
# Create GDP model (using previous example)
238
model = create_gdp_model() # Your GDP model creation function
239
240
# Method 1: Use GDP-specific solver
241
solver = SolverFactory('gdpopt')
242
results = solver.solve(model, tee=True)
243
244
# Method 2: Transform to MILP and solve
245
from pyomo.gdp import TransformationFactory
246
247
# Big-M transformation
248
bigm = TransformationFactory('gdp.bigm')
249
bigm.apply_to(model)
250
251
# Solve transformed model
252
milp_solver = SolverFactory('gurobi') # or 'cplex', 'glpk', etc.
253
results = milp_solver.solve(model, tee=True)
254
255
# Method 3: Hull reformulation
256
model_hull = model.clone()
257
hull = TransformationFactory('gdp.hull')
258
hull.apply_to(model_hull)
259
results = milp_solver.solve(model_hull, tee=True)
260
```
261
262
### Accessing Solution Information
263
264
```python
265
from pyomo.environ import *
266
from pyomo.gdp import Disjunct, Disjunction
267
268
# After solving GDP model
269
if results.solver.termination_condition == TerminationCondition.optimal:
270
print("Solution found!")
271
272
# Check which disjuncts are active
273
for disjunct in [model.path1, model.path2]:
274
if disjunct.indicator_var.value >= 0.5: # Binary variable ≈ 1
275
print(f"{disjunct.name} is active")
276
277
# Access variables within active disjunct
278
if hasattr(disjunct, 'cost'):
279
print(f" Cost: {value(disjunct.cost)}")
280
281
# Access main variables
282
print(f"x = {value(model.x)}")
283
print(f"y = {value(model.y)}")
284
print(f"Objective = {value(model.obj)}")
285
```
286
287
### Conditional Constraints with GDP
288
289
```python
290
from pyomo.environ import *
291
from pyomo.gdp import Disjunct, Disjunction
292
293
model = ConcreteModel()
294
295
# Variables
296
model.x = Var(bounds=(0, 100))
297
model.setup_cost = Var(bounds=(0, 1000))
298
model.operating_cost = Var(bounds=(0, 500))
299
300
# Disjunct for "facility not built"
301
model.not_built = Disjunct()
302
model.not_built.no_production = Constraint(expr=model.x == 0)
303
model.not_built.no_setup = Constraint(expr=model.setup_cost == 0)
304
model.not_built.no_operating = Constraint(expr=model.operating_cost == 0)
305
306
# Disjunct for "facility built"
307
model.built = Disjunct()
308
model.built.min_production = Constraint(expr=model.x >= 20) # Minimum viable scale
309
model.built.setup_cost_def = Constraint(expr=model.setup_cost == 200)
310
model.built.operating_cost_def = Constraint(expr=model.operating_cost == 5 * model.x)
311
312
# Either build or don't build
313
model.build_decision = Disjunction(expr=[model.not_built, model.built])
314
315
# Objective: maximize profit (revenue - costs)
316
model.revenue = Expression(expr=10 * model.x)
317
model.total_cost = Expression(expr=model.setup_cost + model.operating_cost)
318
model.obj = Objective(expr=model.revenue - model.total_cost, sense=maximize)
319
```