Trade Model Setup - High-Level Overview¶
Purpose¶
The trade model optimizes global steel and iron trade flows by solving a linear programming (LP) problem. It determines the most cost-effective way to route materials from suppliers through production facilities to demand centers, subject to capacity constraints, trade policies, and physical limitations.
Core Concept¶
The model represents the global steel value chain as a network:
Nodes: Suppliers (mines), production facilities (furnaces), and demand centers (regions)
Edges: Valid material flows between compatible technologies
Objective: Minimize total cost (production + transport + tariffs + carbon)
Constraints: Capacity limits, trade policies, distance restrictions, feedstock ratios
Main Functions¶
1. set_up_steel_trade_lp() - Build the Optimization Model¶
What it does: Constructs the complete LP model structure from simulation data.
Process:
Initializes an empty LP model with solver tolerance settings
Adds all commodities being modeled (steel, iron, etc.)
Creates process centers for suppliers, production facilities, and demand
Defines valid connections between process types
Applies optional constraints (tariffs, distance limits, feedstock ratios)
Adds location-specific transportation costs if available
Key decisions:
Only includes active furnace groups (configurable status filter)
Scales production capacity by safety factor (typically 95%)
Reuses process definitions across multiple furnaces with same technology
Outputs: A fully configured TradeLPModel ready for optimization
2. Helper Functions - Building Model Components¶
create_process_from_furnace_group()¶
Purpose: Converts a furnace group’s technology specifications into an LP process definition.
What it captures:
Input-output relationships (bill of materials)
Minimum/maximum feedstock ratios
Secondary feedstock requirements
Energy costs per input type
Handles edge cases:
Skips feedstocks with missing or invalid data
Warns about technologies with no primary outputs
Reuses existing BOM elements when possible
add_furnace_groups_as_process_centers()¶
Purpose: Represents each steel production facility as an LP node.
What it models:
Production capacity (scaled by availability factor)
Geographic location for distance calculations
Production cost (carbon cost)
Soft minimum utilization target
Filters: Only includes furnaces with active operating status
add_demand_centers_as_process_centers()¶
Purpose: Represents regional steel demand as LP nodes.
What it models:
Regional demand quantity for the simulation year
Geographic center of demand region
Single shared demand process (all regions demand “steel”)
add_suppliers_as_process_centers()¶
Purpose: Represents raw material sources (mines, scrap yards) as LP nodes.
What it models:
Supply capacity for the simulation year
Geographic location
Production/extraction cost
One process type per commodity (e.g., all scrap sources “scrap_supply”)
Per-year supplier fields. Supplier exposes time-varying capacity, production cost, mine cost and mine price as dicts keyed by Year:
Field |
Lookup |
|---|---|
|
Volumes (tonnes) available that year |
|
Production / extraction cost ($/t) |
|
Mine-level cost ($/t), iron-ore mines only |
|
Mine-level price ($/t), iron-ore mines only |
Trade-LP setup, the average-commodity-price calculation in Environment.calculate_average_commodity_price_per_region(), and downstream callers all read the value for the current simulation year. The same per-year structure applies to non-mine suppliers (scrap, etc.) — they simply populate a flat constant across years if their underlying input has no annual variation. Suppliers whose production_cost_by_year does not contain the current year are silently skipped from the average-price calculation.
3. Constraint Functions¶
enforce_trade_tariffs_on_allocations()¶
Purpose: Applies trade policy restrictions to cross-border flows.
Supports three tariff types:
Volume quotas: Maximum tons per year on a route
Absolute taxes: Fixed cost per ton ($/ton)
Percentage taxes: Cost based on commodity price (% of market price)
Features:
Wildcard support for country groups (e.g., “any country to EU”)
Handles iron product families (hot metal, pig iron, DRI → “iron”)
Accumulates multiple taxes on same route
From ISO3 / To ISO3 syntax in the Tariffs master-input sheet. Each entry resolves to a list of ISO3 codes via _resolve_iso3_or_bloc_entry():
Entry |
Resolves to |
|---|---|
|
The single country |
|
All countries flagged for that bloc on the |
|
All countries not flagged for that bloc |
|
All known countries except that one |
|
Kept as a literal wildcard for downstream matching |
Supported trade-bloc names: EU, EFTA/EUCU, OECD, NAFTA, Mercosur, ASEAN, RCEP. Membership comes from boolean columns on each CountryMapping record (the legacy Trade bloc definitions sheet with one column per region marked X is no longer read). For NOT <X>, the resolver first looks up <X> as a bloc and falls back to treating it as an ISO3 code; an unknown <X> raises ValueError rather than silently producing the universe. Adding a new bloc requires (a) adding a boolean column to the Country mapping sheet, (b) adding the field to the CountryMapping model, and (c) appending the bloc name to supported_blocs in read_tariffs() / find_iso3s_of_trade_bloc().
fix_to_zero_allocations_where_distance_doesnt_match_commodity()¶
Purpose: Enforces physical locality constraints on commodity transport at the LP stage.
Three modes, selected by config:
Clustering disabled (legacy): Distance-based fixing against
hot_metal_radius— hot commodities zeroed beyond the radius, cold commodities zeroed inside it.Clustering enabled, iso3 keying: Hot commodities are fixed to zero across iso3 boundaries; cold commodities remain free. Per-FG radius enforcement is deferred to disaggregation.
Clustering enabled, plant-group keying (
cluster_hot_metal_techs_by_plant_group=True): hot commodities are additionally zeroed between meta-furnace-groups with differentplant_group_id. Same-plant-group pairs, and pairs involving non-meta-FG process centres (suppliers / demand), fall through to the iso3 rule.
Applied before solving and reduces model size. Emits a [LP HOT-METAL] Fixed to zero: X cross-country, Y cross-plant-group, Z missing-iso3 ... summary per year.
Secondary Feedstock & Aggregated Constraints¶
Purpose: Limits scrap availability and enforces technology-specific feedstock ratios.
Secondary feedstock constraints:
Regional limits on scrap, recycled materials
Example: “Europe can only source 50M tons scrap/year”
Aggregated constraints:
Technology-level min/max ratios
Example: “EAF must use 50-90% scrap, 10-50% DRI”
4. solve_steel_trade_lp_and_return_commodity_allocations() - Solve & Extract Results¶
What it does:
Solves the LP optimization problem using Pyomo/HiGHS solver
Extracts optimal allocation values from solver variables
Maps LP results back to domain objects (plants, suppliers, demand centers)
Filters out negligible allocations (< 0.0001 tons)
Error handling:
Returns empty allocations if solver fails to find optimal solution
Logs detailed error messages with termination condition
Continues simulation rather than crashing
Debug output:
Writes
trade_lp_variables.csvwith all allocation detailsLogs statistics on non-zero allocations per commodity
Outputs: Dictionary mapping each commodity to its optimal allocation flows
5. Post-Processing Functions¶
identify_bottlenecks()¶
Purpose: Analyzes results to find capacity-constrained facilities.
What it detects:
Furnace groups operating at or near maximum capacity
Potential supply chain chokepoints
Useful for understanding why demand might not be fully met
Note: Currently logs warnings but doesn’t return structured data.
adapt_allocation_costs_for_carbon_border_mechanisms()¶
Purpose: Applies carbon border adjustment mechanisms (CBAM) to trade costs.
What it models:
Export rebates when high-carbon-price region exports to low-carbon-price region
Import adjustments when low-carbon-price region exports to high-carbon-price region
Prevents double-counting when countries belong to multiple policy regions
Generalized design: Works with any carbon border mechanism (EU CBAM, OECD, etc.), not just EU-specific.
Note: Called separately from main setup, typically in allocation workflow.
Configuration¶
Key SimulationConfig Parameters¶
Model behavior:
lp_epsilon: Solver tolerance (1e-3) - how close to constraints is acceptablecapacity_limit: Production safety factor (0.95) - models realistic availabilityactive_statuses: Which furnace states to include (e.g., [“operating”, “mothballed”])
Physical constraints:
hot_metal_radius: Maximum transport distance for hot commodities (~5 km by default). Enforced in several layers:LP-build time — international hot-commodity flows (different ISO3) are fixed to zero via
fix_to_zero_allocations_where_distance_doesnt_match_commodity(). Whencluster_hot_metal_techs_by_plant_group=True, hot flows between meta-FGs with differentplant_group_idare additionally zeroed.Clustering — BOF FGs with no active BF/ESF/SR within radius in the same country are excluded from their cluster. BOF cluster capacity is capped at
min(physical_cap, Σ[reachable_BF_cap] / min_hot_metal_share)so the LP cannot over-allocate.Disaggregation pre-pass (strict) — hot flows to destinations with a BOM minimum-share constraint are solved under strict radius with per-FG physical-capacity caps: radius-violating edges are omitted from the min-cost-flow graph, and when a geographic pocket can’t physically meet its demand, destination demand is scaled down (producing downstream shortfalls the drift mechanism rebalances).
Drift + rebalance — clusters whose actual joint-strict flow differs from LP get their Case 2 (cluster → demand) batches scaled by the drift factor and their Case 3 (supplier → cluster) batches rebalanced so per-cluster demand matches actual production × BOM ratio while per-supplier totals stay below LP (mine capacity hard-bounded).
Disaggregation (relabeling) — hot flows without a binding minimum constraint that exceed the radius are relabeled to their cold equivalent (e.g.
dri_high→hbi_high,hot_metal→pig_iron).Post-disaggregation — physical capacity and BOM consistency are validated for every FG; any BOF FG that received insufficient hot metal has its utilisation corrected downward.
closely_allocated_products: Hot commodities limited to short distances (hot_metal,dri_high/dri_mid/dri_low,liquid_iron).distantly_allocated_products: Cold equivalents that ship globally (pig_iron,hbi_high/hbi_mid/hbi_low,electrolytic_iron).enable_furnace_group_clustering: When enabled, the LP works with meta-furnace-groups (clusters of same-technology-reductant-country FGs) to reduce problem size; all radius and minimum-ratio enforcement runs at disaggregation time as described above.cluster_hot_metal_techs_by_plant_group: When clustering is enabled, FGs whoseeffective_primary_feedstockstouch a closely-allocated commodity (asmetallic_chargeoroutputs) cluster byplant.ultimate_plant_groupinstead ofiso3. Stored on eachMetaFurnaceGroup.plant_group_idand consumed by the LP’s cross-plant-group zero-fix rule (#1 above). Non-affected techs keep iso3 keying; the[CLUSTERING]log line reports the split per year. No effect whenenable_furnace_group_clusteringis off.
See the “Disaggregation: Hot-Metal Radius + Minimum-Ratio Enforcement” section in overview_trade_model.md for full details.
Economic data:
primary_products: Which commodities to optimize ([“steel”, “iron”])transport_kpis: Location-specific transport costs and emissions
Integration with Simulation¶
The trade model is called during each simulation time step:
Allocation Model prepares input data (plants, demand, suppliers for current year)
Setup phase builds LP model with
set_up_steel_trade_lp()Optional adjustments apply carbon border mechanisms
Solve phase optimizes with
solve_steel_trade_lp_and_return_commodity_allocations()Analysis phase identifies bottlenecks and validates results
Allocations are returned to simulation for plant-level profit calculations
For Detailed Implementation¶
For implementation details, parameter types, and code examples, see the comprehensive docstrings in each function within src/steelo/domain/trade_modelling/set_up_steel_trade_lp.py.