mixle.fulfillment module¶
mixle.fulfillment — demand forecasting + supply routing/dispatch (worklist H6).
Two steps, chained: forecast tomorrow’s demand honestly, then route today’s supply to meet it at
minimum cost. Renamed from mixle.distribution: “distribution” collided with mixle.dist
(the object-namespace alias for mixle.stats) – same word, unrelated meanings (probability
distribution vs. supply-chain distribution/routing).
forecast_demand() fits a small regime-switching (Gaussian-emission HMM) model directly from a
raw history series and calls the existing mixle.inference.forecast.forecast() front door for
the exact state-marginal / Monte-Carlo-emission predictive band, then empirically rescales that
band from the residuals on a held-out tail of the SAME history, which usually tracks the nominal
level better than the raw model band when the HMM is mis-specified (few states, short history,
non-Gaussian residuals, …).
That rescaling borrows split-conformal machinery but is not a split-conformal guarantee, and the
band must not be described as one. Split conformal earns its finite-sample coverage from
exchangeable calibration scores; here the calibration cases are successive horizons from a single
held-out tail, so their residuals are ordered, dependent and generally non-exchangeable, and one
constant half-width is then transferred to every future horizon of a different model refit on the
full series. Neither residual invariance across horizon nor invariance across the refit is
established. DemandBandCalibration, attached to every returned forecast, records exactly
what was done – one calibration origin, no horizon-specific evidence, guarantee="empirical" –
so a caller can see the band is an empirical heuristic rather than a certified interval. A
time-series-valid procedure (rolling/block conformal over multiple forecast origins, with
horizon-specific calibration retained) is what a coverage claim here would require.
route_distribution() then turns a forecast into a network flow problem. It takes an explicit
demand_nodes mapping from forecast coordinates to physical network nodes and an explicit risk
policy, because neither is inferable: forecast_demand() produces one value per future TIME
STEP, and a time step is not a plant/depot/customer. Routing used to require the forecast vector to
align one-to-one with supply_nodes by position and used its length as the dimension of a spatial
graph, so a horizon of four silently became four physical nodes – a valid flow for an invented
network. There is still no location-by-time demand tensor, no inventory state, no temporal transport
arc and no lead time here: this compiles ONE period’s spatial dispatch, and a genuine location x time
network with inventory and conservation semantics remains unbuilt.
>>> history = [50.0 + 10 * ((-1) ** (t // 12)) for t in range(60)]
>>> f = forecast_demand(history, horizon=4, level=0.9, seed=0)
>>> f.mean.shape
(4,)
- RISK_POLICIES: dict[str, str] = {'cover_band': 'hi', 'expected_value': 'mean'}
The dispatch risk policies
route_distribution()implements, and which forecast quantity each one actually consumes. Naming one is mandatory: reading onlyForecast.meanwhile the upstream function emphasizes a calibrated band silently discards every measure of forecast uncertainty – two forecasts with the same mean[0, 10]but upper bounds[0, 10]and[1000, 1000]produced the identical cost-10 flow, though the second can represent catastrophic unmet-demand risk.
- class DemandBandCalibration(method, guarantee, n_calibration, calibration_origins, horizon_specific, half_width, assumptions=('calibration residuals are successive horizons from one held-out tail: ordered, dependent, generally NOT exchangeable', 'one constant half-width is transferred to every horizon; residual invariance across horizon is not established', 'the half-width is measured on a model fit to history[:-k] and applied to a model refit on the full history; invariance across the refit is not established'))[source]
Bases:
objectHow a demand band’s width was obtained, and what it is (and is not) entitled to claim.
guaranteeis"empirical": the width was measured from residuals, and nothing here establishes the exchangeability split conformal needs, so no finite-sample coverage is asserted.
- class DemandForecast(mean, lo, hi, level, state_probs, samples=None, method='monte-carlo (chain-path simulation)', n_draws=0, mean_mcse=None, calibration=None)[source]
Bases:
ForecastA
Forecastcarrying theDemandBandCalibrationreceipt for its band.
- forecast_demand(history, horizon, *, level=0.9, seed=0)[source]
Forecast
horizonsteps of demand beyondhistory, with an empirically rescaled band.Fits a 2-state Gaussian-emission HMM to
historyand callsmixle.inference.forecast.forecast()for the exact state-marginal / Monte-Carlo predictive mean and band; the band width is then rescaled usingmixle.inference.conformal.split_conformal()’s quantile machinery: a model fit on everything but the last~35%ofhistoryforecasts that held-out tail, the (prediction, actual) pairs there give one constant additive half-width atlevel, and that half-width is placed around the mean of a second model refit on the fullhistory.The band is empirical, not a split-conformal interval. Its calibration cases are successive horizons from a single held-out tail – ordered, dependent, generally non-exchangeable – so the finite-sample coverage split conformal offers is not established here, and the one half-width is reused across horizons and across a model refit without evidence of invariance. Empirically the band usually tracks
levelclosely on well-behaved series, which is what it is for; treat it as a calibrated-looking heuristic and read.calibrationfor the terms.- Parameters:
- Returns:
A
DemandForecast(.mean, .lo, .hi, .level, .state_probs, .calibration).- Return type:
DemandForecast
- route_distribution(supply_nodes, demand_forecast, cost, cap, *, demand_nodes, risk)[source]
Route supply to meet forecast demand at minimum cost (H1/IC-9’s
min_cost_flow()).Compiles ONE period’s spatial dispatch:
supply = supply_nodes - demandis the net node supplymin_cost_flow()consumes directly under the given(n, n)arccap/cost, wheredemandis the forecast quantity chosen byrisk, scattered onto the network nodes named bydemand_nodes.Both keyword arguments are required, and deliberately have no default:
demand_nodesseparates temporal forecast coordinates from spatial entities. A forecast fromforecast_demand()is indexed by future TIME STEP; this function needs demand indexed by physical node. Alignment used to be implicit and positional, with the forecast’s own length taken as the network’s node count, so a horizon of four became four plant/depot/customer nodes and the solver returned a valid flow for a network nobody described.risknames which forecast quantity is dispatched against (seeRISK_POLICIES), so an expected-value dispatch is labelled as one and cannot be mistaken for having used the forecast’s calibrated coverage.
This is not a stochastic or multi-period program: there is no location-by-time demand tensor, no inventory state, no temporal transport arc, no lead time, no shortage penalty and no chance constraint.
cover_bandis a robust dispatch against the band the forecast reports, and that band’s own guarantee is whatever the forecast claims (forforecast_demand(), empirical – seeDemandBandCalibration).- Parameters:
supply_nodes (Any) – length-
navailable supply per network node.demand_forecast (Forecast) – a
Forecast(fromforecast_demand()ormixle.inference.forecast.forecast()).cost (Any) –
(n, n)per-unit arc routing cost.cap (Any) –
(n, n)arc capacity.demand_nodes (Any) – one network node index per forecast coordinate, naming which node that coordinate’s demand belongs to. Must be unique and within
range(n).risk (str) – a key of
RISK_POLICIES.
- Returns:
The resolved
mixle.relations.Flow(value= total routing cost,flow= arcs).- Return type:
Flow