TM-PAM Connector Documentation

Purpose

The TM_PAM_connector bridges the Trade Module (TM) and Plant Agent Model (PAM) by translating trade optimization results into operational parameters for individual furnace groups. It propagates costs through supply chains and updates furnace group attributes (utilization rates, bill of materials, emissions) based on actual trade flows.

Location: src/steelo/domain/trade_modelling/TM_PAM_connector.py


Role in Simulation

The Trade Module solves an LP optimization to determine:

  • Which furnace groups should produce how much

  • Where products should be shipped

  • Optimal allocation of feedstocks (scrap, DRI, iron ore, etc.)

The TM-PAM Connector then:

  1. Converts optimization results → NetworkX graph representing the supply chain

  2. Propagates costs from raw materials through processing stages to final products

  3. Updates furnace groups with their allocated production volumes, material costs, and emissions

When it runs: After every Trade Module optimization, before PAM makes strategic decisions.


Key Concepts

1. Supply Chain as a Directed Graph

The connector models the entire steel production supply chain as a NetworkX MultiDiGraph:

[Iron Ore] ──→ [BF Furnace] ──→ [BOF Furnace] ──→ [Demand]
                    

[Scrap] ──────────→ [EAF Furnace] ──→ [Demand]
  • Nodes: Process centers (furnace groups, supply sources, demand sinks)

  • Edges: Material flows with volumes, costs, and efficiencies

2. Cost Propagation

Costs accumulate as materials move through the supply chain:

Raw Material Cost
    + Transport Cost (from supplier to furnace)
    + Processing Energy Cost (electricity, gas, etc.)
    ↓
Intermediate Product Cost
    + Transport Cost (from intermediate to final furnace)
    + Processing Energy Cost
    ↓
Final Product Cost

Algorithm: Breadth-first traversal starting from raw material sources (iron ore, scrap), accumulating costs at each processing stage.

3. Allocations vs Volumes

  • Volume: Amount of material shipped/produced (output quantity)

  • Allocation: Amount of material required as input (accounts for process inefficiency)

  • Formula: Allocation = Volume / Process Efficiency

Example: If EAF has 95% yield, producing 100t steel requires 100/0.95 = 105.3t of scrap input.


Main Workflow

Stage 1: Graph Construction

Method: create_graph(solved_trade_allocations)

  1. Extracts all non-zero allocations from LP solution: (from_pc, to_pc, commodity) volume

  2. Creates nodes for each process center (furnace group)

  3. Creates edges for each material flow, storing:

    • volume: Shipped quantity

    • transport_cost: Cost to move material between locations

    • processing_energy_cost: Energy cost at source (electricity, gas, etc.)

    • process: Process identifier (e.g., “eaf_scrap_electricity”)

    • process_efficiency: Yield/conversion rate

    • output: Primary product of this process

Result: self.G populated with nodes and edges representing the trade network.


Stage 2: Input Allocation Calculation

Method: calculate_allocations_for_graph()

Converts output volumes to input requirements using process efficiencies:

allocation = edge['volume'] / edge['process_efficiency']

For each edge, updates the allocations attribute with the computed input requirement.

Result: Each edge now has both volume (output) and allocations (input) attributes.


Stage 3: Cost Propagation

Method: propage_cost_forward_by_layers_and_normalize()

Propagates costs forward through the graph using breadth-first search:

  1. Identify roots: Nodes with no incoming edges (raw material sources)

  2. BFS traversal: Process nodes layer by layer

  3. For each edge (u → v):

    • Get base cost from source node u (raw material cost or accumulated upstream cost)

    • Add the producer’s own per-unit carbon (when u is a producing furnace, see below)

    • Add processing energy cost at destination v

    • Add transport cost for this shipment

    • Add tariff cost for this shipment (looked up via get_tariff_cost(from_iso, to_iso, commodity))

    • Multiply by volume shipped

    • Accumulate total cost at destination node v

  4. Normalize: Divide total cost by total outgoing volume to get per-unit cost

Result: Each node has:

  • product_cost: Total accumulated cost by commodity

  • unit_cost: Cost per tonne by commodity

  • allocations: Volume and cost breakdown by commodity

Producer carbon propagation

Each producing furnace’s carbon — ProcessCenter.production_cost, equal to the furnace group’s carbon_cost_per_unit — is stamped onto the graph node as own_unit_cost during create_graph(). During BFS, when an edge leaves a producing node, own_unit_cost is added to per_unit_base so it embeds onto outgoing flows. The addition is guarded by G.in_degree(u) > 0 so root supplier nodes are skipped — their cost already enters via base_cost and would otherwise double-count.

Invariant: own carbon flows out, never inward. The producer’s BOM is built from its incoming allocations only, so it never picks up its own self-carbon. Carbon enters the producer’s own economics later via unit_production_cost = unit_total_opex + carbon_cost_per_unit; embedding it on incoming edges as well would double-count.

Tariff propagation

Tariff taxes from Allocations.tariff_taxes (carried over from the LP solution) are stored on the connector and looked up per edge via get_tariff_cost(from_iso, to_iso, commodity). The lookup checks the exact (from, to, commodity) key first, then the three wildcard variants (*, to, comm) / (from, *, comm) / (from, to, *), summing every match — mirroring the LP’s return_potential_tariff_keys logic. The resulting per-tonne tariff is added to each edge’s material_tariff_transportation_cost and propagates downstream alongside material and transport costs.


Stage 4: Update Furnace Groups

After cost propagation, the connector updates furnace group attributes:

4a. Update Utilization Rates

Method: update_furnace_group_utilisation(furnace_groups)

allocated_volumes = sum(outgoing_edge_volumes)
utilization_rate = allocated_volumes / capacity

Sets fg.utilization_rate based on actual production assigned by Trade Module.

4b. Update Bill of Materials

Method: update_bill_of_materials(furnace_groups)

For each furnace group, extracts from the graph:

Materials (from node allocations):

{
    "scrap": {
        "demand": 105.3,                  # input volume (tonnes)
        "total_cost": 31590,              # USD — includes current step's processing energy
        "unit_cost": 316,                 # USD per tonne of OUTPUT (total_cost / product_volume)
        "total_material_cost": 29500,     # USD — excludes current step's processing energy
        "unit_material_cost": 295,        # USD per tonne of output
        "product_volume": 100.0,          # output volume used for normalisation (tonnes)
    },
    "dri": { ... }
}

Two cost pairs are stored per commodity because downstream consumers need different slices:

  • total_cost / unit_cost include the processing energy consumed at this furnace group (i.e. everything needed to produce the FG’s output, including its own conversion step).

  • total_material_cost / unit_material_cost include upstream material costs, transport, and tariffs, but exclude this FG’s own processing energy.

calculate_variable_opex consumes total_material_cost together with energy (which carries this FG’s processing energy separately) to avoid double-counting. The distinction originates inside the cost-propagation step: MaterialCost on graph nodes tracks the cost of inputs to the FG without its own energy, while Cost adds that energy on top.

Energy (from edge processing costs):

{
    "electricity": {
        "demand": 100.0,          # production volume
        "total_cost": 6000,       # USD
        "unit_cost": 60           # USD/t
    }
}

Sets fg.bill_of_materials = {"materials": {...}, "energy": {...}}

4c. Update Emissions

Method: update_furnace_group_emissions(furnace_groups)

Calls fg.set_emissions_based_on_allocated_volumes() for each furnace group if it has a valid BOM. This calculates emissions from material consumption and technology emission factors.

Sets fg.emissions with structure:

{
    "plant_boundary": {"scope_1": 500, "scope_2": 200},
    "supply_chain": {"scope_3": 1000}
}

Key Methods Reference

Initialization

__init__(dynamic_feedstocks_classes, plants, transport_kpis)

Creates connector and populates lookup tables:

  • flat_feedstocks_dict: O(1) feedstock lookup by name

  • feedstock_energy_requirements: Energy requirements per feedstock

  • processing_energy_cost: Energy costs by furnace group and commodity

  • chosen_reductant: Reductant choice for each furnace

  • transport_costs: Transport cost lookup (from_iso, to_iso, commodity) cost

  • iron_furnaces, steel_furnaces: Lists of furnace group IDs by product type

Graph Construction & Cost Propagation

set_up_network_and_propagate_costs(solved_trade_allocations)

High-level orchestration method that calls in sequence:

  1. create_graph() - Build NetworkX graph from trade results

  2. calculate_allocations_for_graph() - Convert volumes to input requirements

  3. validate_edge_attributes() - Check graph structure

  4. propage_cost_forward_by_layers_and_normalize() - Propagate costs through network

Furnace Group Updates

update_furnace_group_utilisation(furnace_groups)

Sets fg.utilization_rate = allocated_volumes / capacity

update_bill_of_materials(furnace_groups)

Sets fg.bill_of_materials with material and energy costs from graph

update_furnace_group_emissions(furnace_groups)

Calculates and sets fg.emissions based on BOM and emission factors

Utility Methods

get_transport_cost(from_iso, to_iso, commodity)

Returns transport cost between two countries for a commodity (USD/t)

extract_transportation_costs(furnace_groups)

Returns detailed transport cost breakdown for each furnace group’s incoming shipments


Integration Points

Called By

update_furnace_utilization_rates handler (in handlers.py:241):

tmpc = TM_PAM_connector(
    dynamic_feedstocks_classes=env.dynamic_feedstocks,
    plants=uow.plants,
    transport_kpis=env.transport_kpis,
)
tmpc.set_up_network_and_propagate_costs(solved_trade_allocations=trade_allocations)
tmpc.update_furnace_group_utilisation(fgs)
tmpc.update_bill_of_materials(fgs)
tmpc.update_furnace_group_emissions(fgs)
env.allocation_and_transportation_costs = tmpc.extract_transportation_costs(fgs)

Dependencies

Inputs:

  • Allocations object from Trade Module LP solver

  • PlantInMemoryRepository for accessing all plants and furnace groups

  • dynamic_feedstocks dict mapping technologies to feedstock options

  • TransportKPI list with transportation costs

Updates:

  • FurnaceGroup.utilization_rate

  • FurnaceGroup.allocated_volumes

  • FurnaceGroup.bill_of_materials

  • FurnaceGroup.emissions

Used By:

  • PAM decision-making (uses updated costs and utilization)

  • Balance sheet calculations (uses updated BOM costs)

  • Emissions reporting (uses updated emissions)


Data Flow Through Connector

Trade Module LP Solver
    ↓
Allocations: (from_pc, to_pc, commodity) → volume
    ↓
TM_PAM_connector.set_up_network_and_propagate_costs()
    ↓
1. create_graph()
   → NetworkX MultiDiGraph with nodes (furnaces) and edges (flows)
    ↓
2. calculate_allocations_for_graph()
   → Add input allocations (volume / efficiency) to edges
    ↓
3. propage_cost_forward_by_layers_and_normalize()
   → BFS cost accumulation from raw materials to final products
   → Each node gets: product_cost, unit_cost, allocations
    ↓
4. update_furnace_group_utilisation()
   → fg.utilization_rate = sum(outgoing_volumes) / capacity
    ↓
5. update_bill_of_materials()
   → fg.bill_of_materials = {materials: {...}, energy: {...}}
    ↓
6. update_furnace_group_emissions()
   → fg.emissions = {boundary: {scope: value}}
    ↓
PAM uses updated FurnaceGroup attributes for decision-making

Known Limitations

  1. Assumes DAG Structure: Cost propagation assumes no cycles in the supply chain graph. This is determined by the structure of the dynamic bill of materials and legal process connectors given to the Trade Module.

  2. No Transport Mode Differentiation: All transport costs treated equally - no distinction between rail, sea, truck, etc.

  3. Zero-Volume Edge Handling: Edges with volume < LP_TOLERANCE are skipped, which may lose some small flows.


Summary

The TM-PAM Connector serves as the critical bridge between optimization and simulation:

  • Input: Trade optimization results (allocations)

  • Process: Graph-based cost propagation through supply chains

  • Output: Updated furnace group parameters (utilization, BOM, emissions)

  • Purpose: Ensures PAM makes decisions based on actual trade-optimized costs and production levels

This two-way integration enables:

  1. Trade → PAM: Operational parameters reflect optimized production schedules

  2. PAM → Trade: Strategic decisions (technology switches, expansions) feed back into future trade optimizations