views-frames 1.9.0__tar.gz → 1.10.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.
Files changed (44) hide show
  1. {views_frames-1.9.0 → views_frames-1.10.0}/PKG-INFO +12 -4
  2. {views_frames-1.9.0 → views_frames-1.10.0}/README.md +11 -3
  3. {views_frames-1.9.0 → views_frames-1.10.0}/pyproject.toml +1 -1
  4. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/conformance/__init__.py +45 -0
  5. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/feature_frame.py +31 -0
  6. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/index.py +67 -12
  7. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/prediction_frame.py +31 -0
  8. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/target_frame.py +30 -0
  9. {views_frames-1.9.0 → views_frames-1.10.0}/.gitignore +0 -0
  10. {views_frames-1.9.0 → views_frames-1.10.0}/LICENSE +0 -0
  11. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/__init__.py +0 -0
  12. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/_typing.py +0 -0
  13. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/_validation.py +0 -0
  14. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/io/__init__.py +0 -0
  15. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/io/arrow.py +0 -0
  16. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/io/npz.py +0 -0
  17. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/metadata.py +0 -0
  18. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/protocols.py +0 -0
  19. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/py.typed +0 -0
  20. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames/spatial_level.py +0 -0
  21. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/__init__.py +0 -0
  22. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/conformance.py +0 -0
  23. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/frames.py +0 -0
  24. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/grouping.py +0 -0
  25. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/module.py +0 -0
  26. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/proportional.py +0 -0
  27. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/py.typed +0 -0
  28. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/result.py +0 -0
  29. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_reconcile/validation.py +0 -0
  30. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/__init__.py +0 -0
  31. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/_common.py +0 -0
  32. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/aggregate.py +0 -0
  33. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/bimodality.py +0 -0
  34. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/collapse.py +0 -0
  35. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/config.py +0 -0
  36. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/conformance.py +0 -0
  37. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/exceedance.py +0 -0
  38. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/expected_shortfall.py +0 -0
  39. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/interval.py +0 -0
  40. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/point.py +0 -0
  41. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/py.typed +0 -0
  42. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/summarize_tower.py +0 -0
  43. {views_frames-1.9.0 → views_frames-1.10.0}/src/views_frames_summarize/tower.py +0 -0
  44. {views_frames-1.9.0 → views_frames-1.10.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.9.0
3
+ Version: 1.10.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
@@ -36,7 +36,7 @@ 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.9.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
39
+ > **Status:** **v1.10.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
40
40
  > v1.1 surface is
41
41
  > purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
42
42
  > `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
@@ -50,7 +50,10 @@ Description-Content-Type: text/markdown
50
50
  > ADR-025 immutability-by-convention, the adversarial red-test batch; v1.9.0 moves the
51
51
  > tower-tip MAP to the **top floor** of the published tower — `tip_mass` 0.25 with the
52
52
  > **MAP-containment law** in the conformance suite, ADR-019 Amendment 3 — and adds the
53
- > `research/figures/` tower-figure toolkit). This
53
+ > `research/figures/` tower-figure toolkit; v1.10.0 adds the **dense-grid fill**
54
+ > primitive — `reindex_fill(other, *, fill_value)` on all three frames +
55
+ > `SpatioTemporalIndex.cartesian` + the published `assert_reindex_fill_law`, ADR-026,
56
+ > unblocking pandas-free FAO ingestion). This
54
57
  > README is the design
55
58
  > bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
56
59
  > io, conformance suite) plus the `src/views_frames_summarize/` sibling package
@@ -346,7 +349,12 @@ genuinely reused core. Build this once:
346
349
  `(time, unit)` **at a single `SpatialLevel`**. **This is the label-alignment
347
350
  that today drags pandas back in** — pred↔actual join, partial-overlap
348
351
  evaluation, same-level reindex. This alignment logic lives in the leaf
349
- unconditionally.
352
+ unconditionally. The dense-grid pair rides the same join (ADR-026):
353
+ `cartesian(times, units, level)` builds the time-major product index
354
+ (explicit arrays only — deriving them is consumer policy), and the frames'
355
+ `reindex_fill(other, *, fill_value)` aligns with **no** superset requirement,
356
+ filling absent rows (present rows bit-exact; law-pinned by
357
+ `assert_reindex_fill_law`).
350
358
  - **Cross-level operations (`cross_level_align`) — protocol here, data injected.**
351
359
  The cm↔pgm **cross-level join** (country↔grid) is **not** a same-axis set op; it
352
360
  is a one-to-many lookup against a `priogrid→country` mapping that is **injected**
@@ -4,7 +4,7 @@
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.9.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
7
+ > **Status:** **v1.10.0 — frozen API, published to PyPI** (frozen since v1.0.0, ADR-018; the
8
8
  > v1.1 surface is
9
9
  > purely additive — the coherent posterior summary, ADR-019; v1.2.0 rebuilt the tower
10
10
  > `outside-in`, C-44; v1.3.0 makes the tower summary distribution-agnostic — no magnitude
@@ -18,7 +18,10 @@
18
18
  > ADR-025 immutability-by-convention, the adversarial red-test batch; v1.9.0 moves the
19
19
  > tower-tip MAP to the **top floor** of the published tower — `tip_mass` 0.25 with the
20
20
  > **MAP-containment law** in the conformance suite, ADR-019 Amendment 3 — and adds the
21
- > `research/figures/` tower-figure toolkit). This
21
+ > `research/figures/` tower-figure toolkit; v1.10.0 adds the **dense-grid fill**
22
+ > primitive — `reindex_fill(other, *, fill_value)` on all three frames +
23
+ > `SpatioTemporalIndex.cartesian` + the published `assert_reindex_fill_law`, ADR-026,
24
+ > unblocking pandas-free FAO ingestion). This
22
25
  > README is the design
23
26
  > bible; the contract it specifies is realised in `src/views_frames/` (index, frames,
24
27
  > io, conformance suite) plus the `src/views_frames_summarize/` sibling package
@@ -314,7 +317,12 @@ genuinely reused core. Build this once:
314
317
  `(time, unit)` **at a single `SpatialLevel`**. **This is the label-alignment
315
318
  that today drags pandas back in** — pred↔actual join, partial-overlap
316
319
  evaluation, same-level reindex. This alignment logic lives in the leaf
317
- unconditionally.
320
+ unconditionally. The dense-grid pair rides the same join (ADR-026):
321
+ `cartesian(times, units, level)` builds the time-major product index
322
+ (explicit arrays only — deriving them is consumer policy), and the frames'
323
+ `reindex_fill(other, *, fill_value)` aligns with **no** superset requirement,
324
+ filling absent rows (present rows bit-exact; law-pinned by
325
+ `assert_reindex_fill_law`).
318
326
  - **Cross-level operations (`cross_level_align`) — protocol here, data injected.**
319
327
  The cm↔pgm **cross-level join** (country↔grid) is **not** a same-axis set op; it
320
328
  is a one-to-many lookup against a `priogrid→country` mapping that is **injected**
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "views-frames"
7
- version = "1.9.0"
7
+ version = "1.10.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" },
@@ -37,6 +37,7 @@ __all__ = [
37
37
  "assert_frame_contract",
38
38
  "assert_frame_envelope",
39
39
  "assert_index_alignment_laws",
40
+ "assert_reindex_fill_law",
40
41
  ]
41
42
 
42
43
 
@@ -136,6 +137,50 @@ def assert_index_alignment_laws(index_a: Any, index_b: Any) -> None:
136
137
  assert np.array_equal(index_a.unit[pos], index_a.unit), "searchsorted self-identity"
137
138
 
138
139
 
140
+ def assert_reindex_fill_law(frame: Any, target: Any, fill_value: float) -> None:
141
+ """Assert the dense-grid fill law (ADR-026) for ``frame`` against ``target``.
142
+
143
+ ``reindex_fill`` aligns a frame to a target index with **no** superset
144
+ requirement, filling absent rows. The law:
145
+
146
+ - the result's index equals ``target`` row-for-row (time, unit, level);
147
+ - every target row present in ``frame`` comes through **bit-exact**;
148
+ - every absent row equals ``fill_value`` exactly (NaN-safe comparison);
149
+ - when every target row is present, ``reindex_fill`` degenerates to
150
+ ``reindex`` (filling a superset frame adds nothing).
151
+
152
+ Args:
153
+ frame: a frame exposing ``reindex_fill``/``reindex``/``index``/``values``.
154
+ target: a ``SpatioTemporalIndex`` (same level) to densify against.
155
+ fill_value: the fill for absent rows (``NaN`` is legal).
156
+
157
+ Raises:
158
+ AssertionError: a law is violated.
159
+ """
160
+ _require_assertions()
161
+ filled = frame.reindex_fill(target, fill_value=fill_value)
162
+ assert filled.index.level == target.level, "reindex_fill must keep the level"
163
+ assert np.array_equal(filled.index.time, target.time), (
164
+ "reindex_fill result index must equal the target (time)"
165
+ )
166
+ assert np.array_equal(filled.index.unit, target.unit), (
167
+ "reindex_fill result index must equal the target (unit)"
168
+ )
169
+ pos = frame.index.searchsorted(target)
170
+ found = pos >= 0
171
+ assert np.array_equal(
172
+ filled.values[found], frame.values[pos[found]], equal_nan=True
173
+ ), "reindex_fill must pass present rows through bit-exact"
174
+ expected = np.full_like(filled.values[~found], np.float32(fill_value))
175
+ assert np.array_equal(filled.values[~found], expected, equal_nan=True), (
176
+ "reindex_fill must set every absent row to fill_value"
177
+ )
178
+ if bool(found.all()):
179
+ assert np.array_equal(
180
+ filled.values, frame.reindex(target).values, equal_nan=True
181
+ ), "reindex_fill on a superset frame must equal reindex"
182
+
183
+
139
184
  def assert_cross_level_alignment_law(
140
185
  index: Any, mapping: Any, target_level: Any
141
186
  ) -> None:
@@ -154,6 +154,37 @@ class FeatureFrame:
154
154
  )
155
155
  return self.select(self._index.searchsorted(other))
156
156
 
157
+ def reindex_fill(
158
+ self, other: SpatioTemporalIndex, *, fill_value: float
159
+ ) -> FeatureFrame:
160
+ """Align to ``other``'s rows, filling rows absent from this frame.
161
+
162
+ The dense-grid companion to :meth:`reindex` (ADR-026): every row of
163
+ ``other`` present in this frame comes through **bit-exact**; a row
164
+ absent from it gets ``fill_value``, broadcast across the feature and
165
+ sample axes. There is no superset requirement — that is the difference
166
+ from the fail-loud ``reindex``. The result's index **is** ``other``
167
+ (immutable, shared); ``feature_names`` and metadata are preserved.
168
+
169
+ Same-level join semantics: assumes unique ``(time, unit)`` rows in
170
+ **this** frame (register C-21); ``other`` may repeat rows. Allocates
171
+ the full ``(other.n_rows, F, S)`` buffer — densifying a large grid is
172
+ a deliberate, costly act. A ``NaN`` fill is legal here, but the
173
+ summarize estimators fail loud on NaN draws by design (ADR-017).
174
+
175
+ Raises:
176
+ ValueError: ``other`` is at a different ``SpatialLevel``.
177
+ """
178
+ pos = self._index.searchsorted(other)
179
+ found = pos >= 0
180
+ out = np.full(
181
+ (other.n_rows, *self._values.shape[1:]),
182
+ np.float32(fill_value),
183
+ dtype=np.float32,
184
+ )
185
+ out[found] = self._values[pos[found]]
186
+ return FeatureFrame(out, other, self._feature_names, self._metadata)
187
+
157
188
  # ---- persistence --------------------------------------------------------
158
189
 
159
190
  def save(self, directory: Path | str) -> None:
@@ -43,9 +43,7 @@ class SpatioTemporalIndex:
43
43
  level: SpatialLevel,
44
44
  ) -> None:
45
45
  if not isinstance(level, SpatialLevel):
46
- raise TypeError(
47
- f"level must be a SpatialLevel, got {type(level).__name__}"
48
- )
46
+ raise TypeError(f"level must be a SpatialLevel, got {type(level).__name__}")
49
47
  is_array = isinstance(time, np.ndarray) and time.ndim >= 1
50
48
  n = int(time.shape[0]) if is_array else -1
51
49
  validate_identifiers({"time": time, "unit": unit}, n_rows=n)
@@ -56,6 +54,65 @@ class SpatioTemporalIndex:
56
54
  self._unit.setflags(write=False)
57
55
  self._level = level
58
56
 
57
+ # ---- construction -------------------------------------------------------
58
+
59
+ @classmethod
60
+ def cartesian(
61
+ cls,
62
+ times: IntArray,
63
+ units: IntArray,
64
+ level: SpatialLevel,
65
+ ) -> SpatioTemporalIndex:
66
+ """The dense ``(time × unit)`` product index, in time-major order.
67
+
68
+ The dense-grid constructor (ADR-026): every combination of the given
69
+ ``times`` and ``units``, ordered time-major (all units of the first time,
70
+ then all units of the next), so dense indices are canonical across
71
+ consumers. The inputs are **explicit arrays only** — any rule for
72
+ deriving them (e.g. "the units present in the last time step") is
73
+ consumer policy and never lives in the leaf.
74
+
75
+ Fails loud on duplicated ``times`` or ``units``: a duplicated product
76
+ input manufactures duplicate ``(time, unit)`` rows, which make every
77
+ same-level join undefined (register C-21) — always a caller bug here.
78
+
79
+ Allocates ``len(times) × len(units)`` rows eagerly; densifying a large
80
+ grid is a deliberate, costly act (a full-pgm dense grid runs to tens of
81
+ millions of rows).
82
+
83
+ Args:
84
+ times: 1-D integer array of unique time identifiers.
85
+ units: 1-D integer array of unique unit identifiers.
86
+ level: The ``SpatialLevel`` of the produced index.
87
+
88
+ Raises:
89
+ TypeError: ``times``/``units`` is not an integer-dtype numpy array,
90
+ or ``level`` is not a ``SpatialLevel``.
91
+ ValueError: an input array is not 1-D or contains duplicate values.
92
+ """
93
+ for name, arr in (("times", times), ("units", units)):
94
+ if not isinstance(arr, np.ndarray):
95
+ raise TypeError(
96
+ f"cartesian {name} must be a numpy array, got {type(arr).__name__}"
97
+ )
98
+ if not np.issubdtype(arr.dtype, np.integer):
99
+ raise TypeError(
100
+ f"cartesian {name} must be an integer dtype, got {arr.dtype}"
101
+ )
102
+ if arr.ndim != 1:
103
+ raise ValueError(f"cartesian {name} must be 1-D, got ndim={arr.ndim}")
104
+ if np.unique(arr).shape[0] != arr.shape[0]:
105
+ raise ValueError(
106
+ f"cartesian {name} contains duplicate values; a duplicated "
107
+ "product input manufactures duplicate (time, unit) rows, "
108
+ "which make same-level joins undefined (register C-21)"
109
+ )
110
+ return cls(
111
+ time=np.repeat(times, units.shape[0]),
112
+ unit=np.tile(units, times.shape[0]),
113
+ level=level,
114
+ )
115
+
59
116
  # ---- core surface -------------------------------------------------------
60
117
 
61
118
  @property
@@ -109,9 +166,11 @@ class SpatioTemporalIndex:
109
166
  @staticmethod
110
167
  def _row_view(keys: NDArray[np.int64]) -> NDArray[np.void]:
111
168
  """View each ``(time, unit)`` row as a single void scalar for set ops."""
112
- return np.ascontiguousarray(keys).view(
113
- np.dtype((np.void, keys.dtype.itemsize * keys.shape[1]))
114
- ).reshape(-1)
169
+ return (
170
+ np.ascontiguousarray(keys)
171
+ .view(np.dtype((np.void, keys.dtype.itemsize * keys.shape[1])))
172
+ .reshape(-1)
173
+ )
115
174
 
116
175
  def _require_same_level(self, other: SpatioTemporalIndex) -> None:
117
176
  if self._level != other._level:
@@ -180,9 +239,7 @@ class SpatioTemporalIndex:
180
239
  rows = self._row_view(self._keys())
181
240
  return bool(len(np.unique(rows)) == rows.shape[0])
182
241
 
183
- def select(
184
- self, indexer: IntArray | NDArray[np.bool_]
185
- ) -> SpatioTemporalIndex:
242
+ def select(self, indexer: IntArray | NDArray[np.bool_]) -> SpatioTemporalIndex:
186
243
  """A new index of the rows at integer positions **or** a boolean mask.
187
244
 
188
245
  The row-selection primitive the frame-level ``select``/``reindex`` build on:
@@ -298,9 +355,7 @@ class SpatioTemporalIndex:
298
355
  sorted_rows = map_rows[order]
299
356
 
300
357
  self_rows = self._row_view(self._keys())
301
- pos = np.clip(
302
- np.searchsorted(sorted_rows, self_rows), 0, len(sorted_rows) - 1
303
- )
358
+ pos = np.clip(np.searchsorted(sorted_rows, self_rows), 0, len(sorted_rows) - 1)
304
359
  found = sorted_rows[pos] == self_rows
305
360
  if not bool(found.all()):
306
361
  miss = int(np.argmax(~found))
@@ -118,6 +118,37 @@ class PredictionFrame:
118
118
  )
119
119
  return self.select(self._index.searchsorted(other))
120
120
 
121
+ def reindex_fill(
122
+ self, other: SpatioTemporalIndex, *, fill_value: float
123
+ ) -> PredictionFrame:
124
+ """Align to ``other``'s rows, filling rows absent from this frame.
125
+
126
+ The dense-grid companion to :meth:`reindex` (ADR-026): every row of
127
+ ``other`` present in this frame comes through **bit-exact**; a row
128
+ absent from it gets ``fill_value``, broadcast across the sample axis.
129
+ There is no superset requirement — that is the difference from the
130
+ fail-loud ``reindex``. The result's index **is** ``other`` (immutable,
131
+ shared); metadata is preserved.
132
+
133
+ Same-level join semantics: assumes unique ``(time, unit)`` rows in
134
+ **this** frame (register C-21); ``other`` may repeat rows. Allocates
135
+ the full ``(other.n_rows, S)`` buffer — densifying a large grid is a
136
+ deliberate, costly act. A ``NaN`` fill is legal here, but the
137
+ summarize estimators fail loud on NaN draws by design (ADR-017).
138
+
139
+ Raises:
140
+ ValueError: ``other`` is at a different ``SpatialLevel``.
141
+ """
142
+ pos = self._index.searchsorted(other)
143
+ found = pos >= 0
144
+ out = np.full(
145
+ (other.n_rows, *self._values.shape[1:]),
146
+ np.float32(fill_value),
147
+ dtype=np.float32,
148
+ )
149
+ out[found] = self._values[pos[found]]
150
+ return PredictionFrame(out, other, self._metadata)
151
+
121
152
  # ---- persistence --------------------------------------------------------
122
153
 
123
154
  def save(self, directory: Path | str) -> None:
@@ -113,6 +113,36 @@ class TargetFrame:
113
113
  )
114
114
  return self.select(self._index.searchsorted(other))
115
115
 
116
+ def reindex_fill(
117
+ self, other: SpatioTemporalIndex, *, fill_value: float
118
+ ) -> TargetFrame:
119
+ """Align to ``other``'s rows, filling rows absent from this frame.
120
+
121
+ The dense-grid companion to :meth:`reindex` (ADR-026): every row of
122
+ ``other`` present in this frame comes through **bit-exact**; a row
123
+ absent from it gets ``fill_value``. There is no superset requirement —
124
+ that is the difference from the fail-loud ``reindex``. The result's
125
+ index **is** ``other`` (immutable, shared); metadata is preserved.
126
+
127
+ Same-level join semantics: assumes unique ``(time, unit)`` rows in
128
+ **this** frame (register C-21); ``other`` may repeat rows. Allocates
129
+ the full ``(other.n_rows, 1)`` buffer — densifying a large grid is a
130
+ deliberate, costly act. A ``NaN`` fill is legal here, but the
131
+ summarize estimators fail loud on NaN draws by design (ADR-017).
132
+
133
+ Raises:
134
+ ValueError: ``other`` is at a different ``SpatialLevel``.
135
+ """
136
+ pos = self._index.searchsorted(other)
137
+ found = pos >= 0
138
+ out = np.full(
139
+ (other.n_rows, *self._values.shape[1:]),
140
+ np.float32(fill_value),
141
+ dtype=np.float32,
142
+ )
143
+ out[found] = self._values[pos[found]]
144
+ return TargetFrame(out, other, self._metadata)
145
+
116
146
  # ---- persistence --------------------------------------------------------
117
147
 
118
148
  def save(self, directory: Path | str) -> None:
File without changes
File without changes