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 only Forecast.mean while 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: object

How a demand band’s width was obtained, and what it is (and is not) entitled to claim.

guarantee is "empirical": the width was measured from residuals, and nothing here establishes the exchangeability split conformal needs, so no finite-sample coverage is asserted.

Parameters:
  • method (str)

  • guarantee (str)

  • n_calibration (int)

  • calibration_origins (int)

  • horizon_specific (bool)

  • half_width (float)

  • assumptions (tuple[str, ...])

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: Forecast

A Forecast carrying the DemandBandCalibration receipt for its band.

Parameters:
  • mean (Any)

  • lo (Any)

  • hi (Any)

  • level (float)

  • state_probs (ndarray)

  • samples (Any)

  • method (str)

  • n_draws (int)

  • mean_mcse (Any)

  • calibration (DemandBandCalibration | None)

forecast_demand(history, horizon, *, level=0.9, seed=0)[source]

Forecast horizon steps of demand beyond history, with an empirically rescaled band.

Fits a 2-state Gaussian-emission HMM to history and calls mixle.inference.forecast.forecast() for the exact state-marginal / Monte-Carlo predictive mean and band; the band width is then rescaled using mixle.inference.conformal.split_conformal()’s quantile machinery: a model fit on everything but the last ~35% of history forecasts that held-out tail, the (prediction, actual) pairs there give one constant additive half-width at level, and that half-width is placed around the mean of a second model refit on the full history.

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 level closely on well-behaved series, which is what it is for; treat it as a calibrated-looking heuristic and read .calibration for the terms.

Parameters:
  • history (Any) – past demand observations (one univariate series).

  • horizon (int) – number of future periods to forecast.

  • level (float) – central-interval mass the half-width targets (0.9 -> a ~90%-wide band).

  • seed (int) – reproducibility (both the HMM’s EM/MC internals and the calibration split).

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 - demand is the net node supply min_cost_flow() consumes directly under the given (n, n) arc cap/cost, where demand is the forecast quantity chosen by risk, scattered onto the network nodes named by demand_nodes.

Both keyword arguments are required, and deliberately have no default:

  • demand_nodes separates temporal forecast coordinates from spatial entities. A forecast from forecast_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.

  • risk names which forecast quantity is dispatched against (see RISK_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_band is a robust dispatch against the band the forecast reports, and that band’s own guarantee is whatever the forecast claims (for forecast_demand(), empirical – see DemandBandCalibration).

Parameters:
  • supply_nodes (Any) – length-n available supply per network node.

  • demand_forecast (Forecast) – a Forecast (from forecast_demand() or mixle.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