Skip to content

Routes — Complete Reference

Status: Implemented (USE-route rollup updated 2026-08; coverage matrix rollout in progress). Primary sources: files/indirect/indirect_demand.py, files/bad_forecast/runner.py, files/supply_runner.py, files/supply/src/types.rs, files/DDL/schema.sql

Routes are the backbone of Mirabelle's supply-network model. Every physical or logical flow of stock between two locations — purchasing, transfer, repair, return, deployment — is a row in master.route, typed by master.route_type. Routes drive four distinct subsystems:

  1. Demand rollup — derived (indirect) forecasts propagated along lanes
  2. Bad / repair forecasting — reverse-logistics projections
  3. Forecast netting — firm-order and sales-return consumption
  4. Supply planning & MEIO — order generation, sourcing, safety stock

Audience

Developers and implementation consultants who need the exact semantics of each route type: direction, stock-type conversion, lead-time offset sign, and which split field (quota vs coverage) applies.


Table of Contents


Data Model

master.route_type

Column Description
id PK
name Display name (e.g. "Transfer", "Use (factory good → end-consumer bad)")
planning_type The semantic key. One of BUY, MAKE, TRANSFER, REPAIR, RETURN, BALANCING, USE, ZONE_BALANCING, REPLENISH
source_type Engine-facing category used by the supply solver

All route behaviour is keyed off planning_type — never off location types or hardcoded site roles. The same algorithm works for any network (distribution, pooled-asset, MRO) because only the route types matter.

master.route

One row per (item, destination, type, source) lane. See the full field reference below.

master.route_quota_override

Time-varying quota overrides. The supply runner uses the latest override whose effective_date <= planning_today(), falling back to master.route.quota.


Direction Convention

Every route is directional:

Column Role
site_id Destination — the location that receives stock / bears demand
source_site_id Source — the location that ships / supplies
item_id Item at the destination
source_item_id Item at the source (NULL = same item)

A TRANSFER route (site_id=84, source_site_id=80) means: site 84 is replenished by site 80. Demand at 84 pulls supply from 80.


Route Types & Stock Conversion

Each planning type defines a stock-type conversion between the source pool and the destination pool, and whether the route is a push or pull for rollup purposes:

planning_type Flow Stock conversion Rollup direction Offset
BUY External supplier → site external → good — (leaf supply) −LT (release earlier)
MAKE / BUILD Components → assembly components → good BOM explosion −LT
TRANSFER WH/store replenishment good → good pull (coverage) −LT
REPLENISH Same as TRANSFER (engine-level) good → good pull (coverage) −LT
USE Producer/factory → consumer site good → bad push (quota) +LT
RETURN Consumer → depot/repair facility bad → bad push (quota) +LT
REPAIR Repair shop → demand site bad → good push (quota) +LT (TAT)
BALANCING Intra-zone excess redistribution good → good engine-level −LT
ZONE_BALANCING Inter-zone excess-to-need good → good engine-level −LT

The stock_type column on the route ('good' | 'bad') declares which pool the route draws from; it defaults to 'good' and is set to 'bad' for return/bad-balancing lanes.


Quota vs Coverage

These are two different split semantics, and they must not be conflated:

Quota (push) Coverage (pull)
Question answered "Of what leaves this source, what fraction goes to each destination?" "Of what this destination needs, what fraction comes from each source?"
Normalisation group All routes from the same (item_id, source_site_id) All routes into the same (item_id, site_id)
Sums to ≤ 1.0 across destinations (residual stays at source) ≤ 1.0 across sources (residual = uncovered demand)
Used by USE, RETURN, REPAIR, BALANCING TRANSFER / REPLENISH (demand rollup)
Column master.route.quota master.route.coverage (planned — see status note)

Example (GPS pooled-asset network):

  • Producer Hannover (84) has USE routes to Michelin (82) and Bridgestone (83). quota splits Hannover's good outflow: e.g. 0.6 / 0.4. Normalised per source.
  • Hannover itself is replenished by Rotterdam DC (80) and Frankfurt DC (81). coverage splits Hannover's inbound need: e.g. 0.5 / 0.5. Normalised per destination.

Implementation status

  • USE routes use quota, normalised per source — implemented.
  • TRANSFER rollup currently reads quota and keeps only the best-priority route per destination. Migration to a dedicated coverage column with per-destination normalisation (all sources participate) is in progress.

When all split values in a normalisation group are NULL, the group falls back to an equal split (1/n per route).


The Demand Rollup

The rollup answers: which locations get a real statistical forecast, and which get a derived one?

Rule: a real time-series forecast exists only where actuals are recorded (master.demand_actuals). Every other node's forecast is derived by walking the route network from those roots. In a pooled-asset network the actuals live at the producer (the real client); in a classic distribution network they live at the stores/depots. The algorithm is identical — it is driven purely by where actuals exist and which route types connect the nodes.

flowchart LR
    subgraph actuals["Real forecast (from actuals)"]
        P["Producer / demand leaf<br/><b>GOOD forecast</b><br/>netted with firm orders"]
    end
    subgraph upstream["Upstream supply nodes"]
        D["Depot / warehouse<br/><b>GOOD indirect forecast</b><br/>fcst × coverage, offset −LT"]
    end
    subgraph consumer["Consumer sites"]
        M["Manufacturer / consumer<br/><b>BAD forecast</b><br/>fcst × quota, offset +LT<br/>netted with sales returns"]
    end
    D -- "TRANSFER (good→good)<br/>coverage, −LT" --> P
    P -- "USE (good→bad)<br/>quota, +LT" --> M
    M -- "RETURN (bad→bad)<br/>quota × (1−wash), +LT" --> D
    D -- "REPAIR (bad→good)<br/>quota × (1−wash) × yield, +TAT" --> P

1. Transport / Replenishment rollup (good → good)

The destination's good forecast is rolled up to each source so the source can size its safety stock:

source_fcst[t − LT]  +=  dest_fcst[t] × coverage
  • Offset is negative — the source must release stock LT weeks before the destination needs it.
  • Implemented as demand_type='good_replenishment' in PIPE_indirect_demand (multi-level: rolls up through successive Transfer hops until no further source).
  • Purpose: the resulting indirect forecast at the supply node sizes its service level / safety stock.

2. USE push (good → bad)

A USE route is a push lane: the destination (consumer/manufacturer) receives the source's good forecast as a bad forecast:

bad_fcst_dest[t + LT]  +=  good_fcst_src[t] × quota
  • Offset is positive — bad units appear at the consumer only after deployment and consumption (lead_time + detention_time + transit_time).
  • The bad item at the destination is resolved via the RETURN routes leaving that site (codebase convention for "the dirty item at a site").
  • Implemented as demand_type='use_return' in PIPE_indirect_demand. return_rate is not part of this formula — it is a RETURN-route concept.

3. RETURN (bad → bad)

RETURN routes bring bad material back to the depot/repair facility:

bad_fcst_dest[t + LT]  +=  demand_src[t] × quota × (1 − return_wash_rate)
  • Wash rate = fraction of returned units lost in transit.
  • Quotas from the same source are normalised to sum to 1.
  • Implemented in files/bad_forecast/runner.py → PIPE_supply_forecast (demand_type='bad'). Sources include stat + causal + maintenance + indirect demand, so the bad forecast accounts for all demand streams.

4. REPAIR (bad → good)

Repair routes convert bad stock back into good supply at the demand site:

repair_fcst[t + TAT]  +=  bad_fcst[t] × quota × (1 − repair_wash_rate) × repair_yield
  • Also in PIPE_supply_forecast (demand_type='repair').
  • Consumed by the supply engine as incoming supply (repair_returns) and by netting as an audit column (not subtracted — the engine already credits it).

Netting integration

  • Firm (good) demand orders are registered where the actuals live (is_forecast=FALSE rows) and consume the good forecast there.
  • Sales returns (master_sales_return, status confirmed) are registered at the consumer site (USE destination) and consume the bad forecast (return_consumed_qty / return_netted_qty in PIPE_forecast_netting).

Pipeline Integration

Stage Route types consumed How
indirect-demand step TRANSFER, USE network (historical rollup, no LT shift), good_replenishment (forecast rollup, −LT × split), use_return (good→bad push, +LT × quota), kit_make (BOM)
bad-forecast step (7c) RETURN, REPAIR PIPE_supply_forecast rows (bad, repair)
netting step — (reads the above) Firm orders consume good forecast; sales returns consume bad forecast; repair credited as supply
supply step ALL Routes embedded into SKU plans as RouteOptions; Rust engine picks sources, respects LT, lot sizing, calendars, capacity
meio step RETURN, REPAIR return_rate, repair_yield, TAT mean/CV, wip_qty loaded via _load_repair_params_from_routes (NOT from segment params)
MEIO/supply guardrails USE, REPAIR Proactive repair-balance pre-pass boosts wash-depot SS floors

Lead-time composition

Context Formula
Forward supply (engine) lead_time + transit_time + pick_pack_time
USE bad-forecast offset lead_time + detention_time + transit_time
RETURN bad-forecast offset lead_time + transit_time + pick_pack_time
REPAIR offset (TAT) lead_time + transit_time + pick_pack_time

All day values are converted to ceiling weeks (ceil(days/7)).


Rust Engine — OrderType & RouteOption

The supply engine (files/supply/) maps planning_type to a fixed OrderType enum. The index order is contractual — Python _ORDER_TYPE_IDX must match:

Index OrderType planning_type
0 Buy BUY
1 Build MAKE
2 Repair REPAIR
3 Transfer TRANSFER
4 Balancing BALANCING
5 Replenish REPLENISH
6 Return RETURN
7 Use USE
8 ZoneBalancing ZONE_BALANCING

These indices key the per-SKU arrays latest_firm_by_type: [u8; 9] and min_lt_by_type: [u8; 9] (firm-order block horizon / min lead-time gating in sourcing.rs). Changing the enum length without updating the Python serializers breaks msgpack decoding (invalid length 8, expected an array of length 9).

Each route is serialised into a RouteOption (files/supply/src/types.rs):

Field From route column Notes
route_id id
source_type route_type.source_type engine category
source_location_id source_site_id
lead_time lead_time + transit_time + pick_pack_time weeks
unit_cost / order_cost unit_cost / order_cost
min_qty / mult_qty / max_qty lot sizing
capacity_id capacity_id links to pipe_supply_capacity resource
shipping_calendar / receiving_calendar ship_calendar_id / lead_time_calendar_id 52-week bitmasks
repair_yield repair_yield 0 = not repairable (None-safe)
return_rate return_rate REPAIR/RETURN demand fraction
repair_turnaround lead_time REPAIR TAT
priority priority lower wins
quota quota (+ route_quota_override) engine order-splitting
stock_type stock_type 'good' / 'bad' pool
detention_time detention_time USE-route detention
setup_cost / expedite_cost / transport_mode same costing

Field Reference — master.route

Column Type Meaning
id BIGINT PK
item_id, site_id BIGINT Item + destination
source_item_id, source_site_id BIGINT Item + source (NULL = same item / external)
supplier_id BIGINT External supplier (BUY)
type_id BIGINT FK → route_type (carries planning_type)
tag TEXT Free label
quota DOUBLE Push split from source (USE/RETURN/REPAIR/BALANCING); engine order split
coverage DOUBLE (planned) Pull split into destination (TRANSFER rollup)
priority SMALLINT Route selection order (lower wins)
lead_time SMALLINT Base lead time (days)
pick_pack_time, transit_time, inspection_time SMALLINT LT components (days)
safety_lead_time, ptf SMALLINT Planning time fence / safety LT
*_calendar_id (8 FKs) BIGINT Per-component working calendars → 52-week bitmasks
yield DOUBLE Process yield (MAKE)
unit_cost, order_cost DOUBLE Costing
min_qty, mult_qty, max_qty DOUBLE Lot sizing
end_date DATE Route expiry (NULL = open)
return_rate DOUBLE RETURN/REPAIR: fraction of demand that returns
repair_yield DOUBLE REPAIR: fraction repaired successfully (0 = not repairable)
return_wash_rate DOUBLE RETURN transit loss (0–1)
repair_wash_rate DOUBLE REPAIR process loss (0–1)
lead_time_cv DOUBLE LT coefficient of variation (MEIO bootstrap)
wip_qty DOUBLE Repair shop work-in-progress
capacity_id BIGINT → pipe_supply_capacity.resource_id
detention_time SMALLINT USE-route detention component
stock_type TEXT 'good' / 'bad' pool drawn
transport_mode TEXT Air / Sea / Road / Rail label
is_expedite, expedite_cost — Expedite premium
setup_cost, scrap_cost, diagnostic_cost DOUBLE Additional cost terms

Key Files

File Role
files/indirect/indirect_demand.py Demand rollup: network, good_replenishment, kit_make, use_return
files/bad_forecast/runner.py RETURN → bad forecast, REPAIR → repair forecast (PIPE_supply_forecast)
files/netting/forecast_netting.py Firm-order / sales-return consumption of good & bad forecasts
files/supply_runner.py Route loading, RouteOption embedding, _ORDER_TYPE_IDX, min_lt_by_type, latest_firm_by_type, proactive repair-balance pre-pass
files/supply/src/types.rs OrderType (9 variants), RouteOption, SkuPlan
files/supply/src/sourcing.rs Route selection, LT/calendar gating, firm-block horizon
files/meio_runner.py _load_repair_params_from_routes — MEIO repair params from RETURN/REPAIR routes
files/DDL/schema.sql master_route, master_route_type, route_quota_override DDL
files/api/master_data_router.py Route grid (Settings → Master Data → Routes)