views-frames 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,82 @@
1
+ """Published protocols — the abstract surface consumers type against.
2
+
3
+ Consumers depend on these `Protocol`s (DIP/ISP, ADR-009, README §5); a concrete
4
+ frame is an implementation detail. The surface is segregated so no consumer
5
+ depends on methods it does not use.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
12
+
13
+ import numpy as np
14
+ from numpy.typing import NDArray
15
+
16
+ from views_frames._typing import IntArray
17
+
18
+ if TYPE_CHECKING:
19
+ from views_frames.index import SpatioTemporalIndex
20
+
21
+
22
+ @runtime_checkable
23
+ class SpatioTemporalIndexed(Protocol):
24
+ """What a reconciler / aligner needs: the row identity surface."""
25
+
26
+ @property
27
+ def n_rows(self) -> int:
28
+ """Number of rows (the first axis length)."""
29
+ ...
30
+
31
+ @property
32
+ def identifiers(self) -> dict[str, IntArray]:
33
+ """The integer identifier arrays, keyed by name (e.g. ``time``, ``unit``)."""
34
+ ...
35
+
36
+ @property
37
+ def index(self) -> SpatioTemporalIndex:
38
+ """The row index — the handle for alignment (``cross_level_align`` etc.)."""
39
+ ...
40
+
41
+
42
+ @runtime_checkable
43
+ class Sampled(Protocol):
44
+ """Structural facts about the trailing sample axis (ADR-012).
45
+
46
+ Reduction over the sample axis (mean/MAP/HDI/quantiles) is **not** here — it
47
+ lives in the ``views_frames_summarize`` sibling package (ADR-017).
48
+ """
49
+
50
+ @property
51
+ def sample_count(self) -> int:
52
+ """Size of the trailing sample axis ``S`` (always ``>= 1``)."""
53
+ ...
54
+
55
+ @property
56
+ def is_sample(self) -> bool:
57
+ """True iff ``sample_count > 1``."""
58
+ ...
59
+
60
+
61
+ @runtime_checkable
62
+ class Persistable(Protocol):
63
+ """What I/O needs — and only I/O."""
64
+
65
+ def save(self, directory: Path | str) -> None:
66
+ """Serialize this frame to ``directory``."""
67
+ ...
68
+
69
+ @classmethod
70
+ def load(cls, directory: Path | str, mmap: bool = False) -> Persistable:
71
+ """Deserialize a frame from ``directory``; ``mmap`` propagates (C-07)."""
72
+ ...
73
+
74
+
75
+ @runtime_checkable
76
+ class Frame(SpatioTemporalIndexed, Protocol):
77
+ """The small composition the math layer needs: values + index + n_rows."""
78
+
79
+ @property
80
+ def values(self) -> NDArray[np.float32]:
81
+ """The contiguous ``float32`` value array (first axis = rows)."""
82
+ ...
views_frames/py.typed ADDED
File without changes
@@ -0,0 +1,41 @@
1
+ """`SpatialLevel` — the cm/pgm identifier vocabulary (ADR-015).
2
+
3
+ Labels only: `entity_column` + **time-first** `index_names`. This object never
4
+ carries the cross-level mapping (ADR-014), unit values, ranges, or the grid
5
+ backbone. stdlib only (no numpy, no domain data).
6
+
7
+ Relocated from `views-pipeline-core/.../domain/spatial.py` with the two known
8
+ defects *fixed, not ported* (ADR-015, register C-18): the index tuple is
9
+ time-first ``(month_id, entity)`` (was entity-first, C-65), and the priogrid
10
+ entity name is consistent (`priogrid_id` everywhere).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from enum import Enum
16
+
17
+
18
+ class SpatialLevel(Enum):
19
+ """Spatial level of a frame's rows: country-month or PRIO-GRID-month.
20
+
21
+ Carries the level's identifier vocabulary and nothing else.
22
+ """
23
+
24
+ CM = "cm"
25
+ PGM = "pgm"
26
+
27
+ @property
28
+ def entity_column(self) -> str:
29
+ """The unit identifier column name for this level."""
30
+ return _ENTITY_COLUMN[self]
31
+
32
+ @property
33
+ def index_names(self) -> tuple[str, str]:
34
+ """The ``(time, entity)`` index column names — **time-first** (ADR-015)."""
35
+ return ("month_id", self.entity_column)
36
+
37
+
38
+ _ENTITY_COLUMN: dict[SpatialLevel, str] = {
39
+ SpatialLevel.CM: "country_id",
40
+ SpatialLevel.PGM: "priogrid_id",
41
+ }
@@ -0,0 +1,138 @@
1
+ """`TargetFrame` — observed actuals (ground truth): ``y_true (N, 1)`` float32.
2
+
3
+ A sibling frame (no shared base; ADR-011 Option C). Structurally a
4
+ `PredictionFrame` with ``S == 1`` (the trailing sample axis is explicit; ADR-012).
5
+ Makes the evaluation boundary array-native. ``is_sample`` is always ``False``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ from numpy.typing import NDArray
14
+
15
+ from views_frames._typing import IntArray
16
+ from views_frames._validation import coerce_values, validate_values
17
+ from views_frames.index import SpatioTemporalIndex
18
+ from views_frames.io import npz
19
+ from views_frames.metadata import FrameMetadata
20
+ from views_frames.spatial_level import SpatialLevel
21
+
22
+
23
+ class TargetFrame:
24
+ """Immutable observed-actuals frame: ``(N, 1)`` float32 + a spatiotemporal index."""
25
+
26
+ def __init__(
27
+ self,
28
+ y_true: object,
29
+ index: SpatioTemporalIndex,
30
+ metadata: FrameMetadata | None = None,
31
+ ) -> None:
32
+ values = coerce_values(y_true)
33
+ validate_values(values)
34
+ if values.ndim != 2 or values.shape[1] != 1:
35
+ raise ValueError(
36
+ "TargetFrame y_true must have shape (N, 1) with an explicit "
37
+ f"trailing axis (ADR-012), got shape {values.shape}"
38
+ )
39
+ if values.shape[0] != index.n_rows:
40
+ raise ValueError(
41
+ f"y_true has {values.shape[0]} rows but index has {index.n_rows}"
42
+ )
43
+ self._values = values
44
+ self._index = index
45
+ self._metadata = metadata if metadata is not None else FrameMetadata()
46
+
47
+ # ---- core surface -------------------------------------------------------
48
+
49
+ @property
50
+ def values(self) -> NDArray[np.float32]:
51
+ """The ``(N, 1)`` float32 value array."""
52
+ return self._values
53
+
54
+ @property
55
+ def index(self) -> SpatioTemporalIndex:
56
+ """The spatiotemporal row index."""
57
+ return self._index
58
+
59
+ @property
60
+ def metadata(self) -> FrameMetadata:
61
+ """The typed provenance header."""
62
+ return self._metadata
63
+
64
+ @property
65
+ def n_rows(self) -> int:
66
+ """Number of rows ``N``."""
67
+ return int(self._values.shape[0])
68
+
69
+ @property
70
+ def identifiers(self) -> dict[str, IntArray]:
71
+ """The integer identifier arrays from the index."""
72
+ return self._index.identifiers
73
+
74
+ @property
75
+ def sample_count(self) -> int:
76
+ """Always ``1`` for a target frame."""
77
+ return 1
78
+
79
+ @property
80
+ def is_sample(self) -> bool:
81
+ """Always ``False`` — a target carries a single realized value."""
82
+ return False
83
+
84
+ def with_metadata(self, metadata: FrameMetadata) -> TargetFrame:
85
+ """Return a new frame with replaced metadata, **sharing** the values buffer."""
86
+ new = TargetFrame.__new__(TargetFrame)
87
+ new._values = self._values
88
+ new._index = self._index
89
+ new._metadata = metadata
90
+ return new
91
+
92
+ def select(self, indexer: IntArray | NDArray[np.bool_]) -> TargetFrame:
93
+ """A new frame of the rows at integer positions **or** a boolean mask.
94
+
95
+ Rows are selected by numpy fancy indexing — an integer array reorders or
96
+ repeats, a boolean mask filters. Metadata is preserved; the selection
97
+ **copies**. An empty selection yields an empty frame.
98
+ """
99
+ return TargetFrame(
100
+ self._values[indexer], self._index.select(indexer), self._metadata
101
+ )
102
+
103
+ def reindex(self, other: SpatioTemporalIndex) -> TargetFrame:
104
+ """Align this frame to ``other``'s rows, returning a new frame.
105
+
106
+ Fails loud unless this frame's index is a **superset** of ``other``. The
107
+ frame-level companion to the index's ``reindex``/``searchsorted``.
108
+ """
109
+ if not self._index.is_superset_of(other):
110
+ raise ValueError(
111
+ "reindex requires this frame's index to be a superset of `other`; "
112
+ "some target rows are absent"
113
+ )
114
+ return self.select(self._index.searchsorted(other))
115
+
116
+ # ---- persistence --------------------------------------------------------
117
+
118
+ def save(self, directory: Path | str) -> None:
119
+ """Serialize to ``directory`` (npy + npz + header)."""
120
+ npz.save(
121
+ directory,
122
+ values=self._values,
123
+ time=self._index.time,
124
+ unit=self._index.unit,
125
+ level=self._index.level.value,
126
+ metadata=self._metadata.to_dict(),
127
+ )
128
+
129
+ @classmethod
130
+ def load(cls, directory: Path | str, mmap: bool = False) -> TargetFrame:
131
+ """Deserialize a frame from ``directory``; ``mmap`` propagates."""
132
+ state = npz.load(directory, mmap=mmap)
133
+ index = SpatioTemporalIndex(
134
+ time=state["time"],
135
+ unit=state["unit"],
136
+ level=SpatialLevel(state["level"]),
137
+ )
138
+ return cls(state["values"], index, FrameMetadata.from_dict(state["metadata"]))