Skip to content

Supply Planning Engine — Complete Technical Reference

What this page covers

End-to-end description of every phase the supply engine executes, every input it consumes, every constraint it enforces, and every output it produces. Audience: developers, data engineers, and supply planners who need to understand why the engine generates a particular order or projection.


Architecture Overview

The supply engine is a Python orchestrator wrapping a Rust core. The Rust binary (supply_engine, compiled with PyO3) does all numerically intensive work in parallel using Rayon. The Python layer handles data loading, pre- and post-processing, and multi-echelon orchestration.

supply_runner.py (Python)
  ├─ Pre-processing (Python)
  │    ├─ Netting pre-step        forecast_netting.compute_and_persist()
  │    ├─ Data loading            _load_sku_plans()
  │    ├─ Supersession handling   _apply_supersession()
  │    ├─ BFS upstream discovery  _add_upstream_network_nodes()   (max 4 BFS passes)
  │    ├─ BOM explosion           _explode_bom_demand()
  │    ├─ Calendar pull-forward   _apply_calendar_pull_forward()
  │    └─ Demand aggregation      _aggregate_demands_to_upstream_whs()
  ├─ PASS 1  Rust engine — upstream WH / hub nodes
  │          supply_engine.run_supply_plan(upstream_plans, …)
  ├─ ALLOC   _allocate_transfers()  (Python, Kahn top-down proportional)
  ├─ PASS 2  Rust engine — store / leaf nodes
  │          supply_engine.run_supply_plan(store_plans, …)
  └─ Post-processing (Python)
       ├─ Lot-sizing snap          _snap_lot_sizes()
       ├─ FEFO expiry simulation   _simulate_fefo_expiry()
       ├─ Expedite order gen       _generate_expedite_orders()
       ├─ Pre-position tagging     _tag_prepos_orders()
       └─ Projection enrichment    _enrich_projections()

Rust engine internal passes (engine.rs::run_impl):
  0. Preprocessing     preprocess.rs   forecast consumption + repair-return autofill
  1. SKU filter        engine.rs       active SKU selection
  2. Netting           netting.rs      requirements calculation (parallel)
  3. Sourcing          sourcing.rs     route selection + lot-sizing (parallel)
  4. Capacity          scheduling.rs   resource scheduling (parallel)
  5. Re-netting        netting.rs      final stock projection with placed orders
  6. Push              push.rs         expedite / split residual shortages
  7. Pegging           pegging.rs      FIFO demand-to-supply links
  8. Cost              cost.rs         total cost computation

Inputs — All Data the Engine Consumes

Per-SKU inputs (SkuPlan)

Field Type Source Description
sku_id u32 master.item.id Item primary key
location_id u32 master.location.id Site primary key
initial_inventory f32 master.on_hand Serviceable on-hand: total_on_hand − oh_bad_qty
initial_oh_bad f32 master.on_hand WHERE type = bad Unserviceable / failed units on-hand
initial_oh_new f32 master.on_hand WHERE type = new New stock (depleted only after good stock exhausted)
expiry_period_weeks Option master.item.shelf_life_weeks Shelf life in weeks; NULL = immortal
expiry_batches Vec master.on_hand (batch-level) Known batches with explicit expiry dates
demand[52] [f32;52] PIPE_forecast_netting.supply_plan_qty Netted weekly demand (post-consumption, is_past=0 only)
scheduled_receipts[52] [f32;52] master.supply_orders_firm Firm/released/in-transit ERP orders
repair_returns[52] [f32;52] master.interact_sales_returns OR autofill Confirmed returns or autofilled from return_rate
safety_stock[52] [f32;52] PIPE_meio_results.committed_buffer MEIO safety stock (scalar expanded to 52 weeks)
failure_rate[52] [f32;52] master.route.return_rate Fraction of demand that returns (scalar expanded)
firm_orders[52] [f32;52] master.demand_actuals (confirmed only) Firm customer orders for Rust-side forecast consumption
bad_returns[52] [f32;52] master.interact_sales_returns Unserviceable units returning from field per week
latest_firm_by_type[7] [u8;7] master.supply_orders_firm Latest firm order week per order type (255=none)
min_lt_by_type[7] [u8;7] master.route Minimum lead time per order type (0=no route)
is_upstream bool BFS flag True for WH/hub nodes; False for demand-leaf stores
active bool engine config Whether to plan this SKU at all
policy enum supply scenario Continuous / MinMax{min, target} / Poq{period_weeks}
routes Vec\<RouteOption> master.route All replenishment options (see below)

Per-route inputs (RouteOption)

Field Type Description
route_id u64 Route primary key
source_type enum Repair / Return / Balancing / Transfer / Build / Buy
source_location_id Option\<u32> Source site for Transfer/Balancing routes
lead_time u8 Weeks from release to nominal arrival
lead_time_cv f32 Coefficient of variation for stochastic lead-time padding
unit_cost f32 Cost per unit ordered
order_cost f32 Fixed cost per order placed
min_qty f32 Minimum order quantity (MOQ). Overridden by EOQ from linked inventory_scenario
mult_qty f32 Lot multiple — order must be a multiple of this value
max_qty f32 Maximum order quantity (0 = unlimited)
capacity_id u32 Links to CapacityResource (0 = unconstrained)
shipping_calendar u64 52-bit bitmask of open shipping weeks
receiving_calendar u64 52-bit bitmask of open receiving weeks
repair_yield f32 Fraction of returned units that can be successfully repaired
return_rate f32 Fraction of demand expected to return for repair
repair_turnaround u8 Total lag in weeks: return_transit + repair_TAT
priority i16 Route priority within same source type (lower = higher priority)
quota f32 Multi-sourcing fraction (0–1): this route covers quota × need
setup_cost f32 One-time changeover cost
expedite_cost f32 Premium multiplier on unit_cost when expediting
transport_mode String Air / Sea / Road / Rail
is_expedite bool If True, generates BUY_EXPEDITE orders in post-processing

Global engine configuration (EngineConfig)

Parameter Default Description
horizon_weeks 52 Planning horizon in weeks
workers 0 (auto) Rayon thread pool size (0 = CPU count)
sparse_threshold 0.0 Skip SKUs with total demand below this
source_priority [Repair,Return,Transfer,Build,Buy] Order of preference when multiple routes can cover a shortage
firm_weeks 0 Weeks in frozen horizon — no new engine orders generated
z_score 0.0 Stochastic lead-time padding factor (0 = deterministic)
consolidation_window 0 Merge consecutive orders within N weeks (0 = disabled)
forward_fence 4 Forecast consumption window ahead of a firm order (weeks)
backward_fence 0 Forecast consumption window behind a firm order (weeks)
planning_mode ServiceOptimized ServiceOptimized (fill rate first) or CostOptimized (minimize total cost)
pegging_mode Full Full / Partial / Off
allocation_strategy Fifo Fifo / Priority / FairShare / ExpiryFirst
shortage_penalty 50.0 Penalty cost per unit of unmet demand
lateness_penalty_per_week 10.0 Penalty per unit per week late
lost_sales_penalty 200.0 Penalty per permanently lost sale
holding_cost_pct 0.25 Annual holding cost as fraction of item value
expedite_cost_multiplier 1.5 Extra cost factor for expedited orders
overtime_cost_multiplier 1.5 Extra cost factor for capacity overtime orders
soft_capacity_multiplier 1.2 Soft cap = hard cap × this factor
nervousness_limit 0.0 Max plan-to-plan change rate (0 = disabled)
service_level_weight 0.7 Weight in CostOptimized scoring
cost_weight 0.3 Weight in CostOptimized scoring
lateness_weight 0.1 Weight in CostOptimized scoring
balancing_threshold 0.1 Minimum excess fraction to trigger cross-location balancing
balancing_max_pct 0.5 Maximum fraction of excess to redistribute
firm_orders_block_horizon false Block new orders before the latest firm order per type
respect_lead_time false Block orders in the minimum-LT window (physically impossible)

Inventory Types

The engine tracks five distinct inventory pools simultaneously:

Pool Field Description Used for
Good (serviceable) initial_inventory = total_oh − oh_bad Units available for customer demand Primary netting; depleted before new stock
Bad (unserviceable) initial_oh_bad Failed / returned units awaiting repair Repair-pool planning; does not satisfy demand
New stock initial_oh_new New units (allocated only after good stock exhausted) Projected separately; informational
Repair pipeline repair_returns[52] Units arriving back from repair each week Added to serviceable supply in netting pass
Scheduled receipts (ERP) scheduled_receipts[52] Firm orders already in transit Added to available supply; engine cannot modify these

Additionally, expiry batches (expiry_batches) track lot-level shelf lives for FEFO simulation (see Phase 10).


Phase 0 — Python Pre-processing

0a. Context resolution

From config_scenario_pipeline, the runner resolves four scenario IDs:

  • supply_scenario_id — which supply parameter set to use
  • meio_scenario_id → safety stock from PIPE_meio_results.committed_buffer
  • forecast_scenario_id → demand from PIPE_forecast_netting.supply_plan_qty
  • inventory_scenario_id → EOQ/lot-sizing from scen_inventory_scenario.results

0b. Netting pre-step

forecast_netting.compute_and_persist() refreshes PIPE_forecast_netting with firm-order consumption applied. This is skipped if netting data was refreshed within the last 60 minutes (SUPPLY_NETTING_MAX_AGE_MIN).

The netting pre-step combines statistical, maintenance, and causal forecasts, applies forward/backward consumption against confirmed firm customer orders, and writes supply_plan_qty — the final net demand the Rust engine will see.

0c. Data loading — _load_sku_plans()

A single massively denormalized SQL CTE loads all per-SKU data in one round-trip:

WITH
  fcst  AS (CH PIPE_forecast_netting: supply_plan_qty × 52 weeks per (item_id, site_id)),
  eoq   AS (scen_inventory_scenario.results  items[].eoq  overrides route.min_qty),
  oh    AS (master.on_hand: total_on_hand, oh_bad_qty, oh_new_qty, expiry_batches),
  ss    AS (CH PIPE_meio_results.committed_buffer per item×site),
  primary_route AS (lowest-priority BUY/MAKE route per item×site),
  ret_rt AS (RETURN routes: return_rate, return_lt_weeks),
  rep_rt AS (REPAIR routes: repair_yield, repair_tat_weeks),
  rts   AS (json_agg ALL active routes per item×site),
  sched AS (master.supply_orders_firm: scheduled_receipts[52])
SELECT
  initial_inventory  = total_on_hand - oh_bad_qty,
  initial_oh_bad,
  initial_oh_new,
  safety_stock       = COALESCE(committed_buffer, 0),
  min_qty            = CASE WHEN eoq valid THEN floor(eoq/mult)*mult
                       ELSE route.min_qty END,
  routes_json,
  demand[52],
  scheduled_receipts[52]

EOQ integration: when the pipeline is linked to an inventory_scenario, the per-SKU EOQ replaces route.min_qty as the floor on lot sizing. The value is first aligned to the mult_qty boundary: min_qty = floor(eoq / mult) × mult.

0d. Supersession handling

# Global supersession (no site_id): blocks ALL BUY/MAKE routes for the old item_id
# Site-specific: blocks exact (item_id, site_id) pairs
# stock_rollable: on-hand of from_item added to to_item.initial_inventory at same site

Items marked as superseded have their BUY/MAKE routes silenced — the engine will not generate procurement orders for them. Stock of a superseded item is rolled forward to its successor item's starting inventory when stock_rollable = true.

0e. BFS upstream node discovery — _add_upstream_network_nodes()

for echelon in range(max_echelons):   # default: 4
    # Find all source_site_id values from TRANSFER/MAKE routes
    # for items already in the plan
    new_nodes = query("""
        SELECT DISTINCT r.item_id, r.source_site_id
        FROM master.route r
        JOIN master.route_type rt ON rt.id = r.type_id
        WHERE rt.planning_type IN ('TRANSFER', 'MAKE')
          AND r.item_id IN (:known_ids)
    """)
    if not new_nodes: break
    # Add each new (item, site) with demand=[0]*52, is_upstream=True

Each BFS pass discovers one upstream echelon. Default depth limit = 4 BFS passes → maximum 5 levels (4 upstream WHs + 1 store leaf). See the depth limit discussion below.

Upstream nodes are initialized with: - demand = [0.0] × 52 (no direct customer demand) - initial_inventory from master.on_hand at that site - scheduled_receipts from ERP firm orders at that site - is_upstream = True

0f. BOM explosion — _explode_bom_demand()

For items with BUILD routes, child component demands are augmented:

component_demand[w] += parent_demand[w] × bom_qty

The explosion is applied bottom-up through the BOM tree so multi-level assemblies are handled correctly.

0g. Calendar pull-forward — _apply_calendar_pull_forward()

For each route with a ship_calendar_id, the Python layer computes closure weeks from the 52-bit bitmask and accumulates any demand falling in those weeks into the last available week before the closure. These pre-position orders are tagged TRANSFER_PREPOS in post-processing.

0h. Demand aggregation — _aggregate_demands_to_upstream_whs()

Kahn topological sort (leaves → root): each upstream WH's demand[] is set to the sum of all downstream nodes it supplies. This ensures Pass 1 sees the correct aggregate demand for BUY order generation at warehouses that have zero direct customer demand.


Phase 1 — Rust Engine: Upstream Nodes (Pass 1)

A single call to supply_engine.run_supply_plan(upstream_plans, …) with all upstream (WH/hub) nodes. The Rust engine runs all internal passes (preprocessing → netting → sourcing → scheduling → push → pegging → cost) on these nodes simultaneously.

TRANSFER orders are never generated in Pass 1 — the transfer_pool is always [0;52], meaning the engine treats TRANSFER as unavailable for upstream nodes. BUY, BUILD, REPAIR, and RETURN orders are generated normally.

Output: BUY/BUILD/REPAIR/RETURN orders + 52-week inventory projections for every upstream node.


Phase 2 — Transfer Allocation — _allocate_transfers()

Kahn topological sort root → leaves. This is the inverse direction of demand aggregation.

Algorithm

For each WH node in root-first topological order:
  1. Compute available supply this week:
     ROOT WH (has BUY arrivals):
       flow[w] = max(0, prev_inv + buy_arrivals[w] - curr_inv)
       (implied demand flow — avoids double-counting on-hand)
     INTERMEDIATE WH (pass-through, no BUY routes):
       supply = fresh TRANSFER arrivals from its parent only

  2. Compute each downstream node's need:
       need[dest][w] = demand[w] + max(0, ss[w] - current_projected_inv[dest])

  3. Distribute proportionally:
       alloc[dest][w] = available[w] × need[dest][w] / Σ need[all_dests][w]

  4. Record TRANSFER order and update dest's scheduled_receipts for Pass 2

For a 5-level chain this produces a cascade:

Step 1: Root WH → Hub         (root BUY arrivals pushed down)
Step 2: Hub → Country WH      (TRANSFER arrivals from root pushed down)
Step 3: Country WH → Region   (TRANSFER arrivals from hub pushed down)
Step 4: Region → Store        (TRANSFER arrivals fed into store scheduled_receipts)

SS-aware allocation

ss_part = max(0, alloc - demand[w])    # safety stock top-up portion
# Deducted from WH's own SS budget to avoid over-committing buffers

Phase 3 — Rust Engine: Stores (Pass 2)

A single call to supply_engine.run_supply_plan(store_plans_p2, …) with all store (leaf) nodes. Each store now has TRANSFER arrivals injected into its scheduled_receipts from the allocation step.

The engine runs the same internal passes as Pass 1. Stores whose TRANSFER supply is insufficient may generate local BUY orders (fallback procurement). Residual shortages appear as exceptions.


Rust Engine Internal Passes

Figure: end-to-end flow of one run_supply_plan call — demand ingest, netting, sourcing, capacity scheduling, push, pegging, cost, then order emission.

sequenceDiagram
    participant P as "Python orchestrator"
    participant E as "Rust engine"
    participant S as "Sourcing"
    participant C as "Capacity scheduler"
    participant PG as "Pegging"
    P->>E: "run_supply_plan(SkuPlans)"
    E->>E: "Pass 0 preprocess (forecast consumption + repair autofill)"
    E->>E: "Pass 1 active SKU filter"
    E->>E: "Pass 2 netting (shortage per week)"
    E->>S: "Pass 3 sourcing (route priority, lot-size)"
    S-->>E: "planned orders (qty, release, arrival)"
    E->>C: "Pass 4 capacity scheduling"
    C-->>E: "Planned / Delayed / Overtime / Constrained"
    E->>E: "Pass 5 re-netting + projections"
    E->>E: "Pass 6 push (expedite / split)"
    E->>PG: "Pass 7 FIFO pegging"
    PG-->>E: "PegLink[] (demand, supply)"
    E->>E: "Pass 8 cost computation"
    E-->>P: "orders + projections + pegging"

Rust Pass 0 — Preprocessing (parallel via Rayon)

Forecast consumption (apply_forecast_consumption):

For each firm_orders[fw] > 0:
  remaining = firm_orders[fw]
  backward pass: reduce demand[fw-backward_fence .. fw-1]  (default: 0)
  forward pass:  reduce demand[fw .. fw+forward_fence]      (default: 4 weeks)
  Always clamp to ≥ 0

This is the Rust-side (second) forecast consumption. The Python-side netting pre-step already applied firm-order consumption to PIPE_forecast_netting; the Rust pass re-applies it on the raw firm_orders[] array for any firm orders loaded from ERP that were not yet reflected in the netting output.

Repair-return autofill (autofill_repair_returns):

If repair_returns is all-zero AND the SKU has a Repair route:
  For each Repair route (return_rate, repair_yield, repair_turnaround):
    repair_returns[w] = demand[w - repair_turnaround]
                        × return_rate × repair_yield
  Take MAX over all Repair routes per week

This generates a statistical repair-return forecast when no confirmed returns exist, ensuring the engine accounts for the expected repair pipeline even without explicit return records.

Rust Pass 1 — Active SKU filter

active = [sku for sku in all_skus if sku.active && total_demand(sku) >= sparse_threshold]

SKUs with zero demand throughout the horizon are skipped unless they have safety stock requirements.

Rust Pass 2 — Netting (parallel via Rayon, netting.rs)

The netting pass computes net requirements week by week:

carry = initial_inventory

For t in 0..52:
  available = carry + scheduled_receipts[t] + repair_returns[t]
  required  = demand[t] + safety_stock[t]

  if available + NETTING_EPS ≥ required:
    carry = (available - demand[t]).max(0.0)    # satisfy demand; buffer intact
    shortage[t] = 0.0

  else:
    shortage[t] = (required - available).max(0.0)   # how much we're short
    carry = (available - demand[t]).max(0.0)         # physical stock stays positive
    # KEY: when only the SS buffer is breached, carry can still be > 0
    # e.g. available=20, demand=10, SS=25: shortage=5, carry=10 (not 0)

  inventory[t] = carry

NETTING_EPS = 1e-4  (prevents f32 ULP drift from producing false shortages)

Safety-stock-aware carry: when available inventory covers demand but not the safety stock buffer, the physical carry-forward remains positive. Only when physical stock would go negative does the carry reach zero. This prevents cascading phantom shortages from propagating through subsequent weeks.

Rust Pass 3 — Sourcing (parallel via Rayon, sourcing.rs)

For each shortage week, routes are tried in the configured source_priority order (default: Repair → Return → Transfer → Build → Buy).

Pre-sourcing checks (per route, per shortage week):

Check Condition Effect
Frozen horizon firm_orders_block_horizon=true AND week < latest_firm_by_type Skip this route type this week
Min lead time respect_lead_time=true AND week < min_lt_by_type Skip — physically impossible
Repair pool source_type=Repair Skip if repair_pool[week] == 0
Transfer pool source_type=Transfer/Balancing Skip if transfer_pool[week] == 0

Lead-time calculation with stochastic padding:

effective_LT = if lead_time_cv > 0 && z_score > 0 {
    lead_time + ceil(z_score × lead_time_cv × lead_time)
} else {
    lead_time
};

Calendar adjustment:

raw_release  = max(0, need_week - effective_LT)
release_week = shipping_calendar.shift_to_open(raw_release, Backward)
raw_arrival  = release_week + effective_LT
arrival_week = receiving_calendar.shift_to_open(raw_arrival.min(51), Forward)

A 52-bit bitmask calendar operation: shift_to_open(week, Backward) finds the latest open week at or before week; Forward finds the earliest open week at or after week.

Lot-sizingallocate() — 7-step algorithm:

Input:  needed (shortage quantity), route (min_qty, mult_qty, max_qty, quota)

Step 1: quota    → quota_needed = needed × route.quota  (if 0 < quota < 1)
Step 2: max cap  → capped = min(quota_needed, max_qty)  (0 = unlimited, skip)
Step 3: min floor→ with_min = max(capped, min_qty)
Step 4: min>max  → if with_min > max_qty: return 0       (infeasible)
Step 5: mult up  → rounded = ceil(with_min / mult_qty) × mult_qty
Step 6: max cap  → after_max = min(rounded, max_qty)
Step 7: floor    → if mult_qty > 0 AND max_qty > 0:
                     floored = floor(after_max / mult_qty) × mult_qty
                     if floored < min_qty: return 0       (infeasible — e.g. min=10, mult=6, max=11)
                     return floored
                   else: return after_max

Order-splitting when max_qty is binding:

while remaining > 0 && orders.len() < 10_000 {
    let qty = allocate(remaining, route);
    if qty == 0 { break; }
    orders.push(order(qty, release_week, arrival_week));
    remaining -= qty;
}

Multi-sourcing (quota routes): each quota route is solved independently against the same remaining, allocating its proportional share.

Order policies:

Policy Trigger condition Order quantity
Continuous (default) Any shortage Exactly the shortage amount (then lot-sized)
MinMax{min_qty, target_qty} Any shortage max(target_qty + ss_gap, shortage)
Poq{period_weeks} Only in weeks that are multiples of period_weeks Aggregated demand for the POQ window

Optional: Consolidation pass

When consolidation_window > 0, consecutive orders for the same (sku, location, route) within N weeks are merged:

# Sort orders by (sku_id, location_id, route_id, release_week)
# Greedy merge: if gap ≤ consolidation_window:
#   sum qty, keep earlier release_week, keep later arrival_week

Consolidation reduces the number of purchase orders at the cost of slightly higher cycle stock.

Rust Pass 4 — Capacity scheduling (parallel, scheduling.rs)

Figure: how an order interacts with a capacity resource — soft-cap overtime band, week-by-week scan, and the Constrained fallback.

flowchart TD
    A["Order placed by sourcing"] --> B{"capacity_id > 0?"}
    B -->|"no, unconstrained"| C["Status = Planned"]
    B -->|"yes"| D["Enter scheduling loop<br/>check_week = original_release"]
    D --> E{"avail >= needed?"}
    E -->|"yes"| F["Schedule at check_week"]
    E -->|"no"| G{"avail x (1 + soft_mult) >= needed?"}
    G -->|"yes"| H["Schedule with overtime<br/>Status = Overtime"]
    G -->|"no"| I{"check_week < 52?"}
    I -->|"yes"| D
    I -->|"no"| J["Status = Constrained<br/>CapacityExceeded exception"]
    F --> K{"check_week > original_release?"}
    K -->|"yes"| L["Status = Delayed"]
    K -->|"no"| C

Orders linked to a capacity resource (capacity_id > 0) go through the capacity scheduler.

Priority ordering (highest priority scheduled first):

Source type Priority
Repair 5 (highest)
Return 4
Transfer / Balancing 3
Build 2
Buy 1 (lowest)

Within the same source type, lower unit_cost is preferred.

Scheduling loop:

for check_week in original_release_week..=51:
    avail = capacity_map.get((cap_id, check_week)).unwrap_or(0.0);

    if avail >= needed:
        schedule at check_week  status = Planned (or Delayed if > original_release_week)
        capacity_map[(cap_id, check_week)] -= needed
        break

    else if soft_capacity_multiplier > 0
         && avail × (1 + soft_capacity_multiplier) >= needed:
        schedule at check_week  status = Overtime
        break

    // else: try next week

if not scheduled:
    status = Constrained
    generate CapacityExceeded exception (severity = Critical)

Rust Pass 5 — Re-netting + bad/new stock projection

After placing all orders, a final netting pass is run with the new order arrivals added to scheduled_receipts:

extra_receipts[arrival_week] += order.qty   // for each placed order
(final_inv, final_shortage) = netting_pass(plan, extra_receipts)

Bad-stock projection (informational, does not affect netting):

bad_carry = initial_oh_bad
for w in 0..52:
    oh_bad[w] = bad_carry
    bad_carry = max(0, bad_carry + bad_returns[w] - repair_returns[w])

New-stock projection (good stock depletes before new stock):

oh_good_init = max(0, initial_inventory - initial_oh_new)
for w in 0..52:
    oh_new[w] = new_carry
    consumed = max(0, prev_total - final_inv[w])
    from_good = min(consumed, good_carry);  good_carry -= from_good
    new_carry = max(0, new_carry - (consumed - from_good))
    prev_total = final_inv[w]

Rust Pass 6 — Push pass: expedite and split (push.rs)

For each week that still has remaining shortage after capacity scheduling:

Strategy 1 — Expedite:

// Find the nearest Buy/Repair order arriving AFTER need_week for this SKU
// Pull it back by 1 week:
order.release_week -= 1
order.arrival_week = max(need_week, order.arrival_week - 1)
order.status = Expedited

Strategy 2 — Split:

// Find an order with qty > 2 × remaining_shortage
//   arriving within 2 weeks after need_week
// Split off remaining_shortage quantity → new order at need_week (status=Expedited)
// Reduce original order qty

Strategy 3 — UnmetDemand exception (severity = Critical): No expedite or split possible — the shortage cannot be covered.

Rust Pass 7 — FIFO pegging (pegging.rs)

Figure: FIFO pegging tree — earliest demand is satisfied from initial inventory and earliest-arriving supply first; residual demand becomes unmet.

graph TD
    D1["Demand week 12<br/>qty = 10"]
    D1 --> S0["Initial inventory<br/>arrival week 0, qty = 4"]
    D1 --> S1["Firm order PO-7<br/>arrival week 9, qty = 3"]
    D1 --> S2["Planned BUY<br/>arrival week 11, qty = 3"]
    D2["Demand week 18<br/>qty = 6"]
    D2 --> S2r["Planned BUY (remainder)"]
    D2 --> S3["Planned BUY<br/>arrival week 16, qty = 6"]
    D2 -.->|"queue exhausted"| U["Unmet demand<br/>(no PegLink)"]

Oldest-arriving supply is matched to earliest demand.

// Build supply queue sorted by arrival_week ASC
// Include initial_inventory as virtual supply at week 0 (order_id = 0)

for t in 0..52 where demand[t] > 0:
    remaining = demand[t]
    // Peg from initial_carry first, then from supply_queue FIFO
    while remaining > 0 && !supply_queue.is_empty():
        supply = supply_queue.peek_mut()
        if supply.arrival_week > t: break   // can't peg future supply to past demand
        pegged = min(remaining, supply.qty)
        PegLink { order_id, demand_week: t, supply_week: supply.arrival_week, qty: pegged }
        remaining -= pegged
        supply.qty -= pegged
        if supply.qty  0: supply_queue.pop()
    // Remaining demand after queue exhausted = unmet (no PegLink)

Rust Pass 8 — Cost computation

for order in final_orders:
    base  = order.qty × order.unit_cost + order.order_cost
    extra = match order.status:
        Expedited  order.qty × order.unit_cost × (expedite_cost_multiplier - 1.0)
        Overtime   base × (overtime_cost_multiplier - 1.0)
        _          0.0
    order.total_cost = base + extra + order.penalty_cost

The full CostBreakdown struct (cost.rs) decomposes cost into five components:

Component Description
production_cost Unit procurement / manufacturing cost
transport_cost Standard freight cost (unit cost × qty for BUY routes)
inventory_cost Holding cost accumulated over any delay period
delay_penalty Service penalty, AOG cost, contractual lateness fees
capacity_flex_cost Overtime premium, overflow storage, premium freight

Phase 4 — Python Post-processing

Lot-sizing snap

After the Rust engine returns orders, Python re-applies min/mult/max constraints to correct any floating-point drift:

snapped = min(raw_qty, max_qty)
snapped = max(snapped, min_qty)
snapped = ceil(snapped / mult_qty) * mult_qty
snapped = min(snapped, max_qty)
snapped = floor(snapped / mult_qty) * mult_qty
if snapped < min_qty:
    snapped = 0.0   # infeasible — drop the order

FEFO expiry simulation

For items with expiry_period_weeks or explicit expiry_batches:

# Seed the FEFO queue from on-hand batches (explicit expiry dates)
# plus remaining OH (assumed to expire in expiry_period_weeks)
# Add arriving supply orders to queue at arrival_week + expiry_period_weeks

for w in 0..52:
    # Remove expired batches (expiry_week ≤ w)
    expired[w] = sum of expired batch quantities
    # Sort queue by expiry_week (FEFO)
    # Consume demand from earliest-expiry first
    # Record expired_qty[w]

# Attach expiry_date and expired_qty to each supply order

Expedite order generation

For routes with is_expedite = True, the post-processor generates BUY_EXPEDITE orders to cover any remaining shortage weeks not resolved by the engine's push pass.

Pre-position tagging

Any TRANSFER order matching a pre-position anchor recorded during _apply_calendar_pull_forward() is relabelled TRANSFER_PREPOS.

Projection enrichment

Projection rows from Rust contain only inventory[] and shortage[]. Python adds: - demand[] (from plan data) - supply_received[] (sum of arriving order quantities per week) - safety_stock[] (from plan data) - expired_qty[] (from FEFO simulation)


All Order Types

Type DB value Description
Buy BUY External procurement from supplier
Build BUILD Internal manufacturing / assembly
Repair REPAIR Refurbishment of returned unserviceable units
Transfer TRANSFER Cross-site replenishment (Python allocation)
Balancing BALANCING Cross-location redistribution of excess stock
Return RETURN Anticipated customer returns (as supply)
BUY_EXPEDITE BUY_EXPEDITE Expedited BUY via premium route (post-processing)
TRANSFER_PREPOS TRANSFER_PREPOS Pre-position transfer ahead of calendar closure

Order statuses

Figure: planned-order lifecycle — sourcing places Planned, capacity scheduler may Delay / Overtime / Constrain, push pass may Expedite.

stateDiagram-v2
    [*] --> Planned: "Sourcing places order"
    Planned --> Delayed: "Capacity scheduler pushes to later week"
    Planned --> Overtime: "Scheduled in soft-capacity band"
    Planned --> Constrained: "No capacity slot in horizon"
    Planned --> Expedited: "Push pass pulls order in early"
    Delayed --> Expedited: "Push pass expedite"
    Overtime --> Expedited: "Push pass expedite"
    Constrained --> [*]: "CapacityExceeded exception"
    Expedited --> [*]: "Final order row"
    Planned --> [*]: "Final order row"
Status Badge Meaning
Planned Green Normal planned order; no constraint triggered
Delayed Amber Pushed to a later week by the capacity scheduler
Overtime Orange Scheduled using the soft-capacity (overtime) band
Expedited Blue Pulled in early by the push pass to cover a shortage
Constrained Red No capacity slot found in the entire horizon

All Exception Types

Type Severity Trigger
Shortage Warning Safety stock buffer violated (not physical stockout)
UnmetDemand Critical Physical demand cannot be covered — push pass exhausted
CapacityExceeded Critical No capacity slot found for an order across the full horizon
LateOrder Warning Order arrives after its demand week
RepairPoolEmpty Warning No repair inventory available when a Repair route is needed
BalancingPoolEmpty Info No excess stock available for cross-location balancing
LostSale Critical Firm demand where min LT > demand date + tolerance (Python-generated)

Multi-Echelon Architecture

BFS depth limit

The only depth limit is the BFS cap in _add_upstream_network_nodes():

def _add_upstream_network_nodes(plans, , max_echelons: int = 4, ):
BFS iteration Echelon discovered (example)
1 Direct source of stores → country WH
2 Source of country WH → regional hub
3 Source of regional hub → central WH
4 Source of central WH → root supplier

With the default max_echelons = 4, up to 5 total levels are supported (4 upstream + 1 store leaf). To support deeper networks, raise the cap:

max_echelons = int(supply_cfg.get("max_echelons", 4))

Everything downstream of BFS discovery (demand explosion, Pass 1, allocation, Pass 2) is depth-agnostic — it uses Kahn topological sort which works on any DAG of any depth.

Comparison: MRP engine vs LP solver

Aspect MRP engine LP solver
Upstream planning Pass 1: all upstream nodes, greedy priority Unified LP: all nodes, cost-minimizing
Transfer quantities Proportional push allocation LP decision variables — globally optimal
Store planning Pass 2: stores see injected TRANSFER arrivals Same LP — all echelons unified
Intermediate WH rebalancing Not possible — one-pass top-down LP can rebalance across echelons
Depth sensitivity Proportional error compounds per echelon LP is exact regardless of depth
Performance Very fast (Rust parallel, milliseconds per 1000 SKUs) Slower (LP solver scales with variables²)

Key Design Decisions

Why the safety stock does not consume physical stock

In the netting pass, shortage[t] is set when available < demand[t] + ss[t] but carry is computed as max(0, available - demand[t]). This means:

  • A safety-stock violation produces a shortage signal without reducing physical inventory to zero.
  • This prevents a single safety-stock event from cascading into phantom shortages in all subsequent weeks.
  • Physical stockouts (when available < demand[t]) are reported separately via UnmetDemand exceptions.

Why TRANSFER orders are never generated by the Rust engine

The Rust transfer_pool is always [0;52]. This is a deliberate architectural choice: the Python _allocate_transfers() function owns all inter-location movement logic, using topological-sort cascading that the Rust engine cannot replicate. Pass 1 generates supply at WHs; Pass 2 gives stores their TRANSFER arrivals; the Rust engine sees them as indistinguishable from any other scheduled receipt.

Why scheduled receipts from the ERP are immutable

master.supply_orders_firm is the only source for scheduled_receipts[]. Engine-generated orders from prior runs are never re-loaded as scheduled receipts. This prevents the plan from building on its own outputs and makes each supply run fully reproducible from master data alone.

Repair route auto-sourcing

When a Repair route exists but repair_returns[] is all zero (no confirmed returns yet), the engine autofills a statistical repair forecast:

repair_returns[w] = demand[w - repair_turnaround] × return_rate × repair_yield

This ensures the engine accounts for the expected repair pipeline from day one of going live, before any actual return records exist.


Database Outputs

ClickHouse (analytics layer, partitioned by pipeline_id + scenario_id)

Table Content
PIPE_supply_orders All planned orders with qty, release_week, arrival_week, unit_cost, total_cost, status, order_type
PIPE_supply_inventory_projection 52-week stock projections: inventory, shortage, demand, supply_received, safety_stock, oh_bad, oh_new, expired_qty
PIPE_supply_pegging Demand ↔ supply peg links (PegLink): order_id, demand_week, supply_week, qty
PIPE_supply_exceptions All exceptions: type, severity, week, qty, message

All tables use archive_date partitioning. Queries filter to the latest version via MAX(archive_date) using the _latest_supply_version() helper.


Module Reference

Module Location Role
Python orchestrator files/supply_runner.py Data loading, pre/post-processing, 2-pass coordination
Rust PyO3 binding files/supply/src/lib.rs Entry points: run_supply_plan, run_supply_plan_msgpack, run_allocation
Engine orchestrator files/supply/src/engine.rs Coordinates all Rust passes; run_impl()
Data types files/supply/src/types.rs SkuPlan, RouteOption, SupplyOrder, InventoryProjection, EngineConfig
Preprocessing files/supply/src/preprocess.rs Forecast consumption + repair-return autofill
Netting files/supply/src/netting.rs Net requirements calculation (Passes 2 + 5)
Sourcing files/supply/src/sourcing.rs Route selection + lot-sizing allocate()
Capacity scheduling files/supply/src/scheduling.rs Resource scheduling, soft/hard caps
Push pass files/supply/src/push.rs Expedite and split logic
Pegging files/supply/src/pegging.rs FIFO demand-to-supply links
Transfer allocation files/supply/src/allocation.rs Kahn topo-sort cascading proportional push
Calendar files/supply/src/calendar.rs 52-bit bitmask operations
Cost files/supply/src/cost.rs CostBreakdown struct
Constraints files/supply/src/constraints.rs Hard/Soft constraint algebra, ViolationCostFn
Solver files/supply/src/solver.rs LP/constrained alternative sourcing
Alternatives files/supply/src/alternatives.rs Alternative sourcing option generation
Graph files/supply/src/graph.rs Supply network graph utilities
Supply graph files/supply/src/supply_graph.rs Extended graph for constrained planning
Explanation files/supply/src/explanation.rs Human-readable decision reason generation
API router files/api/supply_router.py FastAPI routes under /api/supply/

Cross-references: LP Transport Solver · Supply Planning (User Guide) · Advanced Supply Planning · EOQ & Lot-Sizing · MEIO Theory