HindcastDelivery¶
Definition¶
HindcastDelivery is a frozen Pydantic model that is the aggregate root of the Delivery bounded context. It holds the complete validated delivery dataset for one commodity, one ADM level (ADM0 / ADM1 / ADM2), and one generated date. It enforces cross-row structural integrity that individual DeliveryRow validators cannot enforce in isolation: absence of duplicate primary keys, and consistent init_date counts across all (year, geo_identifier) groups.
Kind¶
Aggregate root (frozen Pydantic BaseModel). One instance per ADM level per hindcast run (three instances per run in total). There is no extra="forbid" at this level; the per-row strictness lives on DeliveryRow.
Source of truth¶
delivery/schemas.py:227 — class HindcastDelivery.
Key attributes¶
| Field | Type | Notes |
|---|---|---|
rows |
list[DeliveryRow] |
All rows for this delivery CSV, in no guaranteed order |
generated_date |
str |
ISO-8601 YYYY-MM-DD — the date the delivery was produced; validated by _validate_generated_date |
Validation invariants¶
Two model validators run after construction (mode="after"), in addition to the field-level date format check.
No-duplicate-keys invariant¶
schemas.py:249 — _validate_no_duplicate_keys:
@model_validator(mode="after")
def _validate_no_duplicate_keys(self) -> HindcastDelivery:
"""No duplicate (year, init_date, geo_identifier) tuples."""
seen: set[tuple[int, str | None, str]] = set()
for row in self.rows:
key = (row.year, row.init_date, row.geo_identifier)
if key in seen:
msg = f"Duplicate delivery key: {key}"
raise ValueError(msg)
seen.add(key)
return self
Every (year, init_date, geo_identifier) triple must be unique. Duplicates arise when the walk-forward loop emits the same prediction twice (e.g. from a fold overlap bug); this validator surfaces the error at delivery time rather than silently writing duplicate rows to the CSV.
Fold-consistency invariant¶
schemas.py:260 — _validate_fold_consistency:
@model_validator(mode="after")
def _validate_fold_consistency(self) -> HindcastDelivery:
"""All (year, geo) groups must have the same init_date count."""
if not self.rows:
return self
counts: defaultdict[tuple[int, str], int] = defaultdict(int)
for r in self.rows:
counts[(r.year, r.geo_identifier)] += 1
unique_counts = set(counts.values())
if len(unique_counts) > 1:
examples = dict(list(counts.items())[:4])
msg = (
f"Fold inconsistency: not all (year, geo) groups have the same "
f"init_date count. Counts: {unique_counts}. Examples: {examples}"
)
raise ValueError(msg)
return self
The check compares init_date counts (not the date values themselves) because dates naturally differ across years. A mismatch indicates a truncated fold — for example, a model run that started but did not complete one season year. The duplicate-key validator prevents repeated (year, init_date, geo) tuples within a year, so the count invariant catches only cross-year inconsistency.
Generated-date format¶
schemas.py:239 — _validate_generated_date: field validator; rejects any value that does not match ^\d{4}-\d{2}-\d{2}$.
Lifecycle¶
- Assembly —
stages/run_deliver.pycallsdelivery/conversions.py:walk_forward_preds_to_delivery_rowsfor each configured ADM level to producelist[DeliveryRow], then wraps the list withHindcastDelivery(rows=rows, generated_date=today). - Validation — Pydantic runs field and model validators at construction. Structural failures raise
ValueErrorbefore any file is written. - Serialisation —
delivery/conversions.py:delivery_to_dataframeconverts to a Polars DataFrame in the canonical column order frombuild_delivery_column_order. - Persistence — Written to
run_dir/delivery/Treefera_{commodity}_ADM{n}_Hindcast_{YYYYMMDD}.csv. Forecast output follows a parallel path underrun_dir/forecast/{season_year}/{init_date}/delivery/. - Export —
delivery/export.py:run_exportreads the CSVs back, inverts unit conversion, resolvesgeo_identifierto warehouse UUIDs, and delegates tomodel_output_export_util.
Relationships¶
- Contains many: DeliveryRow —
rows: list[DeliveryRow] - Produced by:
stages/run_deliver.py(hindcast) andstages/run_forecast.py(forecast) - Consumed by:
delivery/conversions.py:delivery_to_dataframefor CSV serialisation;delivery/export.pyfor warehouse export - Preflight-gated by:
run/preflight.py:preflight_paths_for_export(export stage only; the deliver stage usespreflight_paths_for_hindcast)
Concepts and pipelines¶
- Delivery pipeline — full end-to-end walkthrough
- DeliveryRow — per-row schema, CI ordering invariant, field table
- Three instances per hindcast run — one for each of ADM0, ADM1, ADM2 — written to three separate CSV files under
run_dir/delivery/
PRs and commits¶
- PR-372 — made
forecast.residual_modemandatory and addedvalidate_residual_modegate; the calibration contract that populates CI bands on everyDeliveryRowin the delivery is now explicitly declared - PR-340 — exposed
nass_actual_area_weighted_allalongsidenass_actualin delivery rows; both columns are now present in everyHindcastDeliveryfor ADM0 rows
Open questions¶
HindcastDeliveryis reused for forecast output (the CSV filename changes but the container model is the same). The nameHindcastDeliveryis therefore slightly misleading for forecast delivery; a shared base or a type alias forForecastDeliveryhas not been introduced.- The fold-consistency validator checks counts only, not that the set of
init_datevalues is actually the same across all geo groups within a year. A scenario where two geos both haveNinit dates but different dates would pass the validator but represents a data anomaly.