views-frames 1.8.0__tar.gz → 1.8.1__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.
Files changed (44) hide show
  1. {views_frames-1.8.0 → views_frames-1.8.1}/PKG-INFO +7 -3
  2. {views_frames-1.8.0 → views_frames-1.8.1}/README.md +6 -2
  3. {views_frames-1.8.0 → views_frames-1.8.1}/pyproject.toml +1 -1
  4. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/conformance/__init__.py +18 -0
  5. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/index.py +5 -0
  6. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/conformance.py +14 -0
  7. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/proportional.py +28 -5
  8. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/conformance.py +14 -0
  9. {views_frames-1.8.0 → views_frames-1.8.1}/.gitignore +0 -0
  10. {views_frames-1.8.0 → views_frames-1.8.1}/LICENSE +0 -0
  11. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/__init__.py +0 -0
  12. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/_typing.py +0 -0
  13. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/_validation.py +0 -0
  14. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/feature_frame.py +0 -0
  15. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/io/__init__.py +0 -0
  16. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/io/arrow.py +0 -0
  17. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/io/npz.py +0 -0
  18. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/metadata.py +0 -0
  19. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/prediction_frame.py +0 -0
  20. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/protocols.py +0 -0
  21. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/py.typed +0 -0
  22. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/spatial_level.py +0 -0
  23. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames/target_frame.py +0 -0
  24. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/__init__.py +0 -0
  25. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/frames.py +0 -0
  26. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/grouping.py +0 -0
  27. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/module.py +0 -0
  28. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/py.typed +0 -0
  29. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/result.py +0 -0
  30. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_reconcile/validation.py +0 -0
  31. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/__init__.py +0 -0
  32. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/_common.py +0 -0
  33. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/aggregate.py +0 -0
  34. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/bimodality.py +0 -0
  35. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/collapse.py +0 -0
  36. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/config.py +0 -0
  37. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/exceedance.py +0 -0
  38. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/expected_shortfall.py +0 -0
  39. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/interval.py +0 -0
  40. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/point.py +0 -0
  41. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/py.typed +0 -0
  42. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/summarize_tower.py +0 -0
  43. {views_frames-1.8.0 → views_frames-1.8.1}/src/views_frames_summarize/tower.py +0 -0
  44. {views_frames-1.8.0 → views_frames-1.8.1}/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.8.0
3
+ Version: 1.8.1
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
@@ -36,14 +36,18 @@ Description-Content-Type: text/markdown
36
36
  > containers (`FeatureFrame`, `PredictionFrame`, and their anticipated siblings)
37
37
  > that every other repo depends on and that depends on nothing internal.
38
38
  >
39
- > **Status:** **v1.7.0 — frozen API** (frozen since v1.0.0, ADR-018; the v1.1 surface is
39
+ > **Status:** **v1.8.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
40
+ > v1.1 surface is
40
41
  > purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
41
42
  > `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
42
43
  > zeroing by default, register C-45; v1.4.0 adds generic provenance to `FrameMetadata`
43
44
  > (`run_id`/`data_version`) and publishes the shared `assert_frame_envelope` checker,
44
45
  > ADR-020; v1.5.0 adds the threshold **exceedance** estimator `P(Y > c)`, ADR-021; v1.6.0
45
46
  > adds the worst-case **expected_shortfall** estimator, ADR-022; v1.7.0 adds a third
46
- > sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023). This
47
+ > sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023; v1.8.0
48
+ > adds the native point-country broadcast + the self-describing reconciliation mode,
49
+ > the three showcase notebooks, and the frames-family hardening pass — Reconcile.md CIC,
50
+ > ADR-025 immutability-by-convention, the adversarial red-test batch). This
47
51
  > README is the design
48
52
  > bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
49
53
  > io, conformance suite) plus the `src/views_frames_summarize/` sibling package
@@ -4,14 +4,18 @@
4
4
  > containers (`FeatureFrame`, `PredictionFrame`, and their anticipated siblings)
5
5
  > that every other repo depends on and that depends on nothing internal.
6
6
  >
7
- > **Status:** **v1.7.0 — frozen API** (frozen since v1.0.0, ADR-018; the v1.1 surface is
7
+ > **Status:** **v1.8.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
8
+ > v1.1 surface is
8
9
  > purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
9
10
  > `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
10
11
  > zeroing by default, register C-45; v1.4.0 adds generic provenance to `FrameMetadata`
11
12
  > (`run_id`/`data_version`) and publishes the shared `assert_frame_envelope` checker,
12
13
  > ADR-020; v1.5.0 adds the threshold **exceedance** estimator `P(Y > c)`, ADR-021; v1.6.0
13
14
  > adds the worst-case **expected_shortfall** estimator, ADR-022; v1.7.0 adds a third
14
- > sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023). This
15
+ > sibling package **`views_frames_reconcile`** — forecast reconciliation, ADR-023; v1.8.0
16
+ > adds the native point-country broadcast + the self-describing reconciliation mode,
17
+ > the three showcase notebooks, and the frames-family hardening pass — Reconcile.md CIC,
18
+ > ADR-025 immutability-by-convention, the adversarial red-test batch). This
15
19
  > README is the design
16
20
  > bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
17
21
  > io, conformance suite) plus the `src/views_frames_summarize/` sibling package
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "views-frames"
7
- version = "1.8.0"
7
+ version = "1.8.1"
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" },
@@ -40,6 +40,20 @@ __all__ = [
40
40
  ]
41
41
 
42
42
 
43
+ def _require_assertions() -> None:
44
+ """Fail loud if assertions are stripped (``python -O``/``-OO``).
45
+
46
+ The suite is built from ``assert`` statements; under optimized bytecode every
47
+ check would silently pass regardless of conformance — a verification tool that
48
+ reports green while dead (falsify audit 2026-07, F3). Refuse to run instead.
49
+ """
50
+ if not __debug__: # pragma: no cover — pytest always runs with assertions on
51
+ raise RuntimeError(
52
+ "the views-frames conformance suite requires assertions; run without "
53
+ "python -O/-OO (PYTHONOPTIMIZE), otherwise every check silently passes"
54
+ )
55
+
56
+
43
57
  def assert_frame_envelope(frame: Any) -> None:
44
58
  """Assert ``frame`` satisfies the shared **frame envelope**.
45
59
 
@@ -56,6 +70,7 @@ def assert_frame_envelope(frame: Any) -> None:
56
70
  Raises:
57
71
  AssertionError: any part of the envelope is violated.
58
72
  """
73
+ _require_assertions()
59
74
  values = frame.values
60
75
  assert isinstance(values, np.ndarray), "values must be a numpy array"
61
76
  assert values.dtype == np.float32, f"values must be float32, got {values.dtype}"
@@ -77,6 +92,7 @@ def assert_frame_contract(frame: Any) -> None:
77
92
  Raises:
78
93
  AssertionError: the envelope or the spatiotemporal identifier rule is violated.
79
94
  """
95
+ _require_assertions()
80
96
  assert_frame_envelope(frame)
81
97
 
82
98
  ids = frame.identifiers
@@ -110,6 +126,7 @@ def assert_index_alignment_laws(index_a: Any, index_b: Any) -> None:
110
126
  Raises:
111
127
  AssertionError: a law is violated.
112
128
  """
129
+ _require_assertions()
113
130
  assert index_a.intersect(index_b) == index_b.intersect(index_a), (
114
131
  "intersect must be commutative"
115
132
  )
@@ -140,6 +157,7 @@ def assert_cross_level_alignment_law(
140
157
  AssertionError: the remap disagrees with the mapping, drops time, or
141
158
  produces the wrong level.
142
159
  """
160
+ _require_assertions()
143
161
  aligned = index.cross_level_align(mapping, target_level)
144
162
  assert aligned.level is target_level, "cross_level_align must carry target_level"
145
163
  assert np.array_equal(aligned.time, index.time), "cross_level_align must keep time"
@@ -132,6 +132,11 @@ class SpatioTemporalIndex:
132
132
  The pure-numpy analogue of ``pd.Index.get_indexer``: a same-level join.
133
133
  """
134
134
  self._require_same_level(other)
135
+ if self.n_rows == 0:
136
+ # An empty index contains nothing: every row of `other` is absent. The
137
+ # general path below would clip positions to -1 and crash with an
138
+ # obscure IndexError on the empty array (falsify audit 2026-07, F4).
139
+ return np.full(other.n_rows, -1, dtype=np.intp)
135
140
  self_rows = self._row_view(self._keys())
136
141
  other_rows = self._row_view(other._keys())
137
142
  order = np.argsort(self_rows, kind="stable")
@@ -20,6 +20,19 @@ from views_frames import PredictionFrame, SpatialLevel
20
20
  from views_frames_reconcile.module import ReconciliationModule
21
21
 
22
22
 
23
+ def _require_assertions() -> None:
24
+ """Fail loud if assertions are stripped (``python -O``/``-OO``).
25
+
26
+ Mirrors ``views_frames.conformance._require_assertions`` (falsify audit 2026-07,
27
+ F3): under optimized bytecode every ``assert`` silently passes — refuse to run.
28
+ """
29
+ if not __debug__: # pragma: no cover — pytest always runs with assertions on
30
+ raise RuntimeError(
31
+ "the reconcile conformance suite requires assertions; run without "
32
+ "python -O/-OO (PYTHONOPTIMIZE), otherwise every check silently passes"
33
+ )
34
+
35
+
23
36
  def assert_reconcile_contract(
24
37
  cm_frame: PredictionFrame,
25
38
  pgm_frame: PredictionFrame,
@@ -35,6 +48,7 @@ def assert_reconcile_contract(
35
48
  Raises:
36
49
  AssertionError: a contract law is violated.
37
50
  """
51
+ _require_assertions()
38
52
  mk = np.asarray(map_keys)
39
53
  mv = np.asarray(map_vals)
40
54
  out = ReconciliationModule(mk, mv).reconcile(cm_frame, pgm_frame)
@@ -9,6 +9,11 @@ stay zero; country totals are authoritative; the result is non-negative.
9
9
  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
+ One deliberate deviation (falsify audit 2026-07): the original's ``+ 1e-8``
13
+ denominator epsilon is replaced by an explicit all-zero-draw guard — bit-identical
14
+ on all realistic data (the epsilon was a float32 no-op for draw sums ≳ 0.1) but
15
+ exactly conserving for tiny nonzero sums, where the epsilon silently deflated the
16
+ scale factor. Negative country totals now fail loud instead of silently clamping.
12
17
  It is intentionally the **same** method — a pragmatic per-draw approximation, not
13
18
  principled joint probabilistic reconciliation. The upgrade to the latter is designed
14
19
  in **ADR-024** (register **C-62**; the cross-repo lineage is views-postprocessing
@@ -27,8 +32,6 @@ from typing import Any
27
32
  import numpy as np
28
33
  from numpy.typing import NDArray
29
34
 
30
- _EPS = np.float32(1e-8)
31
-
32
35
 
33
36
  def reconcile_proportional(
34
37
  grid: NDArray[np.floating[Any]] | object,
@@ -49,7 +52,8 @@ def reconcile_proportional(
49
52
  are clamped to be non-negative.
50
53
 
51
54
  Raises:
52
- ValueError: the grid and country sample counts disagree.
55
+ ValueError: the grid and country sample counts disagree, or any country
56
+ total is negative (cannot be conserved under the non-negativity clamp).
53
57
  """
54
58
  grid_arr = np.asarray(grid, dtype=np.float32)
55
59
  is_point = grid_arr.ndim == 1
@@ -65,13 +69,32 @@ def reconcile_proportional(
65
69
  f"Mismatch in sample count: grid has {grid_arr.shape[0]}, "
66
70
  f"country has {country_arr.shape[0]}"
67
71
  )
72
+ if bool((country_arr < 0).any()):
73
+ # A negative total cannot be conserved under the non-negativity clamp — the
74
+ # output would silently sum to 0, not the total (falsify audit 2026-07, F8).
75
+ raise ValueError(
76
+ "country totals must be non-negative; got a negative total "
77
+ f"(min={float(country_arr.min())}). A negative forecast is an upstream "
78
+ "bug; reconciliation cannot conserve it under the non-negativity clamp."
79
+ )
68
80
 
69
81
  # Preserve zeros: only strictly-positive cells carry probability mass.
70
82
  nonzero = np.where(grid_arr > 0, grid_arr, np.float32(0.0))
71
83
 
72
- # Per-draw proportional scaling to the (authoritative) country total.
84
+ # Per-draw proportional scaling to the (authoritative) country total. Guard the
85
+ # all-zero draws explicitly (they carry no mass and stay zero — the documented
86
+ # edge) instead of an additive epsilon in the denominator: `+ 1e-8` was a no-op
87
+ # in float32 for sums ≳ 0.1 (machine eps exceeds it) but silently deflated the
88
+ # scale factor for tiny nonzero sums — at a draw sum of 1e-8 a country total of
89
+ # 100 reconciled to 50 with no error signal (falsify audit 2026-07, F1). The
90
+ # exact division below is bit-identical to the epsilon form on all realistic
91
+ # data (torch-oracle parity preserved) and conserves exactly for any nonzero sum.
73
92
  sum_nonzero = nonzero.sum(axis=1, keepdims=True) # (S, 1)
74
- scaling = country_arr.reshape(-1, 1) / (sum_nonzero + _EPS) # (S, 1)
93
+ has_mass = sum_nonzero > 0
94
+ safe_sum = np.where(has_mass, sum_nonzero, np.float32(1.0))
95
+ scaling = np.where(
96
+ has_mass, country_arr.reshape(-1, 1) / safe_sum, np.float32(0.0)
97
+ ) # (S, 1)
75
98
  adjusted = np.clip(nonzero * scaling, 0.0, None).astype(np.float32)
76
99
 
77
100
  return np.asarray(adjusted[0] if is_point else adjusted, dtype=np.float32)
@@ -22,12 +22,26 @@ from views_frames_summarize.tower import hdi_tower
22
22
  from views_frames_summarize.tower_point import tower_point
23
23
 
24
24
 
25
+ def _require_assertions() -> None:
26
+ """Fail loud if assertions are stripped (``python -O``/``-OO``).
27
+
28
+ Mirrors ``views_frames.conformance._require_assertions`` (falsify audit 2026-07,
29
+ F3): under optimized bytecode every ``assert`` silently passes — refuse to run.
30
+ """
31
+ if not __debug__: # pragma: no cover — pytest always runs with assertions on
32
+ raise RuntimeError(
33
+ "the summarize conformance suite requires assertions; run without "
34
+ "python -O/-OO (PYTHONOPTIMIZE), otherwise every check silently passes"
35
+ )
36
+
37
+
25
38
  def assert_summarizer_contract(frame: AnyFrame) -> None:
26
39
  """Assert the summarizers behave on ``frame``.
27
40
 
28
41
  Raises:
29
42
  AssertionError: a summarizer violates its output contract.
30
43
  """
44
+ _require_assertions()
31
45
  n = frame.n_rows
32
46
 
33
47
  point = collapse(frame, np.mean)
File without changes
File without changes