From 26061d451ddd846cc5ecc9304f244c5389346af2 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 26 Sep 2024 11:24:51 +0200 Subject: [PATCH 01/10] rename `resolution` to `level` in the base grid info class --- xdggs/grid.py | 8 ++++---- xdggs/healpix.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xdggs/grid.py b/xdggs/grid.py index 8b66df59..b5e3d731 100644 --- a/xdggs/grid.py +++ b/xdggs/grid.py @@ -15,18 +15,18 @@ class DGGSInfo: Parameters ---------- - resolution : int - The resolution of the grid. + level : int + The level within the grid hierarchy tree. """ - resolution: int + level: int @classmethod def from_dict(cls: type[T], mapping: dict[str, Any]) -> T: return cls(**mapping) def to_dict(self: Self) -> dict[str, Any]: - return {"resolution": self.resolution} + return {"level": self.level} def cell_ids2geographic(self, cell_ids): raise NotImplementedError() diff --git a/xdggs/healpix.py b/xdggs/healpix.py index bb76e326..1ce19666 100644 --- a/xdggs/healpix.py +++ b/xdggs/healpix.py @@ -28,7 +28,7 @@ @dataclass(frozen=True) class HealpixInfo(DGGSInfo): - resolution: int + level: int indexing_scheme: Literal["nested", "ring", "unique"] = "nested" From 3f5e7e4f06d865994896b83e655c26918d013294 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 26 Sep 2024 12:15:07 +0200 Subject: [PATCH 02/10] rename for healpix --- xdggs/healpix.py | 26 +++---- xdggs/tests/test_healpix.py | 134 +++++++++++++++++------------------- 2 files changed, 78 insertions(+), 82 deletions(-) diff --git a/xdggs/healpix.py b/xdggs/healpix.py index 1ce19666..46f6d926 100644 --- a/xdggs/healpix.py +++ b/xdggs/healpix.py @@ -35,13 +35,13 @@ class HealpixInfo(DGGSInfo): rotation: list[float, float] = field(default_factory=lambda: [0.0, 0.0]) valid_parameters: ClassVar[dict[str, Any]] = { - "resolution": range(0, 29 + 1), + "level": range(0, 29 + 1), "indexing_scheme": ["nested", "ring", "unique"], } def __post_init__(self): - if self.resolution not in self.valid_parameters["resolution"]: - raise ValueError("resolution must be an integer in the range of [0, 29]") + if self.level not in self.valid_parameters["level"]: + raise ValueError("level must be an integer in the range of [0, 29]") if self.indexing_scheme not in self.valid_parameters["indexing_scheme"]: raise ValueError( @@ -55,7 +55,7 @@ def __post_init__(self): @property def nside(self: Self) -> int: - return 2**self.resolution + return 2**self.level @property def nest(self: Self) -> bool: @@ -70,17 +70,17 @@ def nest(self: Self) -> bool: def from_dict(cls: type[T], mapping: dict[str, Any]) -> T: def translate_nside(nside): log = np.log2(nside) - potential_resolution = int(log) - if potential_resolution != log: + potential_level = int(log) + if potential_level != log: raise ValueError("`nside` has to be an integer power of 2") - return potential_resolution + return potential_level translations = { - "nside": ("resolution", translate_nside), - "order": ("resolution", identity), - "level": ("resolution", identity), - "depth": ("resolution", identity), + "nside": ("level", translate_nside), + "order": ("level", identity), + "resolution": ("level", identity), + "depth": ("level", identity), "nest": ("indexing_scheme", lambda nest: "nested" if nest else "ring"), "rot_latlon": ( "rotation", @@ -122,7 +122,7 @@ def translate(name, value): def to_dict(self: Self) -> dict[str, Any]: return { "grid_name": "healpix", - "resolution": self.resolution, + "level": self.level, "indexing_scheme": self.indexing_scheme, "rotation": self.rotation, } @@ -170,4 +170,4 @@ def grid_info(self) -> HealpixInfo: return self._grid def _repr_inline_(self, max_width: int): - return f"HealpixIndex(nside={self._grid.resolution}, indexing_scheme={self._grid.indexing_scheme}, rotation={self._grid.rotation!r})" + return f"HealpixIndex(nside={self._grid.level}, indexing_scheme={self._grid.indexing_scheme}, rotation={self._grid.rotation!r})" diff --git a/xdggs/tests/test_healpix.py b/xdggs/tests/test_healpix.py index a1a0f00c..d4509025 100644 --- a/xdggs/tests/test_healpix.py +++ b/xdggs/tests/test_healpix.py @@ -20,8 +20,8 @@ # namespace class class strategies: - invalid_resolutions = st.integers(max_value=-1) | st.integers(min_value=30) - resolutions = st.integers(min_value=0, max_value=29) + invalid_levels = st.integers(max_value=-1) | st.integers(min_value=30) + levels = st.integers(min_value=0, max_value=29) indexing_schemes = st.sampled_from(["nested", "ring", "unique"]) invalid_indexing_schemes = st.text().filter( lambda x: x not in ["nested", "ring", "unique"] @@ -37,11 +37,11 @@ def rotations(): @classmethod def grid_mappings(cls): strategies = { - "resolution": cls.resolutions, - "nside": cls.resolutions.map(lambda n: 2**n), - "depth": cls.resolutions, - "level": cls.resolutions, - "order": cls.resolutions, + "resolution": cls.levels, + "nside": cls.levels.map(lambda n: 2**n), + "depth": cls.levels, + "level": cls.levels, + "order": cls.levels, "indexing_scheme": cls.indexing_schemes, "nest": st.booleans(), "rotation": cls.rotations(), @@ -49,7 +49,7 @@ def grid_mappings(cls): } names = { - "resolution": st.sampled_from( + "level": st.sampled_from( ["resolution", "nside", "depth", "level", "order"] ), "indexing_scheme": st.sampled_from(["indexing_scheme", "nest"]), @@ -77,13 +77,13 @@ def cell_ids(max_value=None, dtypes=None): options = st.just({}) def grids( - resolutions=resolutions, + levels=levels, indexing_schemes=indexing_schemes, rotations=rotations(), ): return st.builds( healpix.HealpixInfo, - resolution=resolutions, + level=levels, indexing_scheme=indexing_schemes, rotation=rotations, ) @@ -91,20 +91,20 @@ def grids( @classmethod def grid_and_cell_ids( cls, - resolutions=resolutions, + levels=levels, indexing_schemes=indexing_schemes, rotations=rotations(), dtypes=None, ): - cell_resolutions = st.shared(resolutions, key="common-resolutions") - grid_resolutions = st.shared(resolutions, key="common-resolutions") - cell_ids_ = cell_resolutions.flatmap( - lambda resolution: cls.cell_ids( - max_value=12 * 2 ** (resolution * 2) - 1, dtypes=dtypes + cell_levels = st.shared(levels, key="common-levels") + grid_levels = st.shared(levels, key="common-levels") + cell_ids_ = cell_levels.flatmap( + lambda level: cls.cell_ids( + max_value=12 * 2 ** (level * 2) - 1, dtypes=dtypes ) ) grids_ = cls.grids( - resolutions=grid_resolutions, + levels=grid_levels, indexing_schemes=indexing_schemes, rotations=rotations, ) @@ -120,7 +120,7 @@ def grid_and_cell_ids( np.array([3]), { "grid_name": "healpix", - "resolution": 0, + "level": 0, "indexing_scheme": "nested", "rotation": (0, 0), }, @@ -130,7 +130,7 @@ def grid_and_cell_ids( np.array([3]), { "grid_name": "healpix", - "resolution": 0, + "level": 0, "indexing_scheme": "ring", "rotation": (0, 0), }, @@ -140,7 +140,7 @@ def grid_and_cell_ids( np.array([5, 11, 21]), { "grid_name": "healpix", - "resolution": 1, + "level": 1, "indexing_scheme": "nested", "rotation": (0, 0), }, @@ -150,7 +150,7 @@ def grid_and_cell_ids( np.array([54, 70, 82, 91]), { "grid_name": "healpix", - "resolution": 3, + "level": 3, "indexing_scheme": "nested", "rotation": (0, 0), }, @@ -160,40 +160,40 @@ def grid_and_cell_ids( class TestHealpixInfo: - @given(strategies.invalid_resolutions) - def test_init_invalid_resolutions(self, resolution): + @given(strategies.invalid_levels) + def test_init_invalid_levels(self, level): with pytest.raises( - ValueError, match="resolution must be an integer in the range of" + ValueError, match="level must be an integer in the range of" ): - healpix.HealpixInfo(resolution=resolution) + healpix.HealpixInfo(level=level) @given(strategies.invalid_indexing_schemes) def test_init_invalid_indexing_scheme(self, indexing_scheme): with pytest.raises(ValueError, match="indexing scheme must be one of"): healpix.HealpixInfo( - resolution=0, + level=0, indexing_scheme=indexing_scheme, ) - @given(strategies.resolutions, strategies.indexing_schemes, strategies.rotations()) - def test_init(self, resolution, indexing_scheme, rotation): + @given(strategies.levels, strategies.indexing_schemes, strategies.rotations()) + def test_init(self, level, indexing_scheme, rotation): grid = healpix.HealpixInfo( - resolution=resolution, indexing_scheme=indexing_scheme, rotation=rotation + level=level, indexing_scheme=indexing_scheme, rotation=rotation ) - assert grid.resolution == resolution + assert grid.level == level assert grid.indexing_scheme == indexing_scheme assert grid.rotation == rotation - @given(strategies.resolutions) - def test_nside(self, resolution): - grid = healpix.HealpixInfo(resolution=resolution) + @given(strategies.levels) + def test_nside(self, level): + grid = healpix.HealpixInfo(level=level) - assert grid.nside == 2**resolution + assert grid.nside == 2**level @given(strategies.indexing_schemes) def test_nest(self, indexing_scheme): - grid = healpix.HealpixInfo(resolution=1, indexing_scheme=indexing_scheme) + grid = healpix.HealpixInfo(level=1, indexing_scheme=indexing_scheme) if indexing_scheme not in {"nested", "ring"}: with pytest.raises( ValueError, match="cannot convert indexing scheme .* to `nest`" @@ -209,24 +209,24 @@ def test_nest(self, indexing_scheme): def test_from_dict(self, mapping) -> None: healpix.HealpixInfo.from_dict(mapping) - @given(strategies.resolutions, strategies.indexing_schemes, strategies.rotations()) - def test_to_dict(self, resolution, indexing_scheme, rotation) -> None: + @given(strategies.levels, strategies.indexing_schemes, strategies.rotations()) + def test_to_dict(self, level, indexing_scheme, rotation) -> None: grid = healpix.HealpixInfo( - resolution=resolution, indexing_scheme=indexing_scheme, rotation=rotation + level=level, indexing_scheme=indexing_scheme, rotation=rotation ) actual = grid.to_dict() - assert set(actual) == {"grid_name", "resolution", "indexing_scheme", "rotation"} + assert set(actual) == {"grid_name", "level", "indexing_scheme", "rotation"} assert actual["grid_name"] == "healpix" - assert actual["resolution"] == resolution + assert actual["level"] == level assert actual["indexing_scheme"] == indexing_scheme assert actual["rotation"] == rotation - @given(strategies.resolutions, strategies.indexing_schemes, strategies.rotations()) - def test_roundtrip(self, resolution, indexing_scheme, rotation): + @given(strategies.levels, strategies.indexing_schemes, strategies.rotations()) + def test_roundtrip(self, level, indexing_scheme, rotation): mapping = { "grid_name": "healpix", - "resolution": resolution, + "level": level, "indexing_scheme": indexing_scheme, "rotation": rotation, } @@ -250,7 +250,7 @@ def test_cell_center_roundtrip(self, cell_ids, grid) -> None: np.testing.assert_equal(roundtripped, cell_ids) @pytest.mark.parametrize( - ["cell_ids", "resolution", "indexing_scheme", "expected"], + ["cell_ids", "level", "indexing_scheme", "expected"], ( pytest.param( np.array([3]), @@ -270,11 +270,9 @@ def test_cell_center_roundtrip(self, cell_ids, grid) -> None: ), ) def test_cell_ids2geographic( - self, cell_ids, resolution, indexing_scheme, expected + self, cell_ids, level, indexing_scheme, expected ) -> None: - grid = healpix.HealpixInfo( - resolution=resolution, indexing_scheme=indexing_scheme - ) + grid = healpix.HealpixInfo(level=level, indexing_scheme=indexing_scheme) actual_lon, actual_lat = grid.cell_ids2geographic(cell_ids) @@ -282,7 +280,7 @@ def test_cell_ids2geographic( np.testing.assert_allclose(actual_lat, expected[1]) @pytest.mark.parametrize( - ["cell_centers", "resolution", "indexing_scheme", "expected"], + ["cell_centers", "level", "indexing_scheme", "expected"], ( pytest.param( np.array([[315.0, 66.44353569089877]]), @@ -301,11 +299,9 @@ def test_cell_ids2geographic( ), ) def test_geographic2cell_ids( - self, cell_centers, resolution, indexing_scheme, expected + self, cell_centers, level, indexing_scheme, expected ) -> None: - grid = healpix.HealpixInfo( - resolution=resolution, indexing_scheme=indexing_scheme - ) + grid = healpix.HealpixInfo(level=level, indexing_scheme=indexing_scheme) actual = grid.geographic2cell_ids( lon=cell_centers[:, 0], lat=cell_centers[:, 1] @@ -318,29 +314,29 @@ def test_geographic2cell_ids( ["mapping", "expected"], ( pytest.param( - {"resolution": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, - {"resolution": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, + {"level": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, + {"level": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, id="no_translation", ), pytest.param( { - "resolution": 10, + "level": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0), "grid_name": "healpix", }, - {"resolution": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, + {"level": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, id="no_translation-grid_name", ), pytest.param( {"nside": 1024, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, - {"resolution": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, + {"level": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0)}, id="nside-alone", ), pytest.param( { "nside": 1024, - "resolution": 10, + "level": 10, "indexing_scheme": "nested", "rotation": (0.0, 0.0), }, @@ -348,7 +344,7 @@ def test_geographic2cell_ids( "received multiple values for parameters", [ ValueError( - "Parameter resolution received multiple values: ['nside', 'resolution']" + "Parameter level received multiple values: ['level', 'nside']" ) ], ), @@ -356,7 +352,7 @@ def test_geographic2cell_ids( ), pytest.param( { - "resolution": 10, + "level": 10, "indexing_scheme": "nested", "nest": True, "rotation": (0.0, 0.0), @@ -374,7 +370,7 @@ def test_geographic2cell_ids( pytest.param( { "nside": 1024, - "resolution": 10, + "level": 10, "indexing_scheme": "nested", "nest": True, "rotation": (0.0, 0.0), @@ -386,7 +382,7 @@ def test_geographic2cell_ids( "Parameter indexing_scheme received multiple values: ['indexing_scheme', 'nest']" ), ValueError( - "Parameter resolution received multiple values: ['nside', 'resolution']" + "Parameter level received multiple values: ['level', 'nside']" ), ], ), @@ -426,7 +422,7 @@ def test_grid(self, grid): @pytest.mark.parametrize("variable", variables) @pytest.mark.parametrize("variable_name", variable_names) def test_from_variables(variable_name, variable, options) -> None: - expected_resolution = variable.attrs["resolution"] + expected_level = variable.attrs["level"] expected_scheme = variable.attrs["indexing_scheme"] expected_rot = variable.attrs["rotation"] @@ -434,7 +430,7 @@ def test_from_variables(variable_name, variable, options) -> None: index = healpix.HealpixIndex.from_variables(variables, options=options) - assert index._grid.resolution == expected_resolution + assert index._grid.level == expected_level assert index._grid.indexing_scheme == expected_scheme assert index._grid.rotation == expected_rot @@ -464,15 +460,15 @@ def test_replace(old_variable, new_variable) -> None: @pytest.mark.parametrize("max_width", [20, 50, 80, 120]) -@pytest.mark.parametrize("resolution", [0, 1, 3]) -def test_repr_inline(resolution, max_width) -> None: +@pytest.mark.parametrize("level", [0, 1, 3]) +def test_repr_inline(level, max_width) -> None: grid_info = healpix.HealpixInfo( - resolution=resolution, indexing_scheme="nested", rotation=(0, 0) + level=level, indexing_scheme="nested", rotation=(0, 0) ) index = healpix.HealpixIndex(cell_ids=[0], dim="cells", grid_info=grid_info) actual = index._repr_inline_(max_width) - assert f"nside={resolution}" in actual + assert f"nside={level}" in actual # ignore max_width for now # assert len(actual) <= max_width From 4678c784303e189464fd3770ee592be8eaff10ec Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 26 Sep 2024 12:22:05 +0200 Subject: [PATCH 03/10] rename for h3 --- xdggs/h3.py | 49 +++++++++++++++++++++++++----- xdggs/tests/test_h3.py | 68 +++++++++++++++++++----------------------- 2 files changed, 71 insertions(+), 46 deletions(-) diff --git a/xdggs/h3.py b/xdggs/h3.py index 17db4ed1..b0a05aac 100644 --- a/xdggs/h3.py +++ b/xdggs/h3.py @@ -7,6 +7,8 @@ except ImportError: # pragma: no cover from typing_extensions import Self +import operator + import numpy as np import xarray as xr from h3ronpy.arrow.vector import cells_to_coordinates, coordinates_to_cells @@ -14,26 +16,57 @@ from xdggs.grid import DGGSInfo from xdggs.index import DGGSIndex +from xdggs.itertools import groupby, identity from xdggs.utils import _extract_cell_id_variable, register_dggs @dataclass(frozen=True) class H3Info(DGGSInfo): - resolution: int + level: int - valid_parameters: ClassVar[dict[str, Any]] = {"resolution": range(16)} + valid_parameters: ClassVar[dict[str, Any]] = {"level": range(16)} def __post_init__(self): - if self.resolution not in self.valid_parameters["resolution"]: - raise ValueError("resolution must be an integer between 0 and 15") + if self.level not in self.valid_parameters["level"]: + raise ValueError("level must be an integer between 0 and 15") @classmethod def from_dict(cls: type[Self], mapping: dict[str, Any]) -> Self: - params = {k: v for k, v in mapping.items() if k != "grid_name"} + translations = { + "resolution": ("level", identity), + } + + def translate(name, value): + new_name, translator = translations.get(name, (name, identity)) + + return new_name, name, translator(value) + + translated = (translate(name, value) for name, value in mapping.items()) + grouped = { + name: [(old_name, value) for _, old_name, value in group] + for name, group in groupby(translated, key=operator.itemgetter(0)) + } + duplicated_parameters = { + name: group for name, group in grouped.items() if len(group) != 1 + } + if duplicated_parameters: + raise ExceptionGroup( + "received multiple values for parameters", + [ + ValueError( + f"Parameter {name} received multiple values: {sorted(n for n, _ in group)}" + ) + for name, group in duplicated_parameters.items() + ], + ) + + params = { + name: group[0][1] for name, group in grouped.items() if name != "grid_name" + } return cls(**params) def to_dict(self: Self) -> dict[str, Any]: - return {"grid_name": "h3", "resolution": self.resolution} + return {"grid_name": "h3", "level": self.level} def cell_ids2geographic( self, cell_ids: np.ndarray @@ -43,7 +76,7 @@ def cell_ids2geographic( return lon, lat def geographic2cell_ids(self, lon, lat): - return coordinates_to_cells(lat, lon, self.resolution, radians=False) + return coordinates_to_cells(lat, lon, self.level, radians=False) @register_dggs("h3") @@ -79,4 +112,4 @@ def _replace(self, new_pd_index: PandasIndex): return type(self)(new_pd_index, self._dim, self._grid) def _repr_inline_(self, max_width: int): - return f"H3Index(resolution={self._grid.resolution})" + return f"H3Index(level={self._grid.level})" diff --git a/xdggs/tests/test_h3.py b/xdggs/tests/test_h3.py index d82cab4f..6670c6a2 100644 --- a/xdggs/tests/test_h3.py +++ b/xdggs/tests/test_h3.py @@ -25,22 +25,14 @@ ), ] dims = ["cells", "zones"] -resolutions = [1, 5, 15] +levels = [1, 5, 15] variable_names = ["cell_ids", "zonal_ids", "zone_ids"] variables = [ - xr.Variable( - dims[0], cell_ids[0], {"grid_name": "h3", "resolution": resolutions[0]} - ), - xr.Variable( - dims[1], cell_ids[0], {"grid_name": "h3", "resolution": resolutions[0]} - ), - xr.Variable( - dims[0], cell_ids[1], {"grid_name": "h3", "resolution": resolutions[1]} - ), - xr.Variable( - dims[1], cell_ids[2], {"grid_name": "h3", "resolution": resolutions[2]} - ), + xr.Variable(dims[0], cell_ids[0], {"grid_name": "h3", "level": levels[0]}), + xr.Variable(dims[1], cell_ids[0], {"grid_name": "h3", "level": levels[0]}), + xr.Variable(dims[0], cell_ids[1], {"grid_name": "h3", "level": levels[1]}), + xr.Variable(dims[1], cell_ids[2], {"grid_name": "h3", "level": levels[2]}), ] variable_combinations = [ (old, new) for old, new in itertools.product(variables, repeat=2) @@ -50,29 +42,29 @@ class TestH3Info: @pytest.mark.parametrize( - ["resolution", "error"], + ["level", "error"], ( (0, None), (1, None), - (-1, ValueError("resolution must be an integer between")), + (-1, ValueError("level must be an integer between")), ), ) - def test_init(self, resolution, error): + def test_init(self, level, error): if error is not None: with pytest.raises(type(error), match=str(error)): - h3.H3Info(resolution=resolution) + h3.H3Info(level=level) return - actual = h3.H3Info(resolution=resolution) + actual = h3.H3Info(level=level) - assert actual.resolution == resolution + assert actual.level == level @pytest.mark.parametrize( ["mapping", "expected"], ( - ({"resolution": 0}, 0), + ({"level": 0}, 0), ({"resolution": 1}, 1), - ({"resolution": -1}, ValueError("resolution must be an integer between")), + ({"level": -1}, ValueError("level must be an integer between")), ), ) def test_from_dict(self, mapping, expected): @@ -82,10 +74,10 @@ def test_from_dict(self, mapping, expected): return actual = h3.H3Info.from_dict(mapping) - assert actual.resolution == expected + assert actual.level == expected def test_roundtrip(self): - mapping = {"grid_name": "h3", "resolution": 0} + mapping = {"grid_name": "h3", "level": 0} grid = h3.H3Info.from_dict(mapping) actual = grid.to_dict() @@ -96,7 +88,7 @@ def test_roundtrip(self): ["cell_ids", "cell_centers"], list(zip(cell_ids, cell_centers)) ) def test_cell_ids2geographic(self, cell_ids, cell_centers): - grid = h3.H3Info(resolution=3) + grid = h3.H3Info(level=3) actual = grid.cell_ids2geographic(cell_ids) expected = cell_centers.T @@ -108,7 +100,7 @@ def test_cell_ids2geographic(self, cell_ids, cell_centers): ["cell_centers", "cell_ids"], list(zip(cell_centers, cell_ids)) ) def test_geographic2cell_ids(self, cell_centers, cell_ids): - grid = h3.H3Info(resolution=3) + grid = h3.H3Info(level=3) actual = grid.geographic2cell_ids( lon=cell_centers[:, 0], lat=cell_centers[:, 1] @@ -118,11 +110,11 @@ def test_geographic2cell_ids(self, cell_centers, cell_ids): np.testing.assert_equal(actual, expected) -@pytest.mark.parametrize("resolution", resolutions) +@pytest.mark.parametrize("level", levels) @pytest.mark.parametrize("dim", dims) @pytest.mark.parametrize("cell_ids", cell_ids) -def test_init(cell_ids, dim, resolution): - grid = h3.H3Info(resolution) +def test_init(cell_ids, dim, level): + grid = h3.H3Info(level) index = h3.H3Index(cell_ids, dim, grid) assert index._grid == grid @@ -133,9 +125,9 @@ def test_init(cell_ids, dim, resolution): assert np.all(index._pd_index.index.values == cell_ids) -@pytest.mark.parametrize("resolution", resolutions) -def test_grid(resolution): - grid = h3.H3Info(resolution) +@pytest.mark.parametrize("level", levels) +def test_grid(level): + grid = h3.H3Info(level) index = h3.H3Index([0], "cell_ids", grid) @@ -146,12 +138,12 @@ def test_grid(resolution): @pytest.mark.parametrize("variable_name", variable_names) @pytest.mark.parametrize("options", [{}]) def test_from_variables(variable_name, variable, options): - expected_resolution = variable.attrs["resolution"] + expected_level = variable.attrs["level"] variables = {variable_name: variable} index = h3.H3Index.from_variables(variables, options=options) - assert index._grid.resolution == expected_resolution + assert index._grid.level == expected_level assert (index._dim,) == variable.dims # TODO: how do we check the index, if at all? @@ -161,7 +153,7 @@ def test_from_variables(variable_name, variable, options): @pytest.mark.parametrize(["old_variable", "new_variable"], variable_combinations) def test_replace(old_variable, new_variable): - grid = h3.H3Info(resolution=old_variable.attrs["resolution"]) + grid = h3.H3Info(level=old_variable.attrs["level"]) index = h3.H3Index( cell_ids=old_variable.data, dim=old_variable.dims[0], @@ -179,13 +171,13 @@ def test_replace(old_variable, new_variable): @pytest.mark.parametrize("max_width", [20, 50, 80, 120]) -@pytest.mark.parametrize("resolution", resolutions) -def test_repr_inline(resolution, max_width): - grid = h3.H3Info(resolution=resolution) +@pytest.mark.parametrize("level", levels) +def test_repr_inline(level, max_width): + grid = h3.H3Info(level=level) index = h3.H3Index(cell_ids=[0], dim="cells", grid_info=grid) actual = index._repr_inline_(max_width) - assert f"resolution={resolution}" in actual + assert f"level={level}" in actual # ignore max_width for now # assert len(actual) <= max_width From ce89ce09e07a2c3c84c4d24d497bce1f6da1c9e5 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 26 Sep 2024 12:23:44 +0200 Subject: [PATCH 04/10] rename the remaining instances --- xdggs/tests/test_accessor.py | 12 ++++++------ xdggs/tests/test_index.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/xdggs/tests/test_accessor.py b/xdggs/tests/test_accessor.py index cd6755a0..306e6d4e 100644 --- a/xdggs/tests/test_accessor.py +++ b/xdggs/tests/test_accessor.py @@ -16,7 +16,7 @@ [3], { "grid_name": "healpix", - "resolution": 1, + "level": 1, "indexing_scheme": "ring", }, ) @@ -36,7 +36,7 @@ "cell_ids": ( "cells", [0x832830FFFFFFFFF], - {"grid_name": "h3", "resolution": 3}, + {"grid_name": "h3", "level": 3}, ) } ), @@ -69,7 +69,7 @@ def test_cell_centers(obj, expected): [3], { "grid_name": "healpix", - "resolution": 1, + "level": 1, "indexing_scheme": "ring", }, ) @@ -86,7 +86,7 @@ def test_cell_centers(obj, expected): [3], { "grid_name": "healpix", - "resolution": 1, + "level": 1, "indexing_scheme": "ring", }, ), @@ -100,7 +100,7 @@ def test_cell_centers(obj, expected): "cell_ids": ( "cells", [0x832830FFFFFFFFF], - {"grid_name": "h3", "resolution": 3}, + {"grid_name": "h3", "level": 3}, ) } ), @@ -111,7 +111,7 @@ def test_cell_centers(obj, expected): "cell_ids": ( "cells", [0x832830FFFFFFFFF], - {"grid_name": "h3", "resolution": 3}, + {"grid_name": "h3", "level": 3}, ), } ), diff --git a/xdggs/tests/test_index.py b/xdggs/tests/test_index.py index fb0907fe..2fa45a77 100644 --- a/xdggs/tests/test_index.py +++ b/xdggs/tests/test_index.py @@ -7,7 +7,7 @@ @pytest.fixture def dggs_example(): return xr.Dataset( - coords={"cell_ids": ("cells", [0, 1], {"grid_name": "test", "resolution": 2})} + coords={"cell_ids": ("cells", [0, 1], {"grid_name": "test", "level": 2})} ) From b6c1f45537d27fb0b43c441ddc619d70eba5b28e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Fri, 27 Sep 2024 11:42:37 +0200 Subject: [PATCH 05/10] improve the description of the grid level Co-authored-by: Benoit Bovy --- xdggs/grid.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/xdggs/grid.py b/xdggs/grid.py index b5e3d731..e8cbea79 100644 --- a/xdggs/grid.py +++ b/xdggs/grid.py @@ -16,7 +16,11 @@ class DGGSInfo: Parameters ---------- level : int - The level within the grid hierarchy tree. + Grid hierarchical level. A higher value corresponds to a finer grid resolution + with smaller cell areas. The number of cells covering the whole sphere usually + grows exponentially with increasing level values, ranging from 5-100 cells at + level 0 to millions or billions of cells at level 10+ (the exact numbers depends + on the specific grid). """ level: int From 3f174fed2af1512ad6cda4e7c118368e6aa29d97 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Fri, 27 Sep 2024 11:50:30 +0200 Subject: [PATCH 06/10] deduplicate the parameter translation machinery --- xdggs/grid.py | 34 ++++++++++++++++++++++++++++++++++ xdggs/h3.py | 34 +++------------------------------- xdggs/healpix.py | 40 +++------------------------------------- 3 files changed, 40 insertions(+), 68 deletions(-) diff --git a/xdggs/grid.py b/xdggs/grid.py index e8cbea79..81cd3cc3 100644 --- a/xdggs/grid.py +++ b/xdggs/grid.py @@ -1,6 +1,9 @@ +import operator from dataclasses import dataclass from typing import Any, TypeVar +from xdggs.itertools import groupby, identity + try: from typing import Self except ImportError: # pragma: no cover @@ -37,3 +40,34 @@ def cell_ids2geographic(self, cell_ids): def geographic2cell_ids(self, lon, lat): raise NotImplementedError() + + +def translate_parameters(mapping, translations): + def translate(name, value): + new_name, translator = translations.get(name, (name, identity)) + + return new_name, name, translator(value) + + translated = (translate(name, value) for name, value in mapping.items()) + grouped = { + name: [(old_name, value) for _, old_name, value in group] + for name, group in groupby(translated, key=operator.itemgetter(0)) + } + duplicated_parameters = { + name: group for name, group in grouped.items() if len(group) != 1 + } + if duplicated_parameters: + raise ExceptionGroup( + "received multiple values for parameters", + [ + ValueError( + f"Parameter {name} received multiple values: {sorted(n for n, _ in group)}" + ) + for name, group in duplicated_parameters.items() + ], + ) + + params = { + name: group[0][1] for name, group in grouped.items() if name != "grid_name" + } + return params diff --git a/xdggs/h3.py b/xdggs/h3.py index b0a05aac..0271035e 100644 --- a/xdggs/h3.py +++ b/xdggs/h3.py @@ -7,16 +7,14 @@ except ImportError: # pragma: no cover from typing_extensions import Self -import operator - import numpy as np import xarray as xr from h3ronpy.arrow.vector import cells_to_coordinates, coordinates_to_cells from xarray.indexes import PandasIndex -from xdggs.grid import DGGSInfo +from xdggs.grid import DGGSInfo, translate_parameters from xdggs.index import DGGSIndex -from xdggs.itertools import groupby, identity +from xdggs.itertools import identity from xdggs.utils import _extract_cell_id_variable, register_dggs @@ -36,33 +34,7 @@ def from_dict(cls: type[Self], mapping: dict[str, Any]) -> Self: "resolution": ("level", identity), } - def translate(name, value): - new_name, translator = translations.get(name, (name, identity)) - - return new_name, name, translator(value) - - translated = (translate(name, value) for name, value in mapping.items()) - grouped = { - name: [(old_name, value) for _, old_name, value in group] - for name, group in groupby(translated, key=operator.itemgetter(0)) - } - duplicated_parameters = { - name: group for name, group in grouped.items() if len(group) != 1 - } - if duplicated_parameters: - raise ExceptionGroup( - "received multiple values for parameters", - [ - ValueError( - f"Parameter {name} received multiple values: {sorted(n for n, _ in group)}" - ) - for name, group in duplicated_parameters.items() - ], - ) - - params = { - name: group[0][1] for name, group in grouped.items() if name != "grid_name" - } + params = translate_parameters(mapping, translations) return cls(**params) def to_dict(self: Self) -> dict[str, Any]: diff --git a/xdggs/healpix.py b/xdggs/healpix.py index 46f6d926..c9f481a3 100644 --- a/xdggs/healpix.py +++ b/xdggs/healpix.py @@ -1,4 +1,3 @@ -import operator from collections.abc import Mapping from dataclasses import dataclass, field from typing import Any, ClassVar, Literal, TypeVar @@ -13,18 +12,13 @@ import xarray as xr from xarray.indexes import PandasIndex -from xdggs.grid import DGGSInfo +from xdggs.grid import DGGSInfo, translate_parameters from xdggs.index import DGGSIndex -from xdggs.itertools import groupby, identity +from xdggs.itertools import identity from xdggs.utils import _extract_cell_id_variable, register_dggs T = TypeVar("T") -try: - ExceptionGroup -except NameError: # pragma: no cover - from exceptiongroup import ExceptionGroup - @dataclass(frozen=True) class HealpixInfo(DGGSInfo): @@ -88,35 +82,7 @@ def translate_nside(nside): ), } - def translate(name, value): - new_name, translator = translations.get(name, (name, identity)) - - return new_name, name, translator(value) - - translated = (translate(name, value) for name, value in mapping.items()) - - grouped = { - name: [(old_name, value) for _, old_name, value in group] - for name, group in groupby(translated, key=operator.itemgetter(0)) - } - duplicated_parameters = { - name: group for name, group in grouped.items() if len(group) != 1 - } - if duplicated_parameters: - raise ExceptionGroup( - "received multiple values for parameters", - [ - ValueError( - f"Parameter {name} received multiple values: {sorted(n for n, _ in group)}" - ) - for name, group in duplicated_parameters.items() - ], - ) - - params = { - name: group[0][1] for name, group in grouped.items() if name != "grid_name" - } - + params = translate_parameters(mapping, translations) return cls(**params) def to_dict(self: Self) -> dict[str, Any]: From 5dedc195af802fdfb16dda8f43316aa51a272929 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 2 Oct 2024 12:19:44 +0200 Subject: [PATCH 07/10] add a fallback for `ExceptionGroup` on python 3.10 --- xdggs/grid.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/xdggs/grid.py b/xdggs/grid.py index 81cd3cc3..b15bf19b 100644 --- a/xdggs/grid.py +++ b/xdggs/grid.py @@ -9,6 +9,11 @@ except ImportError: # pragma: no cover from typing_extensions import Self +try: + ExceptionGroup +except NameError: # pragma: no cover + from exceptiongroup import ExceptionGroup + T = TypeVar("T") From 67e9fa02eb39abce273ccc7a67b13787168066f9 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 2 Oct 2024 12:26:30 +0200 Subject: [PATCH 08/10] rename in the test I missed before --- xdggs/tests/test_h3.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xdggs/tests/test_h3.py b/xdggs/tests/test_h3.py index 69524f2c..6069301c 100644 --- a/xdggs/tests/test_h3.py +++ b/xdggs/tests/test_h3.py @@ -112,7 +112,7 @@ def test_geographic2cell_ids(self, cell_centers, cell_ids): np.testing.assert_equal(actual, expected) @pytest.mark.parametrize( - ["resolution", "cell_ids", "expected_coords"], + ["level", "cell_ids", "expected_coords"], ( ( 1, @@ -194,10 +194,10 @@ def test_geographic2cell_ids(self, cell_centers, cell_ids): ), ), ) - def test_cell_boundaries(self, resolution, cell_ids, expected_coords): + def test_cell_boundaries(self, level, cell_ids, expected_coords): expected = shapely.polygons(expected_coords) - grid = h3.H3Info(resolution=resolution) + grid = h3.H3Info(level=level) actual = grid.cell_boundaries(cell_ids) From b8f3985076367bd4f655e72364c6ed505905a032 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 2 Oct 2024 12:29:23 +0200 Subject: [PATCH 09/10] rename in the final test --- xdggs/tests/test_healpix.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xdggs/tests/test_healpix.py b/xdggs/tests/test_healpix.py index 794f1d76..8a4af486 100644 --- a/xdggs/tests/test_healpix.py +++ b/xdggs/tests/test_healpix.py @@ -242,7 +242,7 @@ def test_roundtrip(self, level, indexing_scheme, rotation): ["params", "cell_ids", "expected_coords"], ( ( - {"resolution": 0, "indexing_scheme": "nested"}, + {"level": 0, "indexing_scheme": "nested"}, np.array([2]), np.array( [ @@ -254,7 +254,7 @@ def test_roundtrip(self, level, indexing_scheme, rotation): ), ), ( - {"resolution": 2, "indexing_scheme": "ring"}, + {"level": 2, "indexing_scheme": "ring"}, np.array([12, 54]), np.array( [ @@ -274,7 +274,7 @@ def test_roundtrip(self, level, indexing_scheme, rotation): ), ), ( - {"resolution": 3, "indexing_scheme": "nested"}, + {"level": 3, "indexing_scheme": "nested"}, np.array([293, 17]), np.array( [ @@ -294,7 +294,7 @@ def test_roundtrip(self, level, indexing_scheme, rotation): ), ), ( - {"resolution": 2, "indexing_scheme": "nested"}, + {"level": 2, "indexing_scheme": "nested"}, np.array([79]), np.array( [ From f0562b1568e8e99dc047a1085729de13bc8fce4c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Mon, 28 Oct 2024 13:21:25 +0100 Subject: [PATCH 10/10] create documentation for `DGGSInfo.level` --- docs/api-hidden.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-hidden.rst b/docs/api-hidden.rst index 0dc483b0..1ee23b27 100644 --- a/docs/api-hidden.rst +++ b/docs/api-hidden.rst @@ -5,7 +5,7 @@ .. autosummary:: :toctree: generated - DGGSInfo.resolution + DGGSInfo.level DGGSInfo.from_dict DGGSInfo.to_dict @@ -13,7 +13,7 @@ DGGSInfo.cell_ids2geographic DGGSInfo.geographic2cell_ids - HealpixInfo.resolution + HealpixInfo.level HealpixInfo.indexing_scheme HealpixInfo.valid_parameters HealpixInfo.nside @@ -25,7 +25,7 @@ HealpixInfo.cell_ids2geographic HealpixInfo.geographic2cell_ids - H3Info.resolution + H3Info.level H3Info.valid_parameters H3Info.from_dict