Problem
The FAO delivery is moving from point estimates to predictions-with-uncertainty (S samples per cell). As long as pandas sits on the data path, samples cannot propagate: a distribution becomes an object-dtype list-in-cell (the inherited PGMDataset representation) — the memory / encode-decode wall captured in register C-40 (and pipeline-core's own ~18 GB OOM, their #181). views-frames stores a distribution natively as a contiguous (N, S) float32 PredictionFrame.
This epic maps every pandas site in views-postprocessing and sequences the swap to native views-frames, one site at a time, each with a parity proof, disturbing cross-repo interconnectivity as little as possible.
Key enabling fact: the samples already arrive inside the forecast PGMDataset's object-dtype cells — they're collapsed to point estimates today only because unfao/frames.py hardcodes S=1 and the wires carry object-dtype parquet. So vpp can make its interior sample-aware unilaterally, while the two wire boundaries stay frozen until cross-repo coordination.
Why it matters
The map — three concentric rings (grounded in file:line)
Ring 0 — already pandas-free (do not touch): views_postprocessing/delivery/{coverage,identity,observed_range,provenance}.py, unfao/gaul_schema.py, unfao/source_metadata.py.
Ring 1 — vpp-owned seams (UNILATERAL):
unfao/extraction.py — the pandas→primitives seam (cells_of/months_of/drop_months_above/unmapped_cell_count).
unfao/enrichment.py — pd.read_parquet(lookup) (:48) + .merge(...) (:117-119): a keyed metadata-attach join.
unfao/frames.py — the unused views-frames adapter; _column_2d(...).reshape(-1,1) hardcodes S=1 (:85).
scripts/build_gaul_lookup.py — offline lookup build (off the delivery path).
Ring 2 — forced / contract boundaries (CROSS-REPO-GATED):
Desired end state
A forecast flows through vpp's interior as a PredictionFrame carrying (N, S), producing byte-identical deliveries to today on S=1 inputs (proving the interior is sample-ready) behind frozen wires. The final outbound hop that actually ships (N,S) to FAO (S6) and the historical inbound retirement (S7) are landed only when their cross-repo gates clear.
Scope
In: the 7 sequenced steps below (S1–S7), each independently shippable with a parity proof.
Out: Ring 0 (already pandas-free); pushing the enrichment metadata join into views-frames (S4 moves it off pandas, not into a frame — it is not frame algebra); changing the outbound format ahead of faoapi.
Stories (in sequence)
- S1 —
frames.py: generalize to_prediction_frame to S>1 [UNILATERAL] ← recommended first
- S2 —
extraction.py: frame-native sibling readers [UNILATERAL]
- S3 — forecast "convert at the door" (internal
PredictionFrame) [UNILATERAL]
- S4 —
enrichment.py: numpy/pyarrow keyed gather [UNILATERAL, low priority]
- S5 —
build_gaul_lookup.py: pyarrow-native build [UNILATERAL, lowest priority]
- S6 — forecast outbound → arrow sample-frame wire
[CROSS-REPO-GATED: faoapi #45 + C-40]
- S7 — historical inbound
read_dataframe/PGMDataset retirement [CROSS-REPO-GATED: pipeline-core C-40] — do not move yet
Dependency sequence
S1 ──► S2 ──► S3 ──► S6 (gated: faoapi #45)
S4 (independent, low pri)
S5 (independent, lowest pri)
S7 (gated: pipeline-core C-40) — independent of S1–S6; never blocks samples
Epic acceptance criteria
Refs
Register C-40 (the pandas gate), #45 (the delivery-side draw carrier / faoapi wire), pipeline-core #186/#207/#161/#159/#211 (their DataFrame retirement). Parity pattern: tests/test_views_frames_conformance.py.
Issues
Problem
The FAO delivery is moving from point estimates to predictions-with-uncertainty (S samples per cell). As long as pandas sits on the data path, samples cannot propagate: a distribution becomes an object-dtype list-in-cell (the inherited
PGMDatasetrepresentation) — the memory / encode-decode wall captured in register C-40 (and pipeline-core's own ~18 GB OOM, their #181). views-frames stores a distribution natively as a contiguous(N, S)float32PredictionFrame.This epic maps every pandas site in views-postprocessing and sequences the swap to native views-frames, one site at a time, each with a parity proof, disturbing cross-repo interconnectivity as little as possible.
Key enabling fact: the samples already arrive inside the forecast
PGMDataset's object-dtype cells — they're collapsed to point estimates today only becauseunfao/frames.pyhardcodesS=1and the wires carry object-dtype parquet. So vpp can make its interior sample-aware unilaterally, while the two wire boundaries stay frozen until cross-repo coordination.Why it matters
The map — three concentric rings (grounded in file:line)
Ring 0 — already pandas-free (do not touch):
views_postprocessing/delivery/{coverage,identity,observed_range,provenance}.py,unfao/gaul_schema.py,unfao/source_metadata.py.Ring 1 — vpp-owned seams (UNILATERAL):
unfao/extraction.py— the pandas→primitives seam (cells_of/months_of/drop_months_above/unmapped_cell_count).unfao/enrichment.py—pd.read_parquet(lookup)(:48) +.merge(...)(:117-119): a keyed metadata-attach join.unfao/frames.py— the unused views-frames adapter;_column_2d(...).reshape(-1,1)hardcodes S=1 (:85).scripts/build_gaul_lookup.py— offline lookup build (off the delivery path).Ring 2 — forced / contract boundaries (CROSS-REPO-GATED):
read_dataframe→PGMDataset(unfao.py:58-64) +_append_metadata/_validatepandas ops — gated on pipeline-core DataFrame retirement (C-40; their S5 — The lookup declares its own version instead of inferring it (C-60) #186/Record the operator's correction decisions; put the FAO half to them formally #207/refactor(unfao): S3 — one representation seam; store metadata gets its own home #161/feat(delivery): S1 — one delivery path; refuse an incomplete launch config #159/feat(crafd): CRAF'd producer package — clone of unfao/ (per CLONING.md) #211).pd.read_parquet(io.BytesIO(...))(unfao.py:125) — producer wire format.df.to_parquet(unfao.py:298,309) → Appwrite — gated on the views-faoapi wire contract (FAO delivery path is point/DataFrame-based — won't carry rusty_bucket's pooled draws #45); faoapi already rebuilds views-frames at ingestion.Desired end state
A forecast flows through vpp's interior as a
PredictionFramecarrying(N, S), producing byte-identical deliveries to today on S=1 inputs (proving the interior is sample-ready) behind frozen wires. The final outbound hop that actually ships(N,S)to FAO (S6) and the historical inbound retirement (S7) are landed only when their cross-repo gates clear.Scope
In: the 7 sequenced steps below (S1–S7), each independently shippable with a parity proof.
Out: Ring 0 (already pandas-free); pushing the enrichment metadata join into views-frames (S4 moves it off pandas, not into a frame — it is not frame algebra); changing the outbound format ahead of faoapi.
Stories (in sequence)
frames.py: generalizeto_prediction_frameto S>1[UNILATERAL]← recommended firstextraction.py: frame-native sibling readers[UNILATERAL]PredictionFrame)[UNILATERAL]enrichment.py: numpy/pyarrow keyed gather[UNILATERAL, low priority]build_gaul_lookup.py: pyarrow-native build[UNILATERAL, lowest priority][CROSS-REPO-GATED: faoapi #45 + C-40]read_dataframe/PGMDatasetretirement[CROSS-REPO-GATED: pipeline-core C-40]— do not move yetDependency sequence
Epic acceptance criteria
(N,S)PredictionFrame; deliveries are byte-identical to today on S=1 inputs (parity).delivery/remains pandas-free throughout (the design-contract test stays green).Refs
Register C-40 (the pandas gate), #45 (the delivery-side draw carrier / faoapi wire), pipeline-core #186/#207/#161/#159/#211 (their DataFrame retirement). Parity pattern:
tests/test_views_frames_conformance.py.Issues
[UNILATERAL][UNILATERAL][UNILATERAL][UNILATERAL, low pri][UNILATERAL, lowest pri][GATED: faoapi #45 + C-40][GATED: pipeline-core C-40]