Skip to content

Commit 6e315ff

Browse files
authored
hierarchical operations on cell ids: parents (#62)
* index and accessor implementations for `parents` * return the parents as a `DataArray` * implement the healpix version of the `parents` computation * same for h3 * switch to level * depend on `healpix-geo` * define `zoom_to` * docstrings and typing * implement `zoom_to` for healpix * partially implement `zoom_to` for H3 Fetching the children will need some thought, the children are returned to a flattened list while we need a 2D array (with masks) * check the implementation for healpix * install `healpix-geo` from PyPI * check `h3`'s implementation of `zoom_to` * change to `NotImplementedError` * actually fill in the expected cell ids * bump healpix-geo * install `healpix-geo` from conda-forge * install `healpix-geo` from github for nightly * bump lock file
1 parent ed90dfe commit 6e315ff

File tree

10 files changed

+3762
-3526
lines changed

10 files changed

+3762
-3526
lines changed

ci/environment.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
channels:
2+
- conda-forge
3+
dependencies:
4+
- xarray
5+
- h5netcdf
6+
- netcdf4
7+
- pooch
8+
- matplotlib-base
9+
- shapely
10+
- pytest
11+
- hypothesis
12+
- ruff
13+
- typing-extensions
14+
- geoarrow-pyarrow
15+
- lonboard
16+
- arro3-core
17+
- cdshealpix
18+
- h3ronpy
19+
- pip
20+
- pip:
21+
- healpix-geo

pixi.lock

Lines changed: 3612 additions & 3526 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ requires-python = ">=3.11"
2525
dependencies = [
2626
"xarray",
2727
"cdshealpix",
28+
"healpix-geo>=0.0.6",
2829
"h3ronpy",
2930
"lonboard>=0.9.3",
3031
"pyproj>=3.3",
@@ -138,6 +139,7 @@ pyproj = ">=3.3"
138139
matplotlib-base = ">=3.10.5"
139140
arro3-core = ">=0.4.0"
140141
pooch = ">=1.8.2"
142+
healpix-geo = ">=0.0.6"
141143

142144
[tool.pixi.feature.tests.dependencies]
143145
pytest = "*"
@@ -165,6 +167,7 @@ pyarrow = "*"
165167
h3ronpy = { git = "git+https://github.com/keewis/h3ronpy.git", subdirectory = "h3ronpy", rev = "version" }
166168
cdshealpix = { git = "git+https://github.com/cds-astro/cds-healpix-python.git" }
167169
astropy = { git = "git+https://github.com/astropy/astropy.git" }
170+
healpix-geo = { git = "git+https://github.com/eopf-dggs/healpix-geo.git" }
168171

169172
[tool.pixi.feature.nightly.dependencies]
170173
lonboard = ">=0.11.1"

xdggs/accessor.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,31 @@ def cell_boundaries(self):
174174
boundaries, coords={self._name: self.cell_ids}, dims=self.cell_ids.dims
175175
)
176176

177+
def zoom_to(self, level: int):
178+
"""Change the refinement level of the cell ids to `level`.
179+
180+
Parameters
181+
----------
182+
level : int
183+
The refinement level to change to. Can be smaller than the dataset's
184+
level to compute parents, or bigger to fetch the children. In the
185+
latter case, the array will have an additional `"children"`
186+
dimension.
187+
188+
Returns
189+
-------
190+
zoomed : xr.DataArray
191+
The children or parents of the current cells.
192+
"""
193+
zoomed = self.index.zoom_to(level=level)
194+
195+
if zoomed.ndim == 1:
196+
dims = self.cell_ids.dims
197+
else:
198+
dims = [*self.cell_ids.dims, "children"]
199+
200+
return xr.DataArray(zoomed, coords={self._name: self.cell_ids}, dims=dims)
201+
177202
def explore(self, *, cmap="viridis", center=None, alpha=None, coords=None):
178203
"""interactively explore the data using `lonboard`
179204

xdggs/grid.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def geographic2cell_ids(self, lon, lat):
3939
def cell_boundaries(self, cell_ids, backend="shapely"):
4040
raise NotImplementedError()
4141

42+
def zoom_to(self, cell_ids, level: int):
43+
raise NotImplementedError()
44+
4245

4346
def translate_parameters(mapping, translations):
4447
def translate(name, value):

xdggs/h3.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import xarray as xr
88

99
try:
10+
from h3ronpy import change_resolution
1011
from h3ronpy.vector import (
1112
cells_to_coordinates,
1213
cells_to_wkb_polygons,
1314
coordinates_to_cells,
1415
)
1516
except ImportError:
17+
from h3ronpy.arrow import change_resolution
1618
from h3ronpy.arrow.vector import (
1719
cells_to_coordinates,
1820
cells_to_wkb_polygons,
@@ -196,6 +198,14 @@ def cell_boundaries(self, cell_ids, backend="shapely"):
196198
raise ValueError(f"invalid backend: {backend!r}")
197199
return backend_func(wkb)
198200

201+
def zoom_to(self, cell_ids, level):
202+
if level > self.level:
203+
raise NotImplementedError(
204+
"extracting children is not supported for H3, yet."
205+
)
206+
207+
return np.asarray(change_resolution(cell_ids, level))
208+
199209

200210
@register_dggs("h3")
201211
class H3Index(DGGSIndex):

xdggs/healpix.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,17 @@ def cell_boundaries(self, cell_ids: Any, backend="shapely") -> np.ndarray:
303303

304304
return backend_func(vertices)
305305

306+
def zoom_to(self, cell_ids, level):
307+
if self.indexing_scheme == "ring":
308+
raise ValueError(
309+
"Scaling does not make sense for the 'ring' scheme."
310+
" Please convert to a nested scheme first."
311+
)
312+
313+
from healpix_geo.nested import zoom_to
314+
315+
return zoom_to(cell_ids, self.level, level)
316+
306317

307318
@register_dggs("healpix")
308319
class HealpixIndex(DGGSIndex):

xdggs/index.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ def cell_centers(self) -> tuple[np.ndarray, np.ndarray]:
9898
def cell_boundaries(self) -> np.ndarray:
9999
return self.grid_info.cell_boundaries(self._pd_index.index.values)
100100

101+
def zoom_to(self, level: int) -> np.ndarray:
102+
return self._grid.zoom_to(self._pd_index.index.values, level=level)
103+
101104
@property
102105
def grid_info(self) -> DGGSInfo:
103106
return self._grid

xdggs/tests/test_h3.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,38 @@ def test_cell_boundaries(self, level, cell_ids, backend, expected_coords):
209209

210210
shapely.testing.assert_geometries_equal(converter(actual), expected)
211211

212+
@pytest.mark.parametrize(
213+
["level", "cell_ids", "new_level", "expected"],
214+
(
215+
pytest.param(
216+
3,
217+
np.array([0x832833FFFFFFFFF, 0x832834FFFFFFFFF, 0x832835FFFFFFFFF]),
218+
2,
219+
np.array([0x822837FFFFFFFFF, 0x822837FFFFFFFFF, 0x822837FFFFFFFFF]),
220+
id="parents",
221+
),
222+
pytest.param(
223+
3,
224+
np.array([0x832833FFFFFFFFF, 0x832834FFFFFFFFF, 0x832835FFFFFFFFF]),
225+
1,
226+
np.array([0x81283FFFFFFFFFF, 0x81283FFFFFFFFFF, 0x81283FFFFFFFFFF]),
227+
id="grandparents",
228+
),
229+
),
230+
)
231+
def test_zoom_to(self, level, cell_ids, new_level, expected):
232+
grid = h3.H3Info(level=level)
233+
234+
actual = grid.zoom_to(cell_ids, level=new_level)
235+
np.testing.assert_equal(actual, expected)
236+
237+
def test_zoom_to_children_not_implemented(self):
238+
grid = h3.H3Info(level=3)
239+
cell_ids = np.arange(3)
240+
241+
with pytest.raises(NotImplementedError):
242+
grid.zoom_to(cell_ids, level=4)
243+
212244

213245
@pytest.mark.parametrize("level", levels)
214246
@pytest.mark.parametrize("dim", dims)

xdggs/tests/test_healpix.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,48 @@ def test_geographic2cell_ids(
366366

367367
np.testing.assert_equal(actual, expected)
368368

369+
@pytest.mark.parametrize(
370+
["level", "cell_ids", "new_level", "expected"],
371+
(
372+
pytest.param(
373+
1,
374+
np.array([0, 4, 8, 12, 16]),
375+
0,
376+
np.array([0, 1, 2, 3, 4]),
377+
id="level1-parents",
378+
),
379+
pytest.param(
380+
1,
381+
np.array([0, 1, 2, 3]),
382+
2,
383+
np.array(
384+
[[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11], [12, 13, 14, 15]]
385+
),
386+
id="level1-children",
387+
),
388+
pytest.param(
389+
1,
390+
np.array([0, 4]),
391+
3,
392+
np.stack([np.arange(16), 4 * 4**2 + np.arange(16)]),
393+
id="level1-grandchildren",
394+
),
395+
),
396+
)
397+
def test_zoom_to(self, level, cell_ids, new_level, expected):
398+
grid = healpix.HealpixInfo(level=level, indexing_scheme="nested")
399+
400+
actual = grid.zoom_to(cell_ids, level=new_level)
401+
402+
np.testing.assert_equal(actual, expected)
403+
404+
def test_zoom_to_ring(self):
405+
cell_ids = np.array([1, 2, 3])
406+
grid = healpix.HealpixInfo(level=1, indexing_scheme="ring")
407+
408+
with pytest.raises(ValueError, match="Scaling does not make sense.*'ring'.*"):
409+
grid.zoom_to(cell_ids, level=0)
410+
369411

370412
@pytest.mark.parametrize(
371413
["mapping", "expected"],

0 commit comments

Comments
 (0)