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 +334 -0
- py4dggs-0.2.0/README.md +323 -0
- py4dggs-0.2.0/pyproject.toml +31 -0
- py4dggs-0.2.0/pyproject.toml.orig +26 -0
- py4dggs-0.2.0/src/py4dggs/__init__.py +6 -0
- py4dggs-0.2.0/src/py4dggs/geojson.py +276 -0
- py4dggs-0.2.0/src/py4dggs/grid.py +253 -0
- py4dggs-0.2.0/src/py4dggs/indexings/__init__.py +2 -0
- py4dggs-0.2.0/src/py4dggs/indexings/i3h.py +217 -0
- py4dggs-0.2.0/src/py4dggs/indexings/z7.py +218 -0
- py4dggs-0.2.0/src/py4dggs/interfaces.py +112 -0
- py4dggs-0.2.0/src/py4dggs/projections/__init__.py +6 -0
- py4dggs-0.2.0/src/py4dggs/projections/icovertex.py +789 -0
- py4dggs-0.2.0/src/py4dggs/projections/isea.py +10 -0
- py4dggs-0.2.0/src/py4dggs/projections/ivea.py +9 -0
- py4dggs-0.2.0/src/py4dggs/projections/rtea.py +9 -0
- py4dggs-0.2.0/src/py4dggs/registry.py +24 -0
- py4dggs-0.2.0/src/py4dggs/topologies/__init__.py +6 -0
- py4dggs-0.2.0/src/py4dggs/topologies/hex_a3.py +2558 -0
- py4dggs-0.2.0/src/py4dggs/topologies/hex_a7.py +1315 -0
- py4dggs-0.2.0/src/py4dggs/types.py +52 -0
- py4dggs-0.2.0/src/py4dggs/zone.py +120 -0
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
|
+
|