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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,34 @@ 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.10.0] — 2026-07-27

**The dense-grid fill primitive (ADR-026) — unblocks pandas-free FAO ingestion (#203).**
Additive MINOR; `CONFORMANCE_FLOOR` stays `1.0.0`.

### Added
- **`frame.reindex_fill(other, *, fill_value)`** on all three sibling frames
(`PredictionFrame`/`FeatureFrame`/`TargetFrame`, WET per ADR-011): align to `other`'s
rows with **no** superset requirement — present rows pass through **bit-exact**, absent
rows get the caller's `fill_value` broadcast across the trailing axes (`NaN` legal;
keyword-only and required — no silent default, ADR-009). The result's index **is**
`other`; metadata (and `feature_names`) preserved. Owns the `-1`-sentinel scatter once:
a consumer hand-rolling `values[pos]` silently picks the *last* row for absent cells.
Inherits the C-21 row-uniqueness stance (unique rows assumed in *self*; duplicate
target rows allowed and repeat).
- **`SpatioTemporalIndex.cartesian(times, units, level)`**: the dense product-index
constructor — every `(time, unit)` combination in canonical **time-major** order, from
**explicit arrays only** (deriving them, e.g. "units of the last time step", is
consumer policy). Fails loud on duplicated input values (a duplicated product input
manufactures duplicate rows → undefined same-level joins, C-21).
- **`assert_reindex_fill_law`** in the published conformance suite (ADR-016): result
index equals the target row-for-row; present rows bit-exact; absent rows equal the
fill (NaN-safe); on a superset frame the fill degenerates to `reindex`.
- Consumer note: faoapi's `dense_grid.py` can now delegate (its last-step-entity rule +
C-87 dropped-entity check stay consumer-side) and drop its pandas implementation
(faoapi #242). Densification allocates the full dense buffer — a deliberate, costly
act at grid scale (documented on both symbols).

## [1.9.0] — 2026-07-24

**The tower-tip MAP reads the top floor (ADR-019 Amendment 3): `tip_mass` 0.5 → 0.25.**
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ at the **root of the platform dependency DAG**, plus two sibling operation packa
in the same wheel. numpy only; depends on nothing internal; every other repo
depends *toward* it.

> **Status:** **released — v1.8.x on PyPI**, public API **frozen since v1.0.0**
> **Status:** **released — v1.10.x on PyPI**, public API **frozen since v1.0.0**
> (ADR-018; everything after is additive, `CONFORMANCE_FLOOR` stays `1.0.0`).
> Consumers install `views-frames` and validate against the published conformance
> suite (`views_frames.conformance`, ADR-016). See `CHANGELOG.md` for the release
Expand Down Expand Up @@ -90,7 +90,7 @@ CI additionally gates 100% line+branch coverage

## Governance

Constitutional ADRs 000–010, project ADRs 011–025, CICs for every non-trivial
Constitutional ADRs 000–010, project ADRs 011–026, CICs for every non-trivial
surface (7 active incl. the package-level `Summarize.md` and `Reconcile.md`),
contributor protocols, and standards live in `docs/`. The technical risk register
(`reports/technical_risk_register.md`) is the curated concern/decision log. Run
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
> containers (`FeatureFrame`, `PredictionFrame`, and their anticipated siblings)
> that every other repo depends on and that depends on nothing internal.
>
> **Status:** **v1.9.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
> **Status:** **v1.10.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
> v1.1 surface is
> purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
> `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
Expand All @@ -18,7 +18,10 @@
> ADR-025 immutability-by-convention, the adversarial red-test batch; v1.9.0 moves the
> tower-tip MAP to the **top floor** of the published tower — `tip_mass` 0.25 with the
> **MAP-containment law** in the conformance suite, ADR-019 Amendment 3 — and adds the
> `research/figures/` tower-figure toolkit). This
> `research/figures/` tower-figure toolkit; v1.10.0 adds the **dense-grid fill**
> primitive — `reindex_fill(other, *, fill_value)` on all three frames +
> `SpatioTemporalIndex.cartesian` + the published `assert_reindex_fill_law`, ADR-026,
> unblocking pandas-free FAO ingestion). This
> README is the design
> bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
> io, conformance suite) plus the `src/views_frames_summarize/` sibling package
Expand Down Expand Up @@ -314,7 +317,12 @@ genuinely reused core. Build this once:
`(time, unit)` **at a single `SpatialLevel`**. **This is the label-alignment
that today drags pandas back in** — pred↔actual join, partial-overlap
evaluation, same-level reindex. This alignment logic lives in the leaf
unconditionally.
unconditionally. The dense-grid pair rides the same join (ADR-026):
`cartesian(times, units, level)` builds the time-major product index
(explicit arrays only — deriving them is consumer policy), and the frames'
`reindex_fill(other, *, fill_value)` aligns with **no** superset requirement,
filling absent rows (present rows bit-exact; law-pinned by
`assert_reindex_fill_law`).
- **Cross-level operations (`cross_level_align`) — protocol here, data injected.**
The cm↔pgm **cross-level join** (country↔grid) is **not** a same-axis set op; it
is a one-to-many lookup against a `priogrid→country` mapping that is **injected**
Expand Down
83 changes: 83 additions & 0 deletions docs/ADRs/026_dense_grid_fill_primitive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# ADR-026: Dense-grid fill is a leaf primitive (`reindex_fill` + `cartesian`)

**Status:** Accepted
**Date:** 2026-07-27
**Deciders:** VIEWS platform maintainers
**Consulted:** expert-code-review (2026-07-27, design review of issue #203)
**Informed:** views-faoapi (ingestion, #242), views-pipeline-core, views-postprocessing

---

## Context

faoapi's ingestion still builds its dense forecast grid with pandas, and its docstring
names the reason explicitly: *"views-frames has no fill primitive"*
(`views-faoapi/.../forecast/ingestion/dense_grid.py:3`). That is the last hard blocker on
a pandas-free FAO ingestion edge (faoapi #242; filed here as #203). The operation itself —
align a sparse frame to a complete `(time × unit)` target index and fill absent cells —
is **same-level structural alignment**, the responsibility category `SpatioTemporalIndex`
already owns (`select`/`reindex`/`searchsorted`/`intersect`). It is not an adapter, not
geography, and not policy: it touches only values + identifier arrays.

The machinery half-exists: `searchsorted` returns **-1 for absent rows** (F4-hardened for
the empty index), and frame-level `reindex` is `select(searchsorted(other))` behind a
fail-loud superset guard. What is missing is the materialization of the -1 positions as a
fill — and that missing piece is a genuine silent-corruption hazard when consumers
hand-roll it: `values[pos]` with a -1 in `pos` silently selects the **last row** (numpy
negative indexing) for every absent cell.

## Decision

Two additive symbols (MINOR, v1.10.0; `CONFORMANCE_FLOOR` stays `1.0.0`):

1. **`frame.reindex_fill(other, *, fill_value)`** on all three sibling frames (WET per
ADR-011). Align to `other`'s rows with **no superset requirement**: present rows come
through **bit-exact**; absent rows get `fill_value` broadcast across the trailing
axes. `fill_value` is **keyword-only and required** — no silent default (ADR-009); the
caller states its policy (`0.0` vs `NaN`) aloud. The result's index **is** `other`
(immutable, shared); metadata (and `feature_names`) are preserved. The C-21
row-uniqueness stance is inherited unchanged: unique rows assumed in *self*,
duplicates allowed in the target (a `cross_level_align` product is a legal target).

2. **`SpatioTemporalIndex.cartesian(times, units, level)`** — the dense product-index
constructor, **time-major** order as contract (dense indices are canonical across
consumers). **Explicit arrays only**: any rule for deriving them (faoapi's
"units of the last time step" + dropped-entity check, their C-87) is consumer policy
and never lives in the leaf. **Fails loud on duplicated inputs** — a duplicated
product input manufactures duplicate `(time, unit)` rows, which make every same-level
join undefined (C-21); in a product constructor that is always a caller bug.

3. **`assert_reindex_fill_law`** joins the published conformance suite (ADR-016): result
index equals the target row-for-row; present rows bit-exact; absent rows equal the
fill (NaN-safe); on a superset frame the fill degenerates to `reindex`.

## Alternatives considered

- **A `fill_value=None` kwarg on the frozen `reindex`** — rejected: one symbol, two
contracts (fail-loud vs fill) is the mode-switch shape D-09 (#113) already rejected;
under the ADR-018 freeze it could never be untangled.
- **Index-level only (consumers scatter themselves)** — rejected: it pushes the -1
sentinel into every consumer *untested*; the wrap-to-last-row failure is silent wrong
data, the class of bug this leaf exists to remove.
- **Decline (keep fill consumer-side)** — rejected: the need is receipted
(`dense_grid.py:3`), blocks faoapi #242, and per-consumer numpy reimplementations
would each carry the hazard above.
- **A derivation rule in `cartesian`** (e.g. "observed times × last-step units") —
rejected permanently: that complects policy with structure (the C-52 accretion guard
names exactly this camel's nose). If a "derive the dense grid" convenience is ever
demanded, it belongs consumer-side.

## Consequences

- faoapi's `dense_grid.py` can delegate: build its entity set + C-87 check (policy,
stays theirs), call `cartesian`, then `reindex_fill` — and drop pandas (verified in
faoapi #242, acted on by the maintainers, never from this repo).
- Densification **allocates the full dense buffer** (`other.n_rows × …`): a full-pgm
grid runs to tens of millions of rows, and at large S the values buffer alone can
reach memory-breaking size. This is inherent to densifying, stated in both
docstrings, and tracked in the register — the leaf does not guess a size guard
(policy).
- Scalar fill only, deliberately minimal: a per-row/vector fill would be a future
additive overload if a consumer ever receipts the need.
- The frozen surface grows by three symbols that can never be removed short of a MAJOR
(ADR-018) — accepted on the receipted need and the silent-corruption argument.
7 changes: 4 additions & 3 deletions docs/ADRs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ADRs are divided into:

1. **Constitutional ADRs (000–009)** — foundational architectural rules.
2. **Governance ADRs (010)** — the technical risk register.
3. **Project-Specific ADRs (011–025)** — the ratified contract, estimator, sibling-package,
3. **Project-Specific ADRs (011–026)** — the ratified contract, estimator, sibling-package,
and freeze/immutability decisions.

---
Expand Down Expand Up @@ -51,10 +51,10 @@ Together, these define the invariant layer of the system.

---

## Project-Specific ADRs (011–025) — the ratified project decisions
## Project-Specific ADRs (011–026) — the ratified project decisions

**011–016** ratify the six founding contract decisions from the design bible (README §13a,
all Accepted 2026-06-21); **017–025** are the post-v1 decisions — sibling packages, the API
all Accepted 2026-06-21); **017–026** are the post-v1 decisions — sibling packages, the API
freeze, estimators, contract homes, and the immutability convention (Accepted 2026-06-22
through 2026-07; ADR-024 is a design-direction ADR with implementation deferred). Each cites
the risk-register IDs it resolves.
Expand All @@ -74,6 +74,7 @@ the risk-register IDs it resolves.
- **ADR-023** — [Forecast reconciliation is a sibling package (`views_frames_reconcile`)](023_reconciliation_is_a_sibling_package.md). Adds a third sibling package (mirrors ADR-017): the numpy-only, `views_frames`-only forecast reconciler (`reconcile_proportional`/`ReconciliationModule` — make pgm grid forecasts sum to cm country totals per draw), relocated **WET** from views-postprocessing and proven bit-identical. Charter: frame-reconciliation algorithms only; **never fetch the mapping** (injected as arrays, like `cross_level_align`); no IO/scoring/plotting/foreign `views_*`. Import-DAG `views_frames_reconcile → {views_frames}`; additive/MINOR (v1.7.0), `CONFORMANCE_FLOOR` stays 1.0.0. **Amended 2026-06-27 (#143, v1.8.0):** the sample-count contract — a point cm (`sample_count == 1`) is broadcast across the grid's draws; the equal-count path stays bit-exact.
- **ADR-024** — [Principled joint probabilistic reconciliation — design direction & deferral](024_principled_joint_reconciliation_design.md). Design-only (no code): per-draw `proportional` is an information-losing approximation that pairs grid-draw `s` with country-draw `s` across **independently-trained** models with no shared draw identity. Names the **draw-identity contract** principled reconciliation requires (shared ensemble / copula coupling), fixes the upgrade as a **future sibling module** (probabilistic reconciliation / MinT — never a change to `proportional`), and **defers** implementation until a consumer needs calibrated joint tails *and* the country model can supply the identity/coupling. Records C-62; corrects the ambiguous `proportional.py` "C-37" reference (views-frames C-37 is unrelated/resolved; the lineage is views-postprocessing C-37).
- **ADR-025** — [Value-buffer immutability is by convention; only the index is enforced](025_value_buffer_immutability_by_convention.md). Corrects the "immutable value objects" contract to match the code: only the index (`time`/`unit`) is `setflags(write=False)`-enforced; the frame **value buffer is immutable by convention** (mutating `.values` in place is unsupported and may corrupt buffer-sharing frames), left writeable to preserve zero-copy / `mmap` (C-07). The `setflags`-enforce on `.values` would be "tightening an invariant" on a frozen-surface member (a MAJOR, GOVERNANCE/ADR-018) for a hole nothing exercises, so it is recorded as a **deferred MAJOR-rider** (added free on the next MAJOR), not done now. Resolves C-63; docs-only, `CONFORMANCE_FLOOR` stays 1.0.0.
- **ADR-026** — [Dense-grid fill is a leaf primitive](026_dense_grid_fill_primitive.md). Adds `frame.reindex_fill(other, *, fill_value)` (align with **no** superset requirement; present rows bit-exact, absent rows filled; `fill_value` keyword-only + required) on all three siblings, `SpatioTemporalIndex.cartesian(times, units, level)` (dense product index, time-major, explicit arrays only, fails loud on duplicated inputs), and the published `assert_reindex_fill_law`. Closes #203 (faoapi's "views-frames has no fill primitive", the last pandas blocker on FAO ingestion, faoapi #242); rejects a derivation rule and a `reindex` mode-kwarg (D-09/C-52 precedents). Additive MINOR (v1.10.0); `CONFORMANCE_FLOOR` stays 1.0.0.

---

Expand Down
8 changes: 6 additions & 2 deletions docs/CICs/FeatureFrame.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

**Status:** Active
**Owner:** VIEWS platform maintainers
**Last reviewed:** 2026-07-02
**Related ADRs:** ADR-001, ADR-008, ADR-011, ADR-012, ADR-013
**Last reviewed:** 2026-07-27
**Related ADRs:** ADR-001, ADR-008, ADR-011, ADR-012, ADR-013, ADR-026

> Implemented in v0.1.0 (`src/views_frames/feature_frame.py`). This contract governs
> that implementation.
Expand Down Expand Up @@ -44,6 +44,10 @@ array `y_features (N, F, S)` float32 aligned to a `SpatioTemporalIndex`, carryin
- Row ops return new frames preserving `feature_names`: `select(positions | mask)` and
`reindex(other)` — the latter raises unless this index is a superset of `other`;
selection **copies** the selected `values`.
- `reindex_fill(other, *, fill_value)` (ADR-026): the dense-grid companion — no superset
requirement; present rows bit-exact, absent rows filled (`fill_value` keyword-only,
required, broadcast across `(F, S)`); `feature_names`/metadata preserved; law-pinned
by `assert_reindex_fill_law`.

---

Expand Down
9 changes: 7 additions & 2 deletions docs/CICs/PredictionFrame.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

**Status:** Active
**Owner:** VIEWS platform maintainers
**Last reviewed:** 2026-07-02
**Related ADRs:** ADR-001, ADR-008, ADR-011, ADR-012, ADR-013, ADR-017
**Last reviewed:** 2026-07-27
**Related ADRs:** ADR-001, ADR-008, ADR-011, ADR-012, ADR-013, ADR-017, ADR-026

> Implemented in v0.1.0 (`src/views_frames/prediction_frame.py`), relocated from
> views-pipeline-core with numpy-only validation — the source imports pandas, so the
Expand Down Expand Up @@ -43,6 +43,11 @@ ensemble samples `y_pred (N, S)` float32 aligned to a `SpatioTemporalIndex`.
- Row ops return new frames: `select(positions | mask)` and `reindex(other)` — the
latter raises unless this frame's index is a superset of `other`. Selection **copies**
the selected `values` (only structural/metadata ops share the buffer).
- `reindex_fill(other, *, fill_value)` (ADR-026) is the dense-grid companion: aligns to
`other` with **no** superset requirement — present rows pass through **bit-exact**,
absent rows get the caller's `fill_value` (keyword-only, required; no silent default,
ADR-009). Allocates the full dense buffer; assumes unique rows in *self* (C-21);
law-pinned by `assert_reindex_fill_law` in the published conformance suite.
- Sample-axis reduction (collapse/MAP/HDI) is **not** a method here — it lives in
`views_frames_summarize` (ADR-017). The frame exposes the structural `sample_count`/
`is_sample` only.
Expand Down
9 changes: 7 additions & 2 deletions docs/CICs/SpatioTemporalIndex.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

**Status:** Active
**Owner:** VIEWS platform maintainers
**Last reviewed:** 2026-06-24
**Related ADRs:** ADR-001, ADR-002, ADR-009, ADR-013, ADR-014, ADR-015
**Last reviewed:** 2026-07-27
**Related ADRs:** ADR-001, ADR-002, ADR-009, ADR-013, ADR-014, ADR-015, ADR-026

> Implemented in v0.1.0 (`src/views_frames/index.py`). This contract governs that
> implementation.
Expand Down Expand Up @@ -43,6 +43,11 @@ label-alignment that today drags pandas back into the hot path.
- Guarantees **same-level** set operations over `(time, unit)` are pure-numpy and
deterministic: `intersect`, `reindex` / `searchsorted`, `is_superset_of`, `argsort`,
and `select` (rows at integer positions or a boolean mask).
- Offers the dense product-index constructor `cartesian(times, units, level)`
(ADR-026): every `(time, unit)` combination in **time-major** order, from **explicit
arrays only** (a derivation rule — e.g. "units of the last time step" — is consumer
policy, never the leaf's). Fails loud on duplicated input values: a duplicated
product input manufactures duplicate rows and undefined joins (register C-21).
- Exposes **cross-level** remap as an injected-mapping protocol — `cross_level_align`
and the columnar `cross_level_align_arrays` (register C-26); see §2.
- **Row-uniqueness stance (register C-21):** duplicate `(time, unit)` rows are *allowed*
Expand Down
9 changes: 6 additions & 3 deletions docs/CICs/TargetFrame.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

**Status:** Active
**Owner:** VIEWS platform maintainers
**Last reviewed:** 2026-07-02
**Related ADRs:** ADR-001, ADR-008, ADR-011, ADR-012, ADR-013
**Last reviewed:** 2026-07-27
**Related ADRs:** ADR-001, ADR-008, ADR-011, ADR-012, ADR-013, ADR-026

> Implemented in v0.1.0 (`src/views_frames/target_frame.py`). This contract governs
> that implementation.
Expand Down Expand Up @@ -39,7 +39,10 @@ boundary array-native, replacing the pandas actuals the eval adapter takes today
(writeable for zero-copy — in-place `.values` mutation is unsupported; ADR-025 / C-63).
- Carries a typed `metadata` header (ADR-013) and the same row/metadata surface as the
sibling frames: `with_metadata`, `select(positions | mask)`, `reindex(other)` (raises
unless this index is a superset of `other`); `sample_count == 1`, `is_sample == False`.
unless this index is a superset of `other`), and the dense-grid companion
`reindex_fill(other, *, fill_value)` (ADR-026 — no superset requirement; present rows
bit-exact, absent rows filled; law-pinned by `assert_reindex_fill_law`);
`sample_count == 1`, `is_sample == False`.
- The role (ground truth, single realized value) is explicit so line-graph / eval code
can treat it distinctly from sampled frames.

Expand Down
Loading
Loading