Blocks and Model Transformations
This notebook is part of the SECQUOIA Research Group Pyomo Tutorial. It adapts the structured-modeling and transformation themes from the public Pyomo workshop tutorials into short exercises. Install GLPK before running the mixed-integer examples locally; see Setup and Solvers for solver notes.
Learning objectives¶
By the end of this chapter, you should be able to:
use
Blockobjects to organize variables, expressions, and constraints by subsystem;write block rules that create repeatable model structure;
inspect a structured model without flattening away the hierarchy;
explain the difference between a model and a solver-ready formulation;
apply a Pyomo transformation and compare the transformed formulation to the original model.
import pyomo.environ as pyo
def solve_with_glpk(model):
solver = pyo.SolverFactory("glpk")
if not solver.available(False):
print("GLPK is not available; the model was built but not solved.")
return None
results = solver.solve(model)
print(f"termination: {results.solver.termination_condition}")
return results
1. Structured modeling with Blocks¶
Large algebraic models usually have repeated structure: a plant has units, a network has arcs, a scenario model has one block per scenario, and a decomposition algorithm often needs one block per subproblem. A Block lets you keep that structure in the Pyomo object model instead of encoding every component at the top level.
Exercise 1.1. Build a product block¶
The example below creates one block per product. Each block owns its local production variable, setup decision, parameters, expression, and capacity-linking constraint. The top-level model only contains the shared labor limit and objective.
def build_product_model():
products = ["standard", "premium"]
margin = {"standard": 8, "premium": 14}
labor = {"standard": 2, "premium": 5}
capacity = {"standard": 40, "premium": 25}
setup = {"standard": 20, "premium": 30}
model = pyo.ConcreteModel()
model.PRODUCTS = pyo.Set(initialize=products)
def product_block(block, product):
block.make = pyo.Var(bounds=(0, capacity[product]))
block.open = pyo.Var(within=pyo.Binary)
block.margin = pyo.Param(initialize=margin[product])
block.labor = pyo.Param(initialize=labor[product])
block.capacity = pyo.Param(initialize=capacity[product])
block.setup = pyo.Param(initialize=setup[product])
block.revenue = pyo.Expression(expr=block.margin * block.make)
block.setup_cost = pyo.Expression(expr=block.setup * block.open)
block.capacity_link = pyo.Constraint(expr=block.make <= block.capacity * block.open)
model.product = pyo.Block(model.PRODUCTS, rule=product_block)
model.labor_limit = pyo.Constraint(
expr=sum(model.product[p].labor * model.product[p].make for p in model.PRODUCTS) <= 100
)
model.profit = pyo.Objective(
expr=sum(model.product[p].revenue - model.product[p].setup_cost for p in model.PRODUCTS),
sense=pyo.maximize,
)
return model
structured_model = build_product_model()
solve_with_glpk(structured_model)
for product in structured_model.PRODUCTS:
block = structured_model.product[product]
print(
f"{product:8s}: make={pyo.value(block.make):5.1f}, "
f"open={pyo.value(block.open):3.0f}, revenue={pyo.value(block.revenue):6.1f}"
)
print(f"profit: {pyo.value(structured_model.profit):.1f}")
termination: optimal
standard: make= 40.0, open= 1, revenue= 320.0
premium : make= 4.0, open= 1, revenue= 56.0
profit: 326.0
Exercise 1.2. Inspect the model hierarchy¶
A block-structured model is still a normal Pyomo model. You can traverse it as a tree when that is useful, or ask Pyomo for all active variables and constraints when a solver or analysis routine needs the flattened view.
print("Local variables by product block:")
for product in structured_model.PRODUCTS:
block = structured_model.product[product]
local_vars = [var.local_name for var in block.component_objects(pyo.Var)]
local_cons = [con.local_name for con in block.component_objects(pyo.Constraint)]
print(f" {block.name}: vars={local_vars}, constraints={local_cons}")
flat_vars = list(structured_model.component_data_objects(pyo.Var, active=True))
flat_cons = list(structured_model.component_data_objects(pyo.Constraint, active=True))
print(f"flattened variable count: {len(flat_vars)}")
print(f"flattened constraint count: {len(flat_cons)}")
Local variables by product block:
product[standard]: vars=['make', 'open'], constraints=['capacity_link']
product[premium]: vars=['make', 'open'], constraints=['capacity_link']
flattened variable count: 4
flattened constraint count: 3
Exercise 1.3. Add a shared constraint outside the blocks¶
Blocks do not prevent coupling. The labor constraint above couples the product blocks through the top-level model. Add a second shared constraint that limits total setup decisions to one product, then resolve. How does the production plan change?
one_setup_model = build_product_model()
one_setup_model.one_setup = pyo.Constraint(
expr=sum(one_setup_model.product[p].open for p in one_setup_model.PRODUCTS) <= 1
)
solve_with_glpk(one_setup_model)
for product in one_setup_model.PRODUCTS:
block = one_setup_model.product[product]
print(f"{product:8s}: make={pyo.value(block.make):5.1f}, open={pyo.value(block.open):3.0f}")
print(f"profit: {pyo.value(one_setup_model.profit):.1f}")
termination: optimal
standard: make= 40.0, open= 1
premium : make= 0.0, open= 0
profit: 300.0
2. Model transformations¶
A model is the representation you want to write and maintain. A formulation is the algebraic form handed to a solver. Pyomo transformations rewrite one model representation into another while keeping the original modeling intent explicit.
Exercise 2.1. Relax integrality¶
The next cell relaxes the binary setup variables to continuous variables between 0 and 1. This is not the same problem as the mixed-integer model, but it is a useful diagnostic formulation: the relaxed objective is an upper bound for the maximization model.
relaxed_model = build_product_model()
pyo.TransformationFactory("core.relax_integer_vars").apply_to(relaxed_model)
solve_with_glpk(relaxed_model)
for product in relaxed_model.PRODUCTS:
block = relaxed_model.product[product]
print(
f"{product:8s}: make={pyo.value(block.make):5.1f}, "
f"open={pyo.value(block.open):5.2f}, open_bounds={block.open.bounds}"
)
print(f"relaxed profit: {pyo.value(relaxed_model.profit):.1f}")
termination: optimal
standard: make= 40.0, open= 1.00, open_bounds=(0, 1)
premium : make= 4.0, open= 0.16, open_bounds=(0, 1)
relaxed profit: 351.2
Exercise 2.2. Compare model and formulation counts¶
The transformed model keeps the same block hierarchy, but the setup variables no longer have a binary domain. Use the counts and the solution values to explain why the relaxed formulation is easier for a linear solver and why its solution may not be implementable.
for name, model in [("mixed-integer", structured_model), ("relaxed", relaxed_model)]:
binary_vars = [var for var in model.component_data_objects(pyo.Var) if var.is_binary()]
print(
f"{name:13s}: vars={len(list(model.component_data_objects(pyo.Var))):2d}, "
f"constraints={len(list(model.component_data_objects(pyo.Constraint, active=True))):2d}, "
f"binary_vars={len(binary_vars)}"
)
mixed-integer: vars= 4, constraints= 3, binary_vars=2
relaxed : vars= 4, constraints= 3, binary_vars=0
Exercises to continue¶
Rebuild the lot-sizing model from Pyomo Fundamentals with one block per time period.
Add a block-level expression for each period’s holding and backlog cost.
Relax the period setup variables, solve the relaxed formulation, and compare the result to the original mixed-integer model.
References¶
The Pyomo Block and transformation patterns used here are standard Pyomo modeling tools Bynum et al., 2021.
- Bynum, M. L., Hackebeil, G. A., Hart, W. E., Laird, C. D., Nicholson, B. L., Siirola, J. D., Watson, J.-P., & Woodruff, D. L. (2021). Pyomo–optimization modeling in python (Third, Vol. 67). Springer Science & Business Media.