Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/notebooks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: notebooks

# A LIGHT, NON-BLOCKING drift check (issue #151): it only runs the showcase notebooks
# end-to-end to catch breakage in the frozen `views_frames` surface. It is deliberately
# separate from the core `CI` gate and `continue-on-error`, so a slow or flaky notebook
# never blocks a merge — the notebooks are un-gated dev artifacts (like research/).
on:
push:
branches: [main, development]
pull_request:
branches: [main, development]

jobs:
nbmake:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv python install 3.12
- run: uv sync --python 3.12 --extra docs
# cwd = notebooks/ (working-directory) so each notebook's `import _synthetic`
# resolves to the in-repo helper next to it; the path arg is therefore `.` (the
# notebooks live directly in this dir — `notebooks/` here would mean notebooks/notebooks/
# and collect nothing). The notebooks ship with cleared outputs and re-execute here.
- run: uv run --python 3.12 pytest --nbmake .
working-directory: notebooks
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,62 @@ All notable changes to `views-frames` are documented here. The format is based o
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
[Semantic Versioning](https://semver.org/) as governed in `GOVERNANCE.md`.

## [1.8.0] — 2026-06-28

**Native point-country broadcast in `views_frames_reconcile` (ADR-023 amendment, #143 / Epic #142),
the three showcase notebooks (Epic #166), and a governance/test hardening pass (Epic #179).** All
additive — the frozen leaf and summarize public surface are unchanged, and the hardening work makes
**no `src/` behaviour change**; `CONFORMANCE_FLOOR` stays `1.0.0`.

### Added
- **`ReconciliationModule.reconcile` accepts a point country** (`cm.sample_count == 1`) against a draws
grid (`pgm.sample_count == S`): the point is broadcast across the `S` draws inside the orchestrator
(`np.tile`), so callers no longer tile it themselves (the DRY home of pipeline-core's WET
`align_country_to_grid`, #143). The **aligned-draws** path (`cm.sample_count == S`) is byte-for-byte
unchanged; any other count still fails loud.
- **`ReconciliationModule.reconcile_result(cm, pgm) -> ReconciliationResult`** (#144) — reconciles and
**reports the mode** (`POINT_BROADCAST` | `ALIGNED_DRAWS`) + method (`proportional`) on a returned
`ReconciliationResult`. The mode is *returned*, never stamped on the leaf's generic `FrameMetadata`
(ADR-020 / register C-47 — the numpy leaf carries no reconciliation vocabulary). `reconcile` is
unchanged (it returns `reconcile_result(...).frame`). New public names: `ReconciliationResult`,
`POINT_BROADCAST`, `ALIGNED_DRAWS`, `METHOD_PROPORTIONAL`.

### Notes
- The broadcast lives entirely in `views_frames_reconcile/module.py`; the leaf `proportional` and the
parity-frozen `grouping` hot loop are untouched, so the torch-oracle parity is exact (0.000e+00).
- The aligned-draws mode remains the documented per-draw approximation. **ADR-024** (#145) records the
design direction + deferral for the principled joint upgrade (and corrects `proportional.py`'s
ambiguous "C-37" reference; register C-62). Design-only — no code.

### Documentation
- **Three showcase notebooks** (`notebooks/01_frames`, `02_summaries`, `03_reconciliation`; Epic #166):
public-frozen-API-only, synthetic-data teaching notebooks for the frames contract, the posterior
summaries (with a calibration/coverage + PIT panel), and reconciliation — including a
bit-identity-≠-method-quality panel and a toy-lattice spatial view (register C-59/C-60/C-61).
- **`docs/CICs/Reconcile.md`** (Epic #179) — the package-level Class Intent Contract for
`views_frames_reconcile` (§1–§11): the sum-to-country / zero-preservation / non-negativity /
de-mutation guarantees, the point/aligned **mode** contract, the five fail-loud validation guards
+ the per-draw-approximation caveat, and the green/beige/red test alignment. The reconcile package
was the last non-trivial surface without a CIC (ADR-006); **register C-64 resolved**.
- **ADR-025 — value-buffer immutability is by convention; only the index is enforced** (Epic #179).
Corrects the "immutable value objects" contract (the three frame CICs §9/§3 + README design
principle 2) to match the code: the index (`time`/`unit`) is `setflags(write=False)`-enforced; the
value buffer is immutable *by convention* (left writeable to preserve zero-copy / `mmap` — mutating
`.values` in place is unsupported). The `setflags`-enforce on `.values` would be a MAJOR
("tightening an invariant" on a frozen-surface member, GOVERNANCE/ADR-018), so it is recorded as a
**deferred MAJOR-rider**, not done now; **register C-63 resolved** (contract corrected).

### Tests
- **Adversarial (red) test hardening** (Epic #179), no `src/` change, 100% line+branch coverage held:
- the non-finite (NaN / ±inf) fail-loud guard in `exceedance`/`expected_shortfall` is now pinned on
the **blocked (multi-block) path** — the bad draw placed in a non-first block via the `block_rows`
kwarg with block 0 all-finite (**register C-65 resolved**);
- **conformance-suite negatives** — `assert_reconcile_contract` and `assert_summarizer_contract` are
shown to reject a deliberately non-conforming implementation (the leaf's C-51 envelope-negative
pattern, extended to the sibling packages);
- **reconcile mode-corners** — `reconcile_result.mode` for both-points and pre-tiled-cm inputs (both
`ALIGNED_DRAWS`); and **`ReconciliationResult` frozen-ness** (`FrozenInstanceError`).

## [1.7.0] — 2026-06-26

**Forecast reconciliation is a third sibling package (ADR-023, Epic 11).** A new importable
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,13 @@ makes it safe to depend on from everywhere (SDP).
3. **Immutable value objects.** A frame is validated at construction and then
treated as read-only. Operations (`collapse`, `select`, `with_metadata`)
**return new frames**; they never mutate in place. (Directly forbids the
C-184 cross-repo-mutation anti-pattern.) **Copy-vs-view:** structural and
C-184 cross-repo-mutation anti-pattern.) **Enforced for the index, by
convention for the value buffer:** the identifier arrays (`time`/`unit`) are
write-protected (`setflags(write=False)`); the `values` buffer is left
**writeable on purpose** — so structural ops can share it zero-copy — and is
immutable *by convention*: mutating `.values` in place is unsupported and may
silently corrupt buffer-sharing frames (ADR-025 / register C-63; enforcing it
is a deferred MAJOR-rider). **Copy-vs-view:** structural and
metadata-only operations (`with_metadata`, contiguous `select`) return frames
that **share** the underlying `values` buffer (numpy view / zero-copy), and a
`mmap`-backed frame stays `mmap`-backed — a new frame must never copy a
Expand Down
23 changes: 23 additions & 0 deletions docs/ADRs/023_reconciliation_is_a_sibling_package.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

**Status:** Accepted
**Date:** 2026-06-26
**Amended:** 2026-06-27 — sample-count contract (point-broadcast vs aligned-draws), v1.8.0, #143
**Deciders:** VIEWS platform maintainers
**Consulted:** views-postprocessing (the current, mis-homed host), views-reporting (the frozen torch oracle)
**Informed:** views-pipeline-core, views-models, views-evaluation
Expand Down Expand Up @@ -68,6 +69,19 @@ any `views_*` except `views_frames` (and `views_frames_summarize` only if a real
> map_vals)` arrays. `views_frames_reconcile` owns the *operation*; it never embeds or fetches geography. This
> is asserted in the conformance suite, not just documented.

### Sample-count contract: point-broadcast vs aligned-draws (amended 2026-06-27, #143)

`ReconciliationModule.reconcile` accepts a country (`cm`) frame whose `sample_count` is either **`1`**
(a *point* forecast) or **`S`** (the grid's draw count); any other count fails loud in validation. A point
`cm` is **broadcast** to `S` draws by tiling its single column (`np.tile`) inside the orchestrator
(`module.py`), *before* the unchanged aligned-draws reconcile path runs — so the equal-count path stays
**bit-for-bit identical** (the broadcast triggers only for `sample_count == 1`; the leaf `proportional` and the
parity-frozen `grouping` hot loop are untouched). This is the DRY home of pipeline-core's WET
`align_country_to_grid` (the consumer explicitly earmarked it for here, #143). The **aligned-draws** case
remains the documented *per-draw approximation*: pairing grid-draw `s` with country-draw `s` across
independently-trained models has no shared draw identity — the principled joint upgrade is a separate design
(#145; the pragmatic-vs-principled boundary the `proportional` docstring names).

### Import-DAG

`views_frames_reconcile → {views_frames}` (+ numpy / stdlib). Added to `ALLOWED_INTERNAL` in
Expand All @@ -78,6 +92,11 @@ unchanged.

A new sibling package is **additive ⇒ MINOR**: **1.6.0 → 1.7.0**. `CONFORMANCE_FLOOR` stays `1.0.0`.

The 2026-06-27 amendment (the point-broadcast sample-count contract, #143) is an **additive input-contract
relaxation** — `reconcile` accepts a new input shape (`cm.sample_count == 1`) it previously rejected, and no
existing call changes ⇒ **MINOR: 1.7.0 → 1.8.0**. `CONFORMANCE_FLOOR` stays `1.0.0` (the frozen leaf surface
is untouched; the broadcast lives entirely in `views_frames_reconcile`).

---

## Rationale
Expand Down Expand Up @@ -177,6 +196,10 @@ module*, never a modification of the proportional method.
story, only after parity is locked.
- The principled probabilistic-reconciliation method (views-postprocessing C-37) — a future *sibling module*,
never a modification of `proportional`.
- **Reconciliation-mode provenance (#144):** the mode (`point-broadcast` vs `aligned-draws`) is **returned**
on a `ReconciliationResult` from `reconcile_result`, **not** stamped on the leaf's generic `FrameMetadata`
— that header is governed *generic-only* (ADR-020 / register C-47), so reconciliation vocabulary stays in
the sibling package, off the numpy leaf (decision recorded as register D-12).

---

Expand Down
172 changes: 172 additions & 0 deletions docs/ADRs/024_principled_joint_reconciliation_design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@

# ADR-024: Principled joint probabilistic reconciliation — design direction & deferral

**Status:** Accepted (design direction; implementation deferred)
**Date:** 2026-06-27
**Deciders:** VIEWS platform maintainers
**Consulted:** views-models (the country/grid model owner), views-postprocessing (the C-37 lineage)
**Informed:** views-pipeline-core, views-evaluation

---

## Context

`views_frames_reconcile.reconcile_proportional` makes grid (`pgm`) forecasts sum to their
country (`cm`) total by **top-down proportional disaggregation, applied per posterior draw**:
within draw `s`, each grid cell keeps its relative share and the cells are rescaled so they
sum to draw `s` of the country total. It is a *faithful, parity-proven port* and the right
**pragmatic baseline** — but its own docstring names it "a pragmatic per-draw approximation,
not principled joint probabilistic reconciliation."

Two things make it an approximation, and they are *not* fixed by more samples:

1. **No shared draw identity.** Pairing grid-draw `s` with country-draw `s` is only meaningful
if the two are *the same scenario*. When the grid and country models are trained
**independently** (the current platform reality — `rusty_bucket` and a separate country
model), draw index `s` carries **no shared meaning** across them; the pairing is arbitrary,
even though the arithmetic runs and the output looks reasonable.
2. **Dependence is discarded.** Proportional rescaling ignores the cross-cell error structure
a principled reconciler exploits (MinT) and does not reconcile the *joint* distribution
(probabilistic reconciliation) — so the reconciled joint tails (the thing FAO-style
decisions key on) are not guaranteed calibrated.

This matters **now** because the reconciliation path is live (pipeline-core #233 wired the
frames-native ensemble) and a UN/FAO deliverable consumes reconciled grid→country draws. We
need the upgrade path *written down* — what "principled" requires and when it's worth
building — so the known approximation is **planned debt, not silent debt**. This ADR is the
design; it deliberately ships **no code**.

---

## Decision

1. **The draw-identity contract is the gate.** Principled joint reconciliation requires a
defined notion of *"grid-draw `s` and country-draw `s` are the same scenario."* That holds
only when the grid and country posteriors come from a **shared generative process** — shared
ensemble members / shared seeds, or an explicit **copula coupling**. Absent a declared
shared draw-identity, per-draw pairing is not principled and must not be presented as such.

2. **When built, it is a new sibling module, not a change to `proportional`.** Consistent with
ADR-023's open question, the principled method (probabilistic reconciliation —
Panagiotelis et al. 2023; or MinT — Wickramasuriya et al. 2019) lands as a **new module in
`views_frames_reconcile`** behind the existing injected-mapping interface. `proportional`
stays the documented baseline; nothing about its parity-frozen behaviour changes.

3. **Implementation is deferred** until *both* preconditions hold: (a) a consumer needs
calibrated **joint** country tails (proportional's marginal-rescale is demonstrably
insufficient for its decision), **and** (b) the country model can **supply what the method
needs** — either draws carrying a declared shared draw-identity, or a covariance/coupling the
reconciler can consume. Until then, per-draw proportional remains the shipped method,
labelled as the approximation it is (the `reconcile_result` mode `aligned-draws`, D-12).

**In scope:** the design direction, the draw-identity contract, and the deferral preconditions.
**Out of scope:** any implementation; choosing the *specific* method (that is the future epic's
call, driven by what the country model can supply); the country model's own changes
(views-models); changing `proportional`.

---

## Rationale

Correctness-of-method is a *country-model-shaped* problem, not a reconciler-shaped one: the
reconciler cannot manufacture a joint distribution that the upstream models never shared. So the
honest, lowest-regret move is to **name the precondition** (draw-identity / coupling) and defer
until it can be met, rather than ship a more elaborate method that still pairs independent draws
and merely *looks* principled. WET-before-DRY's sibling logic applies again: keep the baseline,
add the principled method beside it only when its inputs exist — never by mutating the
parity-frozen `proportional`.

---

## Considered Alternatives

### Alternative A: keep proportional as the only method (status quo)
- **Pros:** simplest; parity-proven; no new inputs required.
- **Cons:** information-losing; per-draw pairing of independent draws is not principled; joint
tails not guaranteed calibrated.
- **Revisit when:** a consumer's decision provably needs calibrated joint country tails.

### Alternative B: MinT (trace-minimization; Wickramasuriya 2019)
- **Pros:** minimum-variance unbiased *linear* reconciliation; well-established.
- **Cons:** needs an estimate of the base-forecast **error covariance** the platform does not
currently produce; still a point/linear view, not a full joint posterior.
- **Revisit when:** reconciliation residuals / a covariance estimate become available.

### Alternative C: copula / shared-ensemble coupling
- **Pros:** models grid↔country dependence explicitly; can deliver a calibrated joint.
- **Cons:** heaviest; needs a fitted copula or a genuinely shared ensemble — a country-model
contract change.
- **Revisit when:** the country and grid models share members, or a coupling is fitted upstream.

### Alternative D: marginal (non-per-draw) reconciliation
- **Pros:** honest when draws are *not* shared — reconcile the country **marginal**, don't fake
per-draw identity.
- **Cons:** loses the per-draw coherence downstream summaries (joint `expected_shortfall`, etc.)
rely on.
- **Revisit when:** a consumer wants marginal-only coherence.

---

## Consequences

### Positive
- The known approximation becomes **planned debt with a named precondition**, not a silent one.
- The upgrade path is fixed (new sibling module, injected interface unchanged) so a future epic
is scoped, not exploratory.
- Keeps `proportional` parity-frozen and the leaf untouched.

### Negative
- No improvement now; per-draw proportional persists as the shipped method (documented).
- The hardest dependency (a shared draw-identity / coupling from the country model) is *upstream*
and outside this repo's control — the deferral may be long.

---

## Implementation Notes

**No code in this ADR.** When the preconditions are met, a future implementation epic:
- adds a sibling module under `src/views_frames_reconcile/` (e.g. `probabilistic.py`) behind the
same injected-mapping interface; does **not** modify `proportional`/`grouping`;
- requires the country model (views-models) to declare the draw-identity / supply the coupling —
a cross-repo contract, gated there;
- ships its own parity/validation suite (below) and is additive/MINOR; `CONFORMANCE_FLOOR` stays
`1.0.0`.

This ADR's only repo change is documentation: `proportional.py`'s docstring is corrected to point
at **this ADR** (the prior "tracked as C-37" was ambiguous — views-frames' own C-37 is an
*unrelated, resolved* protocol-conformance item; the principled-reconciliation lineage is
*views-postprocessing* C-37). Tracked as register **C-62**.

---

## Validation & Monitoring

A principled implementation must demonstrate what proportional cannot: **calibrated joint
coverage** — e.g. the reconciled country total's predictive intervals cover at their nominal
rate against held-out actuals, and the joint grid→country dependence is preserved (not just the
per-cell marginals). That coverage check (on synthetic data with a known joint truth) is the
acceptance gate for the future epic, and the signal that would justify lifting the deferral.

---

## Open Questions

- **Which method** (probabilistic reconciliation vs MinT vs copula) — undecided on purpose; it
depends on what the country model can supply (draws + identity, or a covariance, or a coupling).
- **Where draw-identity is declared** — a country-model (views-models) contract; what does the
reconciler require as its injected interface?
- **Is marginal reconciliation (D) a useful interim** for consumers that only need marginal
coherence, shipped before the full joint method?

---

## References

- Wickramasuriya, Athanasopoulos & Hyndman (2019), *Optimal forecast reconciliation* (MinT).
- Panagiotelis, Gamakumara, Athanasopoulos & Hyndman (2023), *Probabilistic forecast
reconciliation*.
- Hyndman et al. (2011), *Optimal combination forecasts for hierarchical time series*.
- `src/views_frames_reconcile/proportional.py` (the per-draw approximation / C-37 anchor);
ADR-023 (reconciliation sibling; Open-Questions deferring this as a future sibling module).
- GH #145 (this story), #142 (epic); views-pipeline-core #233 / C-198 / C-200b; views-postprocessing
C-37 (the cross-repo principled-reconciliation lineage); register C-62, D-12.
Loading
Loading