py4dggs 0.2.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.
py4dggs-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,334 @@
1
+ Metadata-Version: 2.3
2
+ Name: py4dggs
3
+ Version: 0.2.0
4
+ Summary: Canonical, readable, pure-Python multi-grid DGGS reference.
5
+ Author: Jorge S. Mendes de Jesus
6
+ Author-email: Jorge S. Mendes de Jesus <jorge@terraops.org>
7
+ License: MIT
8
+ Requires-Python: >=3.12
9
+ Project-URL: Repository, https://github.com/terraops-org/py4dggs
10
+ Description-Content-Type: text/markdown
11
+
12
+ # to.py4dggs-py
13
+
14
+ *Born 7th July 2026, Aveiro, Portugal @ 18:50 WET*
15
+
16
+ Canonical, readable, pure-Python multi-grid DGGS reference library (package name `py4dggs`).
17
+
18
+ This library provides a clean-room Python implementation of Discrete Global Grid Systems (DGGS),
19
+ designed for clarity, correctness, and zero runtime dependencies (no sibling-repo dependencies
20
+ either - every grid, IGEO7 included, is verified against **pydggal**, the DGGAL engine's own
21
+ Python binding, a normal PyPI dev dependency). IGEO7's Z7 quantization/centroid/hierarchy/text-id
22
+ arithmetic was originally written to be bit-identical to a frozen Python port (`igeo7-py`, a
23
+ separate single-grid sibling repo) - that historical provenance is still true of the code, but the
24
+ sibling-repo *test dependency* itself was removed (2026-07-07); see `tests/test_isea7h_fuzz.py` for
25
+ the live pydggal-based verification and `tests/test_z7.py` for the self-consistency checks on the
26
+ pure bit-packing (which needs no external oracle - it's a definitional scheme, not a geometric one).
27
+
28
+ **The interruption-vertex fix (2026-07-02):** `igeo7-py` (and the JS port it descends from)
29
+ dropped the eC's i1/i2 interruption-frame reconciliation (`RI7H.ec:1991-2004`) in
30
+ `addNonPolarBaseVertices`, so ~0.3% of cells - those whose boundary spans a rhombus
31
+ interruption - got wrong / degenerate `(0,0)` "far-side" vertices, cascading into wrong k-ring
32
+ neighbours. `py4dggs` ports the eC faithfully, so for those cells it matches DGGAL/pydggal (ground
33
+ truth), NOT the buggy `igeo7-py`. See `tests/test_igeo7_vertices_pydggal.py`.
34
+
35
+ See "Verification against pydggal" below for how every grid (IGEO7 included) is tested, and the
36
+ "Golden tables are vendored" note there the conformance fixtures ship in this repo, so no
37
+ sibling repo or credential is needed to run the full suite.
38
+
39
+ ## Quickstart
40
+
41
+ Python 3.12 or newer, and zero runtime dependencies, so nothing else gets
42
+ pulled in:
43
+
44
+ ```bash
45
+ uv add py4dggs # or: pip install py4dggs
46
+ ```
47
+
48
+ To work on the library itself, or to run its test suite, install from a clone
49
+ instead:
50
+
51
+ ```bash
52
+ git clone https://github.com/terraops-org/py4dggs.git
53
+ cd py4dggs
54
+ uv sync # creates .venv/ and installs the dev extras
55
+ ```
56
+
57
+ `uv sync` also brings in the dev dependencies: `pytest`, and `dggal` (pydggal,
58
+ the DGGAL engine's own Python binding), which the suite uses as its correctness
59
+ oracle. If you would rather not use `uv`:
60
+
61
+ ```bash
62
+ python -m venv .venv && source .venv/bin/activate
63
+ pip install -e . # add: pip install pytest dggal==0.0.6 to run the tests
64
+ ```
65
+
66
+ Confirm the install, and that this machine reproduces the verified behaviour:
67
+
68
+ ```bash
69
+ uv run python -c "import py4dggs; print(py4dggs.__all__)"
70
+ uv run pytest -q # the full suite, ~4300 tests in ~11s
71
+ uv run python examples/01_first_zone.py # a first runnable example
72
+ ```
73
+
74
+ From there, `uv run python` drops you into a REPL with the package importable,
75
+ or activate `.venv` and use plain `python`.
76
+
77
+ Runnable scripts live in **`examples/`** (`uv run python examples/01_first_zone.py`); see
78
+ `examples/README.md` for the list. They and every code block in this file are executed by the test
79
+ suite, so they cannot silently go stale.
80
+
81
+ See **`TUTORIAL.md`** for a longer, task-oriented walkthrough with worked examples (choosing a
82
+ grid, neighbours, hierarchy, sub-zones/tiling, error handling), and `ARCHITECTURE.md` for how the
83
+ pieces below fit together and how to read the source.
84
+
85
+ ```python
86
+ from py4dggs import IGEO7, ISEA3H, get_grid
87
+
88
+ # Encode a geographic point into a zone at a given resolution
89
+ lisbon = IGEO7.zone_from_geo(lat=38.7223, lon=-9.1393, res=7)
90
+ print(lisbon.text_id) # "006415654" -- IGEO7's canonical Z7 digit id
91
+ print(lisbon.value) # 940643742434459647 -- packed int (== pydggal's DGGRSZone int)
92
+ print(lisbon.resolution) # 7
93
+ print(lisbon.is_pentagon) # False
94
+
95
+ # Geometry (always WGS84 GeoPoint(lat, lon))
96
+ print(lisbon.centroid) # GeoPoint(lat=38.74412563021016, lon=-9.138855236620287)
97
+ print(len(lisbon.vertices)) # 6 (or 5 for a pentagon)
98
+
99
+ # Round-trip through a zone's canonical text id
100
+ same = IGEO7.zone_from_text(lisbon.text_id)
101
+ assert same == lisbon
102
+
103
+ # Topology: neighbours, hierarchy
104
+ print(len(lisbon.neighbors)) # 6 (or 5 for a pentagon)
105
+ parent = lisbon.parent # one level coarser (None at resolution 0)
106
+ children = lisbon.children # one level finer, 7 of them (6 for a pentagon)
107
+
108
+ # A different grid family: ISEA3H (aperture-3, rhombic addressing)
109
+ rome = ISEA3H.zone_from_geo(lat=41.9028, lon=12.4964, res=6)
110
+ print(rome.text_id) # "D4-83-A" -- rhombic id, NOT a digit path
111
+ print(rome.parents) # (Zone('C4-10-C'),) -- 1 or 3 Zones (non-congruent hierarchy)
112
+
113
+ # Sub-zones -- the DGGS-as-storage primitive (I3H grids only, see "Sub-zones" below)
114
+ depth = 2
115
+ tile_size = rome.count_sub_zones(depth) # 13 -- fixed array length for this "tile"
116
+ slots = rome.sub_zones(depth) # ordered tuple[Zone, ...], len == tile_size
117
+ assert rome.sub_zone_index(slots[0]) == 0
118
+ assert rome.sub_zone_at_index(depth, 0) == slots[0]
119
+
120
+ # Grids can also be looked up by name (useful when the grid is chosen at runtime)
121
+ assert get_grid("ISEA3H") is ISEA3H
122
+ ```
123
+
124
+ (All values above are actual output from running this exact snippet - not illustrative placeholders.)
125
+
126
+ All six registered grids: `IGEO7`, `IVEA7H`, `RTEA7H` (aperture-7, Z7 digit addressing) and
127
+ `ISEA3H`, `IVEA3H`, `RTEA3H` (aperture-3, I3H rhombic addressing) - see "Grids" below for what
128
+ distinguishes each. Every grid exposes the same `Zone`/`Grid` API shown above; what differs is
129
+ which pieces are exact vs. approximated (the "Verification against pydggal" section) and which
130
+ optional capabilities a grid's `Topology` provides (exact neighbours, geometric hierarchy,
131
+ sub-zones - all six now have all three, per "Verification against pydggal" and "Sub-zones" below).
132
+
133
+ ## Grids
134
+
135
+ `py4dggs` is multi-grid. Each grid composes a `Projection` + `Topology` + `Indexing`:
136
+
137
+ - **`IGEO7`** - ISEA projection + aperture-7 hex topology + Z7 indexing (the IGEO7/ISEA7H grid).
138
+ - **`IVEA7H`** - the **IVEA** projection variant with the *same* aperture-7 hex topology + Z7
139
+ indexing. Adding it changed **zero** of the topology/indexing/grid layers - only the projection
140
+ (see below). Get either via `from py4dggs import IGEO7, IVEA7H` or `get_grid("IVEA7H")`.
141
+ - **`RTEA7H`** - the **RTEA** projection variant, the third `VGCRadialVertex` case, again on the
142
+ *same* aperture-7 hex topology + Z7 indexing. Like `IVEA7H`, adding it changed only the
143
+ projection (`variant_consts("rtea")` in `icovertex.py` + a thin `rtea.py` + one registry line) -
144
+ zero changes to topology/indexing/grid. Get it via `from py4dggs import RTEA7H` or
145
+ `get_grid("RTEA7H")`.
146
+ - **`ISEA3H`** - ISEA projection + **aperture-3** rhombic topology + packed **I3H** rhombic indexing.
147
+ This is a second-topology grid, DGGAL-verified (against `pydggal` ISEA3H) for quantization,
148
+ centroids, vertices, **exact topological neighbours** (sub-project A slice **A0**), the
149
+ **canonical rhombic text-id** `"C2-23-C"` (slice **A1**), the **non-congruent geometric
150
+ hierarchy** - `parents` (1 or 3), geometric `children`, `centroid_parent`/`is_centroid_child`
151
+ (slice **A2**) - and **sub-zones** (OGC descendants-at-depth; slice **A3**, see "Sub-zones"
152
+ below). Get it via `from py4dggs import ISEA3H` or `get_grid("ISEA3H")`.
153
+ - **`IVEA3H`** - the **IVEA** projection variant on the *same* aperture-3 rhombic topology + I3H
154
+ indexing as `ISEA3H`. A pure combination - one registry line, zero new topology/indexing code
155
+ (`HexAperture3Topology`/`I3HIndexing` are already projection-agnostic). DGGAL-verified
156
+ (against `pydggal` IVEA3H) for quantization, centroids, vertices, exact neighbours (A0), the
157
+ canonical rhombic text-id (A1), the non-congruent geometric hierarchy (A2), and sub-zones (A3).
158
+ Get it via `from py4dggs import IVEA3H` or `get_grid("IVEA3H")`.
159
+ - **`RTEA3H`** - the **RTEA** projection variant on the *same* aperture-3 rhombic topology + I3H
160
+ indexing as `ISEA3H`/`IVEA3H`. Again a pure combination - one registry line, zero new
161
+ topology/indexing code. DGGAL-verified (against `pydggal` RTEA3H) for quantization,
162
+ centroids, vertices, exact neighbours (A0), the canonical rhombic text-id (A1), the
163
+ non-congruent geometric hierarchy (A2), and sub-zones (A3).
164
+ Get it via `from py4dggs import RTEA3H` or `get_grid("RTEA3H")`. `RTEA3H` is the final cell of the
165
+ projection × aperture matrix - **ISEA/IVEA/RTEA × 7H/3H**, all six grids now registered, and
166
+ all six now carry the full sub-project A surface (exact neighbours, text-id, geometric
167
+ hierarchy, sub-zones).
168
+
169
+ The projection layer is a shared kernel (`projections/icovertex.py`) parameterized by a
170
+ `radial_vertex` variant, with thin per-variant classes (`isea.py`, `ivea.py`, `rtea.py`) -
171
+ mirroring DGGAL's `SliceAndDiceGreatCircleIcosahedralProjection` + `VGCRadialVertex {isea, ivea,
172
+ rtea}`. All three variants (IGEO7/IVEA7H/RTEA7H) now drop into this same kernel.
173
+
174
+ ## Verification against pydggal (DGGAL ground truth)
175
+
176
+ Every grid in this librar, IGEO7 included, is verified directly against **pydggal**, the DGGAL
177
+ Python binding, i.e. the canonical engine itself. Install it as a dev dependency:
178
+ ```
179
+ uv add --dev dggal # dggal==0.0.6
180
+ ```
181
+
182
+ **Golden tables are vendored, so nothing external is required.** The golden-table tests below
183
+ (`test_conformance.py` and its per-grid siblings) read frozen fixtures from `tests/golden/`, which
184
+ ships in this repo along with the `generate.py` that produced them. So `git clone && uv sync && uv
185
+ run pytest` runs the complete suitem, no sibling repo, no credential, no configuration. The tables
186
+ originate from [igeo7-spec](https://github.com/terraops-org/igeo7-spec) (private) and were vendored
187
+ 2026-07-29; `tests/golden/PROVENANCE.md` records the source commit and the regeneration commands.
188
+ The path stays overridable per-grid with `DGGS_GOLDEN_TABLES[_<GRID>]=...` for an out-of-tree table
189
+ set.
190
+
191
+ **`IGEO7`** is verified two ways (the tables ship in-repo; the live-oracle half skips cleanly if `dggal` is absent):
192
+
193
+ - **Live differential fuzz** (`tests/test_isea7h_fuzz.py`) - forward / centroid / vertices /
194
+ neighbours vs pydggal; hierarchy (parent/children) is checked structurally instead, since Z7's
195
+ congruent digit-path hierarchy has no pydggal equivalent (pydggal's own `getZoneParent`/
196
+ `getZoneChildren` return DGGAL's *geometric* Z7 hierarchy, a different, non-congruent relation).
197
+ - **Golden-table conformance** (`tests/test_conformance.py` → **581 passed, 7 xfailed**) -
198
+ against `tests/golden/`. Override the path with `export DGGS_GOLDEN_TABLES=...`. The
199
+ 7 xfails are benign pole/vertex-0 (lon 11.2°) boundary tie-breaks.
200
+
201
+ **`IVEA7H`** is verified two ways (the tables ship in-repo; the live-oracle half skips cleanly if `dggal` is absent):
202
+
203
+ - **Live differential fuzz** (`tests/test_ivea7h_fuzz.py`) - forward / centroid / vertices /
204
+ neighbours vs pydggal. Discrete outputs match exactly; float geometry agrees to ~1e-9°
205
+ (pydggal is a C engine, so agreement is numerical, not bit-identical).
206
+ - **Golden-table conformance** (`tests/test_ivea7h_conformance.py` -> **553 passed, 13 xfailed**) against `tests/golden/ivea7h/` (regenerate with `uv run python tests/golden/generate.py IVEA7H_Z7`).
207
+ Override the path with `export DGGS_GOLDEN_TABLES_IVEA7H=...`. The 13 xfails are exact-singular
208
+ boundary tie-breaks at the pole and the vertex-0 meridian (lon 11.2°).
209
+
210
+ **`RTEA7H`** is verified the same two ways (the tables ship in-repo; the live-oracle half skips cleanly if `dggal` is absent):
211
+
212
+ - **Live differential fuzz** (`tests/test_rtea7h_fuzz.py`) - forward / centroid / vertices /
213
+ neighbours vs pydggal. Discrete outputs match exactly; float geometry agrees to ~1e-9°.
214
+ - **Golden-table conformance** (`tests/test_rtea7h_conformance.py` -> **552 passed, 14 xfailed**) -
215
+ against `tests/golden/rtea7h/` (regenerate with `uv run python tests/golden/generate.py RTEA7H_Z7`).
216
+ Override the path with `export DGGS_GOLDEN_TABLES_RTEA7H=...`. 13 of the 14 xfails are the same
217
+ exact-singularity class as `IVEA7H` (pole + vertex-0 meridian, lon 11.2°); the 14th is an
218
+ ordinary point (London, res 3) whose RTEA-projected cell happens to straddle a 5x6-layout
219
+ rhombus interruption seam, confirmed adjacent to pydggal's cell (both engines agree exactly on
220
+ both cells' vertices/centroids) and consistent with the low, adjacent-only mismatch rate seen
221
+ under broader random fuzzing. See `tests/test_rtea7h_conformance.py` for the full derivation.
222
+
223
+ **`ISEA3H`** (point-keyed geometry) is verified two ways (the tables ship in-repo; the live-oracle half skips cleanly if `dggal` is absent):
224
+
225
+ - **Live differential fuzz** (`tests/test_isea3h_fuzz.py`) - quantize / centroid / vertices /
226
+ neighbours vs pydggal. Discrete outputs match exactly; float geometry agrees to ~1e-9°.
227
+ - **Golden-table conformance** (`tests/test_isea3h_conformance.py` -> **770 passed**) - against
228
+ `tests/golden/isea3h/` (regenerate with `uv run python tests/golden/generate.py ISEA3H`). Override
229
+ the path with `export DGGS_GOLDEN_TABLES_ISEA3H=...`. The verification lever is DGGAL-exact
230
+ `uint64` int packing: our Zone value equals pydggal's `DGGRSZone` int. Includes value-keyed
231
+ text-id/hierarchy/sub-zones tables (154 cases each) alongside the point-keyed geometry tables.
232
+
233
+ **`IVEA3H`** (point-keyed geometry) is verified the same two ways (both skip cleanly if `dggal` is
234
+ absent), using the same DGGAL-exact `uint64` int-packing lever as `ISEA3H`:
235
+
236
+ - **Live differential fuzz** (`tests/test_ivea3h_fuzz.py`) - quantize / centroid / vertices /
237
+ neighbours vs pydggal. Discrete outputs match exactly; float geometry agrees to ~1e-9°.
238
+ - **Golden-table conformance** (`tests/test_ivea3h_conformance.py` -> **758 passed, 12 xfailed**) -
239
+ against `tests/golden/ivea3h/` (regenerate with `uv run python tests/golden/generate.py IVEA3H`).
240
+ Override the path with `export DGGS_GOLDEN_TABLES_IVEA3H=...`. The 12 xfails are exact-boundary
241
+ tie-breaks at the vertex-0 meridian (lon 11.2°, even resolutions only) - the same singularity
242
+ class `IVEA7H` documents, confirmed benign (adjacent cell; both engines agree exactly on that
243
+ cell's own geometry). `ISEA3H` has zero xfails here since ISEA lacks IVEA's extra
244
+ pole-longitude sensitivity. Like text-id/hierarchy, the sub-zones table (A3) is value-keyed, so
245
+ it is immune to this boundary tie-break too.
246
+
247
+ **`RTEA3H`** (point-keyed geometry) is verified the same two ways (both skip cleanly if `dggal` is
248
+ absent), using the same DGGAL-exact `uint64` int-packing lever as `ISEA3H`/`IVEA3H`:
249
+
250
+ - **Live differential fuzz** (`tests/test_rtea3h_fuzz.py`) - quantize / centroid / vertices /
251
+ neighbours vs pydggal. Quantize/centroid/vertices match exactly (0 tolerance); the neighbour (k-ring) check is deliberately tolerant, see the note below.
252
+ - **Golden-table conformance** (`tests/test_rtea3h_conformance.py` -> **748 passed, 22 xfailed**)
253
+ against `tests/golden/rtea3h/` (regenerate with `uv run python tests/golden/generate.py RTEA3H`).
254
+ Override the path with `export DGGS_GOLDEN_TABLES_RTEA3H=...`. The 22 xfails are exact-boundary
255
+ tie-breaks at the vertex-0 meridian (lon 11.2°) at **every** resolution 0-10 (broader than
256
+ `IVEA3H`'s even-only set, because RTEA's own vertex-assignment permutation warps the cells
257
+ straddling this exact meridian differently), confirmed benign the same way (adjacent cell; both
258
+ engines agree exactly on that cell's own geometry). The sub-zones table (A3), like text-id/hierarchy, is value-keyed and unaffected by this tie-break.
259
+
260
+ **A note on neighbours (`Grid.neighbors`):** by default the k-ring is a grid-agnostic *geometric* edge-crossing construction (reflect the centroid through each edge midpoint, re-quantize the
261
+ reflected point). This is **exact for every aperture-7 grid** (IGEO7/IVEA7H/RTEA7H) - verified 0
262
+ mismatch vs pydggal's exact topological neighbours. It is **approximate for the aperture-3 grids** (~0.01% of cells, at root-rhombus boundaries / interruption seams / pentagons / polar rows, where
263
+ the edge-reflection overshoots), so sub-project A slice **A0** gave the `hex_a3` topology an **exact `neighbors` override**m a faithful port of DGGAL's `I3HZone::getNeighbor`/`getNeighbors`.
264
+ `Grid.neighbors` prefers a topology's override when present, else falls back to the geometric
265
+ k-ring. **So all six grids now have exact neighbours vs pydggal** (int-set equality, any seed);
266
+ `test_{isea3h,ivea3h,rtea3h}_fuzz.py` assert strict equality. The geometric k-ring remains the
267
+ grid-agnostic fallback for any future topology that doesn't supply its own.
268
+
269
+ **A note on the hierarchy (`Zone.parents`/`.children`/`.centroid_parent`):** each grid returns its
270
+ *native* hierarchy. The **I3H** grids have only a geometric one - non-congruent, `parents` = 1 or 3,
271
+ `children` = 6 (pentagon) or 7 (hexagon), ported exactly from DGGAL's `getZoneParents`/
272
+ `getZoneChildren` (`hex_a3` topology override, verified vs pydggal, slice A2). The **Z7** grids
273
+ return their congruent *digit* hierarchy (1 parent / 7 children - append/drop a Z7 digit), matching
274
+ `igeo7-py`. Note this deliberately differs from DGGAL's *geometric* hierarchy for `ISEA7H_Z7`, which
275
+ is itself non-congruent (2 parents / 13 children, incl. cells shared from neighbouring base rhombi);
276
+ reproducing that geometric view for the Z7 grids would be a separate slice. `Grid.parents`/
277
+ `children`/`centroid_parent`/`is_centroid_child` prefer a topology override (I3H) and otherwise use
278
+ the congruent digit-path default (Z7).
279
+
280
+ ## Sub-zones
281
+
282
+ Sub-project A slice **A3** adds OGC-style *descendants-at-depth*, the set of all cells `relative_depth`
283
+ levels below a zone (not just its immediate children), as an ordered, indexable sequence, for the
284
+ **ISEA3H**/**IVEA3H**/**RTEA3H** (I3H) grids. This is DGGAL's `getSubZones`/`countSubZones`/
285
+ `getFirstSubZone`/`getSubZoneIndex`/`getSubZoneAtIndex` family, ported faithfully from
286
+ `I3HSubZones.ec` and verified 0-mismatch against `pydggal` across all four I3H cell classes (interior hexagon, edge hexagon straddling a rhombus interruption, non-polar pentagon, polar pentagon) plus a reference-vector replay of the eC source's own worked examples.
287
+
288
+ **Grid coverage:** all five methods below work on the three I3H grids
289
+ (`ISEA3H`/`IVEA3H`/`RTEA3H`) and raise `NotImplementedError("this grid has no
290
+ sub-zone order")` on the three Z7 grids (`IGEO7`/`IVEA7H`/`RTEA7H`). Z7 sub-zones
291
+ are a **planned roadmap item**, not a permanent non-goal - see `TUTORIAL.md`.
292
+
293
+ **API** (mirrored on `Grid` and `Zone`):
294
+
295
+ - `Grid.count_sub_zones(value, relative_depth) -> int` / `Zone.count_sub_zones(relative_depth)` -
296
+ how many descendants a zone has `relative_depth` levels down (closed-form: 7/13/37/... for a
297
+ hexagon at depth 1/2/3, fewer for a pentagon).
298
+ - `Grid.sub_zones(value, relative_depth) -> tuple[int, ...]` / `Zone.sub_zones(relative_depth) ->
299
+ tuple[Zone, ...]` - the full ordered list of descendant values/`Zone`s.
300
+ - `Grid.first_sub_zone(value, relative_depth) -> int` / `Zone.first_sub_zone(relative_depth) ->
301
+ Zone` - the *centroid* descendant (index 0), computed directly without building the whole list.
302
+ - `Grid.sub_zone_index(value, sub_zone_value) -> int` / `Zone.sub_zone_index(sub_zone) -> int` -
303
+ the position of a known descendant within its parent's ordered sub-zone list (`-1` if it is not
304
+ a descendant at a valid depth). A zone is its own sub-zone at index **0** (`relative_depth == 0`),
305
+ matching `sub_zones(v, 0) == (v,)` and pydggal's own `getSubZoneIndex(v, v)`; an *ancestor* gives
306
+ `-1`. On `Zone`, a zone belonging to a **different grid** also gives `-1` - the I3H grids share an
307
+ identical packing, so the grid identity, not just the int, decides.
308
+ - `Grid.sub_zone_at_index(value, relative_depth, index) -> int` / `Zone.sub_zone_at_index(
309
+ relative_depth, index) -> Zone` - the descendant at a given position (`IndexError` if out of
310
+ range).
311
+
312
+ Both `sub_zone_index` and `sub_zone_at_index` are the **generic** `dggrs.ec`-style implementation
313
+ (build the ordered list, then index into or search it); DGGAL's internal `index >= 0`
314
+ *fast-forward* short-circuit - computing an arbitrary index's cell directly, without materializing
315
+ the whole list is deliberately **out of scope** for this port (see the A3 design spec). Callers needing that performance characteristic for very deep sub-zone sets should be aware both methods are currently `O(count_sub_zones)`.
316
+
317
+ **Bound on materialisation:** `count_sub_zones` is a cheap closed form, but `sub_zones` builds the
318
+ whole tuple, so `relative_depth` is additionally checked against `Grid.MAX_MATERIALISED_SUB_ZONES`
319
+ (4,000,000) and raises `InvalidZoneError` past it. Without that bound a caller-supplied depth was
320
+ unbounded - depth 33 on a resolution-0 I3H cell counts 4.6x10^15 sub-zones and would try to build
321
+ them all. Ask `count_sub_zones` first when the depth comes from a request.
322
+
323
+ **Why this matters - DGGS-as-storage:** sub-zones are the mechanism that turns a DGGS into a fixed size **tile store**. Pick a coarse zone as a "tile" (e.g. an ISEA3H cell at resolution 6) and a `relative_depth` (e.g. 4): `count_sub_zones` gives you the tile's fixed array length up front, `sub_zones`/`sub_zone_index`/`sub_zone_at_index` give you a stable, deterministic mapping between
324
+ "array slot" and "fine-resolution cell" - exactly the raster-band/array-index model a format like
325
+
326
+ **Golden-table conformance:** like text-id/hierarchy (A1/A2), sub-zones are checked against a frozen,
327
+ value-keyed golden table (`tests/golden/{isea3h,ivea3h,rtea3h}/subzones.json` -
328
+ `{name, res, value, subZonesByDepth: {"1": [...], "2": [...], "3": [...]}}`, regenerate with
329
+ `python generate.py {ISEA3H,IVEA3H,RTEA3H}`) via `test_subzones` in each grid's
330
+ `tests/test_*3h_conformance.py`, pinning correctness even without a live `pydggal` install. Being
331
+ value-keyed, it is immune to the vertex-0-meridian forward-quantization tie-break the point-keyed
332
+ geometry tables xfail on (see above) `ISEA3H`/`IVEA3H`/`RTEA3H` all have **zero** `test_subzones`
333
+ xfails.
334
+