Skip to content

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:227class 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

  1. Assemblystages/run_deliver.py calls delivery/conversions.py:walk_forward_preds_to_delivery_rows for each configured ADM level to produce list[DeliveryRow], then wraps the list with HindcastDelivery(rows=rows, generated_date=today).
  2. Validation — Pydantic runs field and model validators at construction. Structural failures raise ValueError before any file is written.
  3. Serialisationdelivery/conversions.py:delivery_to_dataframe converts to a Polars DataFrame in the canonical column order from build_delivery_column_order.
  4. Persistence — Written to run_dir/delivery/Treefera_{commodity}_ADM{n}_Hindcast_{YYYYMMDD}.csv. Forecast output follows a parallel path under run_dir/forecast/{season_year}/{init_date}/delivery/.
  5. Exportdelivery/export.py:run_export reads the CSVs back, inverts unit conversion, resolves geo_identifier to warehouse UUIDs, and delegates to model_output_export_util.

Relationships

  • Contains many: DeliveryRowrows: list[DeliveryRow]
  • Produced by: stages/run_deliver.py (hindcast) and stages/run_forecast.py (forecast)
  • Consumed by: delivery/conversions.py:delivery_to_dataframe for CSV serialisation; delivery/export.py for warehouse export
  • Preflight-gated by: run/preflight.py:preflight_paths_for_export (export stage only; the deliver stage uses preflight_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_mode mandatory and added validate_residual_mode gate; the calibration contract that populates CI bands on every DeliveryRow in the delivery is now explicitly declared
  • PR-340 — exposed nass_actual_area_weighted_all alongside nass_actual in delivery rows; both columns are now present in every HindcastDelivery for ADM0 rows

Open questions

  • HindcastDelivery is reused for forecast output (the CSV filename changes but the container model is the same). The name HindcastDelivery is therefore slightly misleading for forecast delivery; a shared base or a type alias for ForecastDelivery has not been introduced.
  • The fold-consistency validator checks counts only, not that the set of init_date values is actually the same across all geo groups within a year. A scenario where two geos both have N init dates but different dates would pass the validator but represents a data anomaly.