views-frames 1.7.0__tar.gz → 1.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {views_frames-1.7.0 → views_frames-1.8.0}/PKG-INFO +14 -2
- {views_frames-1.7.0 → views_frames-1.8.0}/README.md +7 -1
- {views_frames-1.7.0 → views_frames-1.8.0}/pyproject.toml +27 -8
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/__init__.py +14 -1
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/conformance.py +4 -1
- views_frames-1.8.0/src/views_frames_reconcile/module.py +120 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/proportional.py +6 -3
- views_frames-1.8.0/src/views_frames_reconcile/result.py +55 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/validation.py +6 -4
- views_frames-1.7.0/src/views_frames_reconcile/module.py +0 -68
- {views_frames-1.7.0 → views_frames-1.8.0}/.gitignore +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/LICENSE +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/__init__.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/_typing.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/_validation.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/conformance/__init__.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/feature_frame.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/index.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/io/__init__.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/io/arrow.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/io/npz.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/metadata.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/prediction_frame.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/protocols.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/py.typed +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/spatial_level.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames/target_frame.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/frames.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/grouping.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_reconcile/py.typed +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/__init__.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/_common.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/aggregate.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/bimodality.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/collapse.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/config.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/conformance.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/exceedance.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/expected_shortfall.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/interval.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/point.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/py.typed +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/summarize_tower.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/tower.py +0 -0
- {views_frames-1.7.0 → views_frames-1.8.0}/src/views_frames_summarize/tower_point.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: views-frames
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: The VIEWS platform data-contract layer: immutable array+identifier frames (numpy only, root of the dependency DAG).
|
|
5
5
|
Project-URL: Homepage, https://github.com/views-platform/views-frames
|
|
6
6
|
Project-URL: Repository, https://github.com/views-platform/views-frames
|
|
@@ -22,6 +22,12 @@ Requires-Python: >=3.10
|
|
|
22
22
|
Requires-Dist: numpy<3,>=1.26
|
|
23
23
|
Provides-Extra: arrow
|
|
24
24
|
Requires-Dist: pyarrow<20,>=14; extra == 'arrow'
|
|
25
|
+
Provides-Extra: docs
|
|
26
|
+
Requires-Dist: jupyterlab<5,>=4; extra == 'docs'
|
|
27
|
+
Requires-Dist: matplotlib<4,>=3.8; extra == 'docs'
|
|
28
|
+
Requires-Dist: nbmake<2,>=1.5; extra == 'docs'
|
|
29
|
+
Requires-Dist: pyarrow<20,>=14; extra == 'docs'
|
|
30
|
+
Requires-Dist: scipy<2,>=1.11; extra == 'docs'
|
|
25
31
|
Description-Content-Type: text/markdown
|
|
26
32
|
|
|
27
33
|
# views-frames
|
|
@@ -249,7 +255,13 @@ makes it safe to depend on from everywhere (SDP).
|
|
|
249
255
|
3. **Immutable value objects.** A frame is validated at construction and then
|
|
250
256
|
treated as read-only. Operations (`collapse`, `select`, `with_metadata`)
|
|
251
257
|
**return new frames**; they never mutate in place. (Directly forbids the
|
|
252
|
-
C-184 cross-repo-mutation anti-pattern.) **
|
|
258
|
+
C-184 cross-repo-mutation anti-pattern.) **Enforced for the index, by
|
|
259
|
+
convention for the value buffer:** the identifier arrays (`time`/`unit`) are
|
|
260
|
+
write-protected (`setflags(write=False)`); the `values` buffer is left
|
|
261
|
+
**writeable on purpose** — so structural ops can share it zero-copy — and is
|
|
262
|
+
immutable *by convention*: mutating `.values` in place is unsupported and may
|
|
263
|
+
silently corrupt buffer-sharing frames (ADR-025 / register C-63; enforcing it
|
|
264
|
+
is a deferred MAJOR-rider). **Copy-vs-view:** structural and
|
|
253
265
|
metadata-only operations (`with_metadata`, contiguous `select`) return frames
|
|
254
266
|
that **share** the underlying `values` buffer (numpy view / zero-copy), and a
|
|
255
267
|
`mmap`-backed frame stays `mmap`-backed — a new frame must never copy a
|
|
@@ -223,7 +223,13 @@ makes it safe to depend on from everywhere (SDP).
|
|
|
223
223
|
3. **Immutable value objects.** A frame is validated at construction and then
|
|
224
224
|
treated as read-only. Operations (`collapse`, `select`, `with_metadata`)
|
|
225
225
|
**return new frames**; they never mutate in place. (Directly forbids the
|
|
226
|
-
C-184 cross-repo-mutation anti-pattern.) **
|
|
226
|
+
C-184 cross-repo-mutation anti-pattern.) **Enforced for the index, by
|
|
227
|
+
convention for the value buffer:** the identifier arrays (`time`/`unit`) are
|
|
228
|
+
write-protected (`setflags(write=False)`); the `values` buffer is left
|
|
229
|
+
**writeable on purpose** — so structural ops can share it zero-copy — and is
|
|
230
|
+
immutable *by convention*: mutating `.values` in place is unsupported and may
|
|
231
|
+
silently corrupt buffer-sharing frames (ADR-025 / register C-63; enforcing it
|
|
232
|
+
is a deferred MAJOR-rider). **Copy-vs-view:** structural and
|
|
227
233
|
metadata-only operations (`with_metadata`, contiguous `select`) return frames
|
|
228
234
|
that **share** the underlying `values` buffer (numpy view / zero-copy), and a
|
|
229
235
|
`mmap`-backed frame stays `mmap`-backed — a new frame must never copy a
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "views-frames"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.8.0"
|
|
8
8
|
description = "The VIEWS platform data-contract layer: immutable array+identifier frames (numpy only, root of the dependency DAG)."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Simon Polichinel von der Maase", email = "simmaa@prio.org" },
|
|
@@ -39,6 +39,22 @@ Changelog = "https://github.com/views-platform/views-frames/blob/main/CHANGELOG.
|
|
|
39
39
|
# Optional serialization extra — pyarrow is allowed ONLY behind io/ (README §3.1).
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
arrow = ["pyarrow>=14,<20"]
|
|
42
|
+
# Optional docs/notebooks extra — plotting + jupyter for the showcase notebooks
|
|
43
|
+
# (notebooks/, an un-gated dev artifact outside src/). NEVER runtime dependencies;
|
|
44
|
+
# self-references [arrow] so `uv sync --extra docs` also enables the parquet round-trip
|
|
45
|
+
# demo in notebooks/01_frames.ipynb. matplotlib/scipy are notebook-only (the core is
|
|
46
|
+
# numpy-only); these are imported by notebooks, never by the package.
|
|
47
|
+
docs = [
|
|
48
|
+
"views-frames[arrow]",
|
|
49
|
+
"matplotlib>=3.8,<4",
|
|
50
|
+
"scipy>=1.11,<2",
|
|
51
|
+
"jupyterlab>=4,<5",
|
|
52
|
+
# nbmake drives the non-blocking notebook-execution CI check (issue #151): it just
|
|
53
|
+
# runs the notebooks end-to-end to catch frozen-API drift. A test tool, but it rides
|
|
54
|
+
# in [docs] so the single `uv sync --extra docs` provisions everything the notebooks
|
|
55
|
+
# job needs (kernel + plotting + the checker).
|
|
56
|
+
"nbmake>=1.5,<2",
|
|
57
|
+
]
|
|
42
58
|
|
|
43
59
|
[dependency-groups]
|
|
44
60
|
dev = [
|
|
@@ -76,9 +92,10 @@ show_missing = true
|
|
|
76
92
|
[tool.ruff]
|
|
77
93
|
line-length = 88
|
|
78
94
|
target-version = "py310"
|
|
79
|
-
# research/ is the volatile estimator lab
|
|
80
|
-
#
|
|
81
|
-
|
|
95
|
+
# research/ is the volatile estimator lab and notebooks/ holds the un-gated showcase
|
|
96
|
+
# notebooks + their helper (notebooks/_synthetic.py) — both sit outside the frozen,
|
|
97
|
+
# gated src/ and are not held to the leaf's lint/format bar.
|
|
98
|
+
extend-exclude = ["research", "notebooks"]
|
|
82
99
|
|
|
83
100
|
[tool.ruff.lint]
|
|
84
101
|
select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "SIM"]
|
|
@@ -87,11 +104,13 @@ select = ["E", "F", "I", "N", "UP", "B", "A", "C4", "SIM"]
|
|
|
87
104
|
# The falsification stubs assert against the README via long, literal regexes
|
|
88
105
|
# whose lines cannot wrap without changing their semantics.
|
|
89
106
|
"tests/test_falsification_*.py" = ["E501"]
|
|
90
|
-
# The reconciliation parity tests
|
|
91
|
-
#
|
|
92
|
-
#
|
|
107
|
+
# The reconciliation parity tests are faithfully relocated from views-postprocessing
|
|
108
|
+
# (Epic 11 / ADR-023); their dense numpy/dict-comprehension assertions are kept verbatim
|
|
109
|
+
# rather than re-wrapped.
|
|
93
110
|
"tests/test_reconciliation_*.py" = ["E501"]
|
|
94
|
-
|
|
111
|
+
# The dev/ops scripts (oracle regeneration; the reconciliation parity verifier) are not
|
|
112
|
+
# CI-run and carry dense numpy one-liners + report f-strings; not held to the wrap bar.
|
|
113
|
+
"scripts/*.py" = ["E501"]
|
|
95
114
|
|
|
96
115
|
[tool.mypy]
|
|
97
116
|
python_version = "3.10"
|
|
@@ -7,5 +7,18 @@ should be added as sibling modules, not by modifying ``proportional``.
|
|
|
7
7
|
|
|
8
8
|
from views_frames_reconcile.module import ReconciliationModule
|
|
9
9
|
from views_frames_reconcile.proportional import reconcile_proportional
|
|
10
|
+
from views_frames_reconcile.result import (
|
|
11
|
+
ALIGNED_DRAWS,
|
|
12
|
+
METHOD_PROPORTIONAL,
|
|
13
|
+
POINT_BROADCAST,
|
|
14
|
+
ReconciliationResult,
|
|
15
|
+
)
|
|
10
16
|
|
|
11
|
-
__all__ = [
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ALIGNED_DRAWS",
|
|
19
|
+
"METHOD_PROPORTIONAL",
|
|
20
|
+
"POINT_BROADCAST",
|
|
21
|
+
"ReconciliationModule",
|
|
22
|
+
"ReconciliationResult",
|
|
23
|
+
"reconcile_proportional",
|
|
24
|
+
]
|
|
@@ -66,7 +66,10 @@ def assert_reconcile_contract(
|
|
|
66
66
|
rows = (pg_time == t) & (cm_units == c)
|
|
67
67
|
in_sum = pgm_frame.values[rows].sum(axis=0) # (S,)
|
|
68
68
|
out_sum = out.values[rows].sum(axis=0) # (S,)
|
|
69
|
-
|
|
69
|
+
# cm may be a point (sample_count == 1, broadcast inside reconcile) or aligned
|
|
70
|
+
# draws — broadcast its per-(time, country) total to the draw axis either way.
|
|
71
|
+
cm_total = cm_frame.values[cm_pos[(int(t), int(c))]]
|
|
72
|
+
total = np.broadcast_to(cm_total, out_sum.shape)
|
|
70
73
|
active = in_sum != 0
|
|
71
74
|
np.testing.assert_allclose(
|
|
72
75
|
out_sum[active], total[active], rtol=1e-4, atol=1e-3
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Frames-native reconciliation orchestration (epic #31, story #36).
|
|
2
|
+
|
|
3
|
+
`ReconciliationModule` holds the injected `(time, priogrid_gid) -> country_id`
|
|
4
|
+
mapping (geography is injected, never embedded — views-frames ADR-014) and
|
|
5
|
+
applies it: `reconcile(cm_frame, pgm_frame)` validates the inputs and scales the
|
|
6
|
+
grid forecasts to the country totals, returning a **new** pgm frame (de-mutated,
|
|
7
|
+
C-184).
|
|
8
|
+
|
|
9
|
+
**SRP:** orchestration only — the scaling math is the leaf (`proportional`), the
|
|
10
|
+
grouping is `grouping`, the guards are `validation`, the I/O is `frames`. No
|
|
11
|
+
torch, no pandas, no viewser, no wandb: the original's `ProcessPoolExecutor` and
|
|
12
|
+
WandB alerting are dropped (numpy is fast; there is no GPU). If scale ever needs
|
|
13
|
+
parallelism, add it behind this same interface (OCP). Multi-target inputs are
|
|
14
|
+
reconciled by calling `reconcile` once per target.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import NDArray
|
|
23
|
+
|
|
24
|
+
from views_frames import PredictionFrame
|
|
25
|
+
from views_frames_reconcile.grouping import reconcile_pgm_to_cm
|
|
26
|
+
from views_frames_reconcile.result import (
|
|
27
|
+
ALIGNED_DRAWS,
|
|
28
|
+
METHOD_PROPORTIONAL,
|
|
29
|
+
POINT_BROADCAST,
|
|
30
|
+
ReconciliationResult,
|
|
31
|
+
)
|
|
32
|
+
from views_frames_reconcile.validation import validate_reconciliation_inputs
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _broadcast_point_country(
|
|
36
|
+
cm_frame: PredictionFrame, n_samples: int
|
|
37
|
+
) -> PredictionFrame:
|
|
38
|
+
"""Tile a point country forecast (``sample_count == 1``) across ``n_samples`` draws.
|
|
39
|
+
|
|
40
|
+
Bit-identical to the WET broadcast pipeline-core's ``align_country_to_grid`` does
|
|
41
|
+
(views-frames#143): the single column is repeated, so every draw is rescaled to the
|
|
42
|
+
same total. It lives in the orchestrator so the leaf ``proportional`` and the
|
|
43
|
+
parity-frozen ``grouping`` hot loop stay **untouched** — the equal-count path is
|
|
44
|
+
byte-for-byte identical and only ``sample_count == 1`` takes this new path.
|
|
45
|
+
"""
|
|
46
|
+
tiled = np.tile(np.asarray(cm_frame.values, dtype=np.float32), (1, n_samples))
|
|
47
|
+
return PredictionFrame(tiled, cm_frame.index, cm_frame.metadata)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ReconciliationModule:
|
|
51
|
+
"""Reconcile pgm forecasts to cm country totals (one target per call)."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
map_keys: NDArray[np.integer[Any]] | object,
|
|
56
|
+
map_vals: NDArray[np.integer[Any]] | object,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Inject the `(time, priogrid_gid) -> country_id` mapping.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
map_keys: ``(M, 2)`` int array of ``(time, priogrid_gid)`` pairs.
|
|
62
|
+
map_vals: ``(M,)`` int ``country_id`` for each key.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
ValueError: ``map_keys`` is not ``(M, 2)`` or ``map_vals`` is not
|
|
66
|
+
length ``M``.
|
|
67
|
+
"""
|
|
68
|
+
keys = np.asarray(map_keys)
|
|
69
|
+
vals = np.asarray(map_vals)
|
|
70
|
+
if keys.ndim != 2 or keys.shape[1] != 2:
|
|
71
|
+
raise ValueError("map_keys must be an (M, 2) array of (time, priogrid_gid)")
|
|
72
|
+
if vals.shape != (keys.shape[0],):
|
|
73
|
+
raise ValueError("map_vals must be a length-M array aligned to map_keys")
|
|
74
|
+
self._map_keys = keys
|
|
75
|
+
self._map_vals = vals
|
|
76
|
+
|
|
77
|
+
def reconcile_result(
|
|
78
|
+
self, cm_frame: PredictionFrame, pgm_frame: PredictionFrame
|
|
79
|
+
) -> ReconciliationResult:
|
|
80
|
+
"""Reconcile, also reporting the mode — a ``ReconciliationResult``.
|
|
81
|
+
|
|
82
|
+
A **point** country (``cm.sample_count == 1``) is broadcast across the grid's
|
|
83
|
+
draws before reconciling (mode ``point-broadcast``); an **aligned** country
|
|
84
|
+
(``cm.sample_count == S``) scales draw-for-draw (mode ``aligned-draws``, the
|
|
85
|
+
documented per-draw approximation). Any other count fails loud.
|
|
86
|
+
|
|
87
|
+
The mode is **returned**, never stamped on the leaf's generic ``FrameMetadata``
|
|
88
|
+
(ADR-020 / register C-47 — the leaf carries no reconciliation vocabulary; see
|
|
89
|
+
``result.py``).
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: the inputs fail validation (level / sample-count — cm must be 1
|
|
93
|
+
or pgm's ``S`` / time coverage / missing country forecast).
|
|
94
|
+
"""
|
|
95
|
+
validate_reconciliation_inputs(
|
|
96
|
+
cm_frame, pgm_frame, self._map_keys, self._map_vals
|
|
97
|
+
)
|
|
98
|
+
if cm_frame.sample_count != pgm_frame.sample_count:
|
|
99
|
+
cm = _broadcast_point_country(cm_frame, pgm_frame.sample_count)
|
|
100
|
+
mode = POINT_BROADCAST
|
|
101
|
+
else:
|
|
102
|
+
cm = cm_frame
|
|
103
|
+
mode = ALIGNED_DRAWS
|
|
104
|
+
frame = reconcile_pgm_to_cm(pgm_frame, cm, self._map_keys, self._map_vals)
|
|
105
|
+
return ReconciliationResult(frame=frame, mode=mode, method=METHOD_PROPORTIONAL)
|
|
106
|
+
|
|
107
|
+
def reconcile(
|
|
108
|
+
self, cm_frame: PredictionFrame, pgm_frame: PredictionFrame
|
|
109
|
+
) -> PredictionFrame:
|
|
110
|
+
"""Validate the inputs, then return a new pgm frame reconciled to cm totals.
|
|
111
|
+
|
|
112
|
+
Point/aligned handling and fail-loud are as :meth:`reconcile_result`; this
|
|
113
|
+
returns only the reconciled frame. Call :meth:`reconcile_result` when you also
|
|
114
|
+
need the reconciliation *mode* (point-broadcast vs aligned-draws).
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: the inputs fail validation (level / sample-count — cm must be 1
|
|
118
|
+
or pgm's ``S`` / time coverage / missing country forecast).
|
|
119
|
+
"""
|
|
120
|
+
return self.reconcile_result(cm_frame, pgm_frame).frame
|
|
@@ -10,9 +10,12 @@ This is a *faithful, numpy-only* port of views-reporting's
|
|
|
10
10
|
``ForecastReconciler.reconcile_forecast`` (torch), migrated here because the
|
|
11
11
|
algorithm belongs in post-processing, not reporting (views-reporting issue #72).
|
|
12
12
|
It is intentionally the **same** method — a pragmatic per-draw approximation, not
|
|
13
|
-
principled joint probabilistic reconciliation. The upgrade to the latter is
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
principled joint probabilistic reconciliation. The upgrade to the latter is designed
|
|
14
|
+
in **ADR-024** (register **C-62**; the cross-repo lineage is views-postprocessing
|
|
15
|
+
C-37) and is deliberately **deferred**: per-draw pairing of independently-trained
|
|
16
|
+
grid and country draws has no shared draw identity, so the principled method waits on
|
|
17
|
+
a defined draw-identity/coupling contract and a consumer that needs calibrated joint
|
|
18
|
+
tails.
|
|
16
19
|
|
|
17
20
|
No torch, no pandas — numpy only.
|
|
18
21
|
"""
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The reconciliation result — the reconciled frame plus *how* it was produced (#144).
|
|
2
|
+
|
|
3
|
+
The reconciliation **mode** (`point-broadcast` vs `aligned-draws`) and **method** are
|
|
4
|
+
reported here, on a reconcile-package result object — deliberately **not** stamped onto
|
|
5
|
+
the leaf's generic ``FrameMetadata``. That header is governed *generic-only* (ADR-020 /
|
|
6
|
+
register C-47): the numpy leaf carries provenance meaningful for *any* frame (`run_id`,
|
|
7
|
+
`data_version`) and must never carry domain/operation vocabulary it cannot know — as
|
|
8
|
+
evaluation provenance lives in views-evaluation's ``MetricFrame``, not the leaf.
|
|
9
|
+
Reconciliation is a sibling operation, so its provenance is reported by the sibling.
|
|
10
|
+
|
|
11
|
+
A caller that needs the mode reads it off :class:`ReconciliationResult`
|
|
12
|
+
(``ReconciliationModule.reconcile_result``); a caller that only needs the frame uses
|
|
13
|
+
``ReconciliationModule.reconcile``, which returns the frame directly.
|
|
14
|
+
|
|
15
|
+
The mode reflects **what this reconcile call did** (broadcast a point, or scaled aligned
|
|
16
|
+
draws) — not upstream provenance: a caller that pre-tiles a point country to ``S`` draws
|
|
17
|
+
before calling reconciles in ``aligned-draws`` mode. The string values match
|
|
18
|
+
pipeline-core's ``reconcile_frames`` constants verbatim, so the *vocabulary* is shared
|
|
19
|
+
across the repos that produce and consume the mode.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
|
|
26
|
+
from views_frames import PredictionFrame
|
|
27
|
+
|
|
28
|
+
#: Mode — the call **broadcast a point**: the country arrived as a point
|
|
29
|
+
#: (``cm.sample_count == 1``) against a multi-draw grid and was tiled across the grid's
|
|
30
|
+
#: ``S`` draws (every draw rescaled to the same total).
|
|
31
|
+
POINT_BROADCAST = "point-broadcast"
|
|
32
|
+
#: Mode — the call **scaled aligned draws**: the country already carried the grid's draw
|
|
33
|
+
#: count (``cm.sample_count == S``) and was scaled draw-for-draw — the per-draw
|
|
34
|
+
#: approximation (the principled joint upgrade is a separate design, #145). The mode
|
|
35
|
+
#: describes *what this call did*: a point against a single-draw grid, or a point the
|
|
36
|
+
#: caller pre-tiled to ``S``, also reads as aligned-draws (nothing was broadcast).
|
|
37
|
+
ALIGNED_DRAWS = "aligned-draws"
|
|
38
|
+
#: The reconciliation algorithm — top-down proportional, per draw.
|
|
39
|
+
METHOD_PROPORTIONAL = "proportional"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class ReconciliationResult:
|
|
44
|
+
"""A reconciled grid frame plus how it was produced.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
frame: the reconciled pgm ``PredictionFrame`` (identical to what ``reconcile``
|
|
48
|
+
returns).
|
|
49
|
+
mode: :data:`POINT_BROADCAST` or :data:`ALIGNED_DRAWS`.
|
|
50
|
+
method: the reconciliation method, e.g. :data:`METHOD_PROPORTIONAL`.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
frame: PredictionFrame
|
|
54
|
+
mode: str
|
|
55
|
+
method: str
|
|
@@ -6,7 +6,8 @@ buried in the orchestrator. The original's intents map as:
|
|
|
6
6
|
|
|
7
7
|
- dataset type checks -> spatial-level guard (cm@CM, pgm@PGM)
|
|
8
8
|
- same time steps + exact overlap -> identical set of time values
|
|
9
|
-
- (per-draw scaling needs it) -> equal `sample_count
|
|
9
|
+
- (per-draw scaling needs it) -> equal `sample_count`, or a point cm
|
|
10
|
+
(`sample_count == 1`) broadcast across the draws
|
|
10
11
|
- valid countries -> every (time, country) the grid maps to has
|
|
11
12
|
a country forecast in the cm frame
|
|
12
13
|
|
|
@@ -44,10 +45,11 @@ def validate_reconciliation_inputs(
|
|
|
44
45
|
raise ValueError(
|
|
45
46
|
f"grid frame must be at SpatialLevel.PGM, got {pgm_frame.index.level}"
|
|
46
47
|
)
|
|
47
|
-
if cm_frame.sample_count
|
|
48
|
+
if cm_frame.sample_count not in (1, pgm_frame.sample_count):
|
|
48
49
|
raise ValueError(
|
|
49
|
-
f"sample-count mismatch: cm has {cm_frame.sample_count}, "
|
|
50
|
-
f"
|
|
50
|
+
f"sample-count mismatch: cm has {cm_frame.sample_count}, pgm has "
|
|
51
|
+
f"{pgm_frame.sample_count}; cm must be 1 (point, broadcast across draws) "
|
|
52
|
+
f"or {pgm_frame.sample_count} (aligned draws)"
|
|
51
53
|
)
|
|
52
54
|
|
|
53
55
|
cm_times = {int(t) for t in np.unique(cm_frame.index.time)}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
"""Frames-native reconciliation orchestration (epic #31, story #36).
|
|
2
|
-
|
|
3
|
-
`ReconciliationModule` holds the injected `(time, priogrid_gid) -> country_id`
|
|
4
|
-
mapping (geography is injected, never embedded — views-frames ADR-014) and
|
|
5
|
-
applies it: `reconcile(cm_frame, pgm_frame)` validates the inputs and scales the
|
|
6
|
-
grid forecasts to the country totals, returning a **new** pgm frame (de-mutated,
|
|
7
|
-
C-184).
|
|
8
|
-
|
|
9
|
-
**SRP:** orchestration only — the scaling math is the leaf (`proportional`), the
|
|
10
|
-
grouping is `grouping`, the guards are `validation`, the I/O is `frames`. No
|
|
11
|
-
torch, no pandas, no viewser, no wandb: the original's `ProcessPoolExecutor` and
|
|
12
|
-
WandB alerting are dropped (numpy is fast; there is no GPU). If scale ever needs
|
|
13
|
-
parallelism, add it behind this same interface (OCP). Multi-target inputs are
|
|
14
|
-
reconciled by calling `reconcile` once per target.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
from typing import Any
|
|
20
|
-
|
|
21
|
-
import numpy as np
|
|
22
|
-
from numpy.typing import NDArray
|
|
23
|
-
|
|
24
|
-
from views_frames import PredictionFrame
|
|
25
|
-
from views_frames_reconcile.grouping import reconcile_pgm_to_cm
|
|
26
|
-
from views_frames_reconcile.validation import validate_reconciliation_inputs
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ReconciliationModule:
|
|
30
|
-
"""Reconcile pgm forecasts to cm country totals (one target per call)."""
|
|
31
|
-
|
|
32
|
-
def __init__(
|
|
33
|
-
self,
|
|
34
|
-
map_keys: NDArray[np.integer[Any]] | object,
|
|
35
|
-
map_vals: NDArray[np.integer[Any]] | object,
|
|
36
|
-
) -> None:
|
|
37
|
-
"""Inject the `(time, priogrid_gid) -> country_id` mapping.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
map_keys: ``(M, 2)`` int array of ``(time, priogrid_gid)`` pairs.
|
|
41
|
-
map_vals: ``(M,)`` int ``country_id`` for each key.
|
|
42
|
-
|
|
43
|
-
Raises:
|
|
44
|
-
ValueError: ``map_keys`` is not ``(M, 2)`` or ``map_vals`` is not
|
|
45
|
-
length ``M``.
|
|
46
|
-
"""
|
|
47
|
-
keys = np.asarray(map_keys)
|
|
48
|
-
vals = np.asarray(map_vals)
|
|
49
|
-
if keys.ndim != 2 or keys.shape[1] != 2:
|
|
50
|
-
raise ValueError("map_keys must be an (M, 2) array of (time, priogrid_gid)")
|
|
51
|
-
if vals.shape != (keys.shape[0],):
|
|
52
|
-
raise ValueError("map_vals must be a length-M array aligned to map_keys")
|
|
53
|
-
self._map_keys = keys
|
|
54
|
-
self._map_vals = vals
|
|
55
|
-
|
|
56
|
-
def reconcile(
|
|
57
|
-
self, cm_frame: PredictionFrame, pgm_frame: PredictionFrame
|
|
58
|
-
) -> PredictionFrame:
|
|
59
|
-
"""Validate the inputs, then return a new pgm frame reconciled to cm totals.
|
|
60
|
-
|
|
61
|
-
Raises:
|
|
62
|
-
ValueError: the inputs fail validation (level / sample-count / time
|
|
63
|
-
coverage / missing country forecast).
|
|
64
|
-
"""
|
|
65
|
-
validate_reconciliation_inputs(
|
|
66
|
-
cm_frame, pgm_frame, self._map_keys, self._map_vals
|
|
67
|
-
)
|
|
68
|
-
return reconcile_pgm_to_cm(pgm_frame, cm_frame, self._map_keys, self._map_vals)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|