Optimization Constraints¶
By default, optimex minimizes the environmental impact of a specified category while meeting the time-specific demand \(\mathbf{f}_t\) (see Theory: Complete Formulation). Real-world optimization problems often require additional constraints to reflect practical limitations, policy requirements, or scenario-specific conditions — such as existing brownfield capacities, resource bottlenecks, or cumulative environmental budgets.
This page documents all available constraint options in optimex.
Overview¶
Constraints in optimex are specified through the OptimizationModelInputs object. After creating your model inputs from the LCA processor, you can add constraints before creating the optimization model:
from optimex import converter, optimizer
# Parse LCA data
manager = converter.ModelInputManager()
model_inputs = manager.parse_from_lca_processor(lca_data_processor)
# Add constraints (examples below)
model_inputs.cumulative_process_limits_max = {"ProcessA": 100.0}
model_inputs.category_impact_limits = {("climate_change", 2030): 1000.0}
# Create and solve the model
model = optimizer.create_model(model_inputs, name="my_model", objective_category="climate_change")
solved_model, objective, results = optimizer.solve_model(model)
Process Deployment Limits¶
Control how many units of a process can be installed.
Limits are in process units
Deployment limits bound var_installation, which counts process units. One unit delivers the production of its production exchange over its whole lifetime, so a limit of 50 allows a lifetime output of 50 reference-flow units, not an annual output of 50. See Foreground Modeling.
Time-Specific Deployment Limits¶
Limit the capacity installed for a specific process at a specific year.
| Field | Type | Description |
|---|---|---|
process_deployment_limits_max |
Dict[Tuple[str, int], float] |
Upper bound on (process, year) installation |
process_deployment_limits_min |
Dict[Tuple[str, int], float] |
Lower bound on (process, year) installation |
Example: Limit new solar capacity to 50 MW in 2025
model_inputs.process_deployment_limits_max = {
("solar_pv", 2025): 50.0,
("solar_pv", 2026): 75.0,
("solar_pv", 2027): 100.0,
}
Example: Require at least 10 MW of wind installation in 2030
Cumulative Deployment Limits¶
Limit the total capacity installed for a process across all time periods.
| Field | Type | Description |
|---|---|---|
cumulative_process_limits_max |
Dict[str, float] |
Maximum total installation for a process |
cumulative_process_limits_min |
Dict[str, float] |
Minimum total installation for a process |
Example: Cap total nuclear capacity at 500 MW
Example: Require at least 200 MW total renewable capacity
Process Operation Limits¶
Control how much a process can operate in each time period.
| Field | Type | Description |
|---|---|---|
process_operation_limits_max |
Dict[Tuple[str, int], float] |
Upper bound on (process, year) operation |
process_operation_limits_min |
Dict[Tuple[str, int], float] |
Lower bound on (process, year) operation |
Operation vs Installation
Operation limits constrain how much of the installed capacity is actually used in each period. A process can only operate up to its installed capacity, but operation limits can further restrict this.
Both variables are counted in process units: var_operation[p, v, t] is how many units of vintage v run in year t, bounded by var_installation[p, v]. An operation limit therefore caps the units running in that year, summed over all vintages — multiply by the process's output per unit and year to express it as a production limit (or use flow limits to bound production directly).
Example: Limit coal plant operation during phase-out
model_inputs.process_operation_limits_max = {
("coal_plant", 2025): 80.0, # Max 80% of capacity
("coal_plant", 2030): 50.0, # Max 50% of capacity
("coal_plant", 2035): 0.0, # Complete phase-out
}
Example: Ensure minimum baseload operation
model_inputs.process_operation_limits_min = {
("nuclear_plant", 2025): 50.0, # Minimum 50 MW operation
("nuclear_plant", 2026): 50.0,
}
Category Impact Limits¶
Constrain the environmental impact in specific categories, either at specific times or cumulatively.
Time-Specific Impact Limits¶
Limit the impact in a category at a specific year.
| Field | Type | Description |
|---|---|---|
category_impact_limits |
Dict[Tuple[str, int], float] |
Upper bound on (category, year) impact |
Example: Annual carbon budget
model_inputs.category_impact_limits = {
("climate_change", 2025): 1000000.0, # 1 Mt CO2-eq in 2025
("climate_change", 2030): 500000.0, # 0.5 Mt CO2-eq in 2030
("climate_change", 2035): 100000.0, # 0.1 Mt CO2-eq in 2035
}
Example: Limit land use in specific years
model_inputs.category_impact_limits = {
("land_use", 2025): 5000.0, # Max 5000 m² in 2025
("land_use", 2030): 4000.0, # Max 4000 m² in 2030
}
Cumulative Impact Limits¶
Limit the total impact across all time periods.
| Field | Type | Description |
|---|---|---|
cumulative_category_impact_limits |
Dict[str, float] |
Upper bound on total impact in a category |
Example: Total carbon budget
model_inputs.cumulative_category_impact_limits = {
"climate_change": 5000000.0, # 5 Mt CO2-eq total budget
}
Example: Multi-category constraints
model_inputs.cumulative_category_impact_limits = {
"climate_change": 5000000.0,
"water_use": 1000000.0,
"land_use": 50000.0,
}
Combining Time-Specific and Cumulative Limits
You can use both types of limits simultaneously. For example, set annual carbon budgets with category_impact_limits while also enforcing a total budget with cumulative_category_impact_limits.
Flow Limits¶
Constrain specific material or energy flows in the system.
Time-Specific Flow Limits¶
Limit flows at specific times. Flows can be products, intermediate flows, or elementary flows.
| Field | Type | Description |
|---|---|---|
flow_limits_max |
Dict[Tuple[str, int], float] |
Upper bound on (flow, year) |
flow_limits_min |
Dict[Tuple[str, int], float] |
Lower bound on (flow, year) |
Example: Limit natural gas consumption
model_inputs.flow_limits_max = {
("natural_gas", 2025): 1000.0, # Max 1000 units in 2025
("natural_gas", 2030): 500.0, # Max 500 units in 2030
}
Example: Require minimum hydrogen production
Example: Limit CO2 emissions per year
Cumulative Flow Limits¶
Limit the total flow across all time periods.
| Field | Type | Description |
|---|---|---|
cumulative_flow_limits_max |
Dict[str, float] |
Upper bound on total flow |
cumulative_flow_limits_min |
Dict[str, float] |
Lower bound on total flow |
Example: Total resource budget
model_inputs.cumulative_flow_limits_max = {
"rare_earth_elements": 10000.0, # Limited total availability
}
Constraining an elementary flow
Elementary flows without a characterization factor in any of your impact
categories are dropped during LCA processing, since they cannot affect any
impact. To constrain such a flow, list it in
LCAConfig.background_inventory.retain_flows. See
Background Inventory.
Setting the limit through ModelInputManager.override() raises if the flow is
not in the model. Assigning it to an existing OptimizationModelInputs
skips that check, so create_model() logs a warning instead — for flows it
has to ignore, for years outside the horizon, and for flows no process
exchanges, whose limit could never bind.
Process Coupling¶
Link the deployment of related processes with fixed ratios.
| Field | Type | Description |
|---|---|---|
process_coupling |
Dict[Tuple[str, str], float] |
Ratio constraint: process1 = ratio * process2 |
Example: Coupled storage and generation
# For every 4 MW of solar, require 1 MW of battery storage
model_inputs.process_coupling = {
("battery_storage", "solar_pv"): 0.25, # storage = 0.25 * solar
}
Example: Backup capacity requirements
# Gas backup must be at least 50% of wind capacity
model_inputs.process_coupling = {
("gas_peaker", "wind_turbine"): 0.5,
}
Coupling Direction
The coupling constraint enforces installation[process1] = ratio * installation[process2]. Make sure to set up the ratio in the correct direction.
Existing Capacity (Brownfield)¶
Model systems with pre-existing infrastructure that was installed before the optimization horizon.
| Field | Type | Description |
|---|---|---|
existing_capacity |
Dict[Tuple[str, int], float] |
Units at (process, installation_year) |
Existing capacity is given in the same process units as var_installation, and the entries behave like vintages of that installation year: they can run while their lifecycle stage falls inside the operation window, and they carry no installation impacts.
Brownfield vs Greenfield
- Greenfield: Optimization starts from scratch with no existing capacity
- Brownfield: Some capacity already exists from previous investments
Example: Existing power plant fleet
model_inputs.existing_capacity = {
("coal_plant", 2010): 500.0, # 500 MW installed in 2010
("gas_plant", 2015): 300.0, # 300 MW installed in 2015
("nuclear_plant", 2005): 1000.0, # 1000 MW installed in 2005
}
Key characteristics of existing capacity:
-
Installation year must be before the optimization horizon: If your
SYSTEM_TIMEstarts at 2020, existing capacity must have installation years before 2020. -
Installation impacts are excluded: The environmental impacts of building existing capacity are considered "sunk costs" and not counted in the optimization.
-
Operation impacts are included: When existing capacity operates during the optimization horizon, its operational emissions are counted.
-
Capacity retirement: Existing capacity is subject to the same lifetime constraints as new capacity. It will retire based on the process's operation time limits.
Combining Constraints¶
Multiple constraints can be combined to model complex scenarios:
from optimex import converter, optimizer
# Parse LCA data
manager = converter.ModelInputManager()
model_inputs = manager.parse_from_lca_processor(lca_data_processor)
# Scenario: Rapid decarbonization with practical limits
model_inputs.cumulative_category_impact_limits = {
"climate_change": 5000000.0, # Total carbon budget
}
model_inputs.category_impact_limits = {
("climate_change", 2030): 800000.0, # Annual limit in 2030
("climate_change", 2040): 200000.0, # Annual limit in 2040
}
model_inputs.process_deployment_limits_max = {
("nuclear_plant", 2025): 0.0, # No new nuclear until 2026
("nuclear_plant", 2026): 100.0,
("nuclear_plant", 2027): 200.0,
}
model_inputs.cumulative_process_limits_max = {
"coal_plant": 0.0, # No new coal plants
}
model_inputs.existing_capacity = {
("coal_plant", 2000): 2000.0, # Existing coal to phase out
("gas_plant", 2010): 1000.0,
}
# Create and solve
model = optimizer.create_model(
model_inputs,
name="decarbonization_scenario",
objective_category="climate_change"
)
solved_model, objective, results = optimizer.solve_model(model)
Constraint Summary Table¶
| Constraint Type | Time-Specific | Cumulative |
|---|---|---|
| Process Deployment | process_deployment_limits_max/min |
cumulative_process_limits_max/min |
| Process Operation | process_operation_limits_max/min |
— |
| Category Impact | category_impact_limits |
cumulative_category_impact_limits |
| Flow Limits | flow_limits_max/min |
cumulative_flow_limits_max/min |
| Process Coupling | process_coupling |
— |
| Existing Capacity | existing_capacity |
— |
Handling Infeasibility¶
If constraints are too restrictive, the optimization may become infeasible (no solution exists). To debug:
# Enable IIS (Irreducible Infeasible Set) generation
solved_model, objective, results = optimizer.solve_model(
model,
compute_iis=True
)
# Check model_iis.ilp for the conflicting constraints
Common Causes of Infeasibility
- Demand cannot be met with available capacity limits
- Impact limits are below the minimum achievable impact
- Conflicting min/max constraints
- Insufficient existing capacity with restrictive deployment limits