From b7dac042b779052e20fd77263803513886f49d4a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 27 May 2026 12:46:44 +0200 Subject: [PATCH 01/20] implement ellipsoid support for the cf convention --- xdggs/conventions/cf.py | 43 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/xdggs/conventions/cf.py b/xdggs/conventions/cf.py index dfb74efd..c8d1e442 100644 --- a/xdggs/conventions/cf.py +++ b/xdggs/conventions/cf.py @@ -1,4 +1,4 @@ -from collections.abc import Hashable +from collections.abc import Hashable, Sequence from typing import Any, Literal import numpy as np @@ -22,6 +22,22 @@ def remove_grid_mapping(ds, name): return new +def remove_keys(mapping: dict[str, Any], exclude: Sequence[str]) -> dict[str, Any]: + return {k: v for k, v in mapping.items() if k not in exclude} + + +ellipsoid_attribute_translations = { + "semi_major_axis": "semimajor_axis", + "semi_minor_axis": "semiminor_axis", + "earth_radius": "radius", + "reference_ellipsoid_name": "name", +} + + +def extract_ellipsoid_parameters(mapping: dict[str, Any]) -> dict[str, Any]: + return {k: v for k, v in mapping.items() if k in ellipsoid_attribute_translations} + + @register_convention("cf") class Cf(Convention): def translate_keys( @@ -35,6 +51,17 @@ def translate_keys( return {translations.get(key, key): value for key, value in mapping.items()} + def translate_ellipsoid( + self, + mapping: dict[str, Any], + direction: Literal["forward", "inverse"] = "forward", + ) -> dict[str, Any] | None: + translations = ellipsoid_attribute_translations + if direction == "inverse": + translations = {v: k for k, v in translations.items()} + + return {translations.get(key, key): value for key, value in mapping.items()} + def decode( self, ds: xr.Dataset, @@ -70,7 +97,14 @@ def decode( ) name = coord_names[0] - grid_info = self.translate_keys(crs.attrs, direction="forward") + ellipsoid = extract_ellipsoid_parameters(crs.attrs) + grid_info = self.translate_keys( + remove_keys(crs.attrs, ellipsoid), direction="forward" + ) | { + "ellipsoid": self.translate_attribute_translations( + ellipsoid, direction="forward" + ) + } grid_name = grid_info["grid_name"] var = ds.variables[name].copy(deep=False) @@ -100,8 +134,11 @@ def encode(self, ds: xr.Dataset, *, encoding: dict[str, Any] | None = None): grid_name = infer_grid_name(ds.dggs.index) grid_info_dict = grid_info.to_dict() - metadata = self.translate_keys(grid_info_dict, direction="inverse") + ellipsoid = grid_info_dict.pop("ellipsoid", {}) + metadata = self.translate_keys( + grid_info_dict, direction="inverse" + ) | self.translate_ellipsoid(ellipsoid, direction="inverse") crs = xr.Variable((), np.int8(0), metadata) additional_var_attrs = {"grid_mapping": grid_name} From 243f353ef74e79a6035323ed18d418a323bc5e03 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 27 May 2026 15:17:11 +0200 Subject: [PATCH 02/20] support parsing a dict with a semiminor axis definition --- xdggs/ellipsoid.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/xdggs/ellipsoid.py b/xdggs/ellipsoid.py index 6ed702f9..c58295f7 100644 --- a/xdggs/ellipsoid.py +++ b/xdggs/ellipsoid.py @@ -29,7 +29,15 @@ class Ellipsoid: @classmethod def from_dict(cls, mapping): - return cls(**mapping) + semimajor = mapping["semimajor_axis"] + if (semiminor := mapping.get("semiminor_axis")) is not None: + inverse_flattening = semimajor / (semimajor - semiminor) + else: + inverse_flattening = mapping["inverse_flattening"] + name = mapping.get("name") + return cls( + semimajor_axis=semimajor, inverse_flattening=inverse_flattening, name=name + ) def to_dict(self): mapping = asdict(self) From f8e16e81ffee290c01e1d3cfa9dce5e466913557 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 27 May 2026 15:17:59 +0200 Subject: [PATCH 03/20] drop the index when encoding --- xdggs/conventions/xdggs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdggs/conventions/xdggs.py b/xdggs/conventions/xdggs.py index 8a12d057..28662f3a 100644 --- a/xdggs/conventions/xdggs.py +++ b/xdggs/conventions/xdggs.py @@ -65,4 +65,4 @@ def encode(self, ds: xr.Dataset, *, encoding: None = None) -> xr.Dataset: # TODO: `assign_coords` + `assign_attrs` drops the index ds_ = ds.copy(deep=False) ds_[coord].attrs.update(metadata) - return ds_ + return ds_.drop_indexes(coord) From 92aa0b5ed5e10883c615f9e461c6e38a0898ff5a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 27 May 2026 15:18:15 +0200 Subject: [PATCH 04/20] include the inverse flattening in the table --- xdggs/conventions/cf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xdggs/conventions/cf.py b/xdggs/conventions/cf.py index c8d1e442..6f983c98 100644 --- a/xdggs/conventions/cf.py +++ b/xdggs/conventions/cf.py @@ -31,6 +31,7 @@ def remove_keys(mapping: dict[str, Any], exclude: Sequence[str]) -> dict[str, An "semi_minor_axis": "semiminor_axis", "earth_radius": "radius", "reference_ellipsoid_name": "name", + "inverse_flattening": "inverse_flattening", } From 39b1cac2d9e7e3aaf917f424eb82f20c86f7a215 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 27 May 2026 15:18:33 +0200 Subject: [PATCH 05/20] remove typo --- xdggs/conventions/cf.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/xdggs/conventions/cf.py b/xdggs/conventions/cf.py index 6f983c98..877acecf 100644 --- a/xdggs/conventions/cf.py +++ b/xdggs/conventions/cf.py @@ -101,11 +101,7 @@ def decode( ellipsoid = extract_ellipsoid_parameters(crs.attrs) grid_info = self.translate_keys( remove_keys(crs.attrs, ellipsoid), direction="forward" - ) | { - "ellipsoid": self.translate_attribute_translations( - ellipsoid, direction="forward" - ) - } + ) | {"ellipsoid": self.translate_ellipsoid(ellipsoid, direction="forward")} grid_name = grid_info["grid_name"] var = ds.variables[name].copy(deep=False) From a5ba360d19a5b274555a2dca73a632ab6f8f2273 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 2 Jun 2026 16:11:13 +0200 Subject: [PATCH 06/20] remove upper bounds --- pixi.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pixi.toml b/pixi.toml index 3c09f757..67b704c6 100644 --- a/pixi.toml +++ b/pixi.toml @@ -98,13 +98,13 @@ isort = "*" jupyterlab = "*" jupyter-resource-usage = "*" jupyterlab_code_formatter = "*" -python-build = ">=1.3.0,<2" -h3-py = ">=4.3.0,<5" -geopandas = ">=1.1.1,<2" -pyinstrument = ">=5.1.1,<6" -jupyterlab-myst = ">=2.4.2,<3" -h5netcdf = ">=1.7.3,<2" -zarr = ">=3.1.5,<4" +python-build = ">=1.3.0" +h3-py = ">=4.3.0" +geopandas = ">=1.1.1" +pyinstrument = ">=5.1.1" +jupyterlab-myst = ">=2.4.2" +h5netcdf = ">=1.7.3" +zarr = ">=3.1.5" [environments] nightly = { features = ["tests", "nightly"], no-default-feature = true } From 9a5afb46c2c61fc8695803ceed433867279d5608 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 2 Jun 2026 16:11:25 +0200 Subject: [PATCH 07/20] add `cf-xarray` to the dev dependencies --- pixi.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pixi.toml b/pixi.toml index 67b704c6..867a14f2 100644 --- a/pixi.toml +++ b/pixi.toml @@ -105,6 +105,7 @@ pyinstrument = ">=5.1.1" jupyterlab-myst = ">=2.4.2" h5netcdf = ">=1.7.3" zarr = ">=3.1.5" +cf-xarray = ">=0.11.1" [environments] nightly = { features = ["tests", "nightly"], no-default-feature = true } From 426d88e1a39d18e894f1617500a1b99ff327c180 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 2 Jun 2026 16:12:11 +0200 Subject: [PATCH 08/20] change `dev` to be the default env --- pixi.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixi.toml b/pixi.toml index 867a14f2..4d25ed05 100644 --- a/pixi.toml +++ b/pixi.toml @@ -113,4 +113,4 @@ ci-py311 = { features = ["tests", "ci-py311"] } ci-py312 = { features = ["tests", "ci-py312"] } ci-py313 = { features = ["tests", "ci-py313"] } docs = { features = ["docs"] } -dev = { features = ["tests", "dev"] } +default = { features = ["tests", "dev"] } From 0bbc4355fb4ff6506cbfc65c0b1fc90c2efb3701 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 2 Jun 2026 16:27:00 +0200 Subject: [PATCH 09/20] error for invalid convention --- xdggs/conventions/xdggs.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xdggs/conventions/xdggs.py b/xdggs/conventions/xdggs.py index 28662f3a..074e1daa 100644 --- a/xdggs/conventions/xdggs.py +++ b/xdggs/conventions/xdggs.py @@ -45,7 +45,14 @@ def decode( # TODO: avoid serializing / deserializing cycle grid_info = grid_info.to_dict() - grid_name = grid_info["grid_name"] + try: + grid_name = grid_info["grid_name"] + except KeyError: + raise DecoderError( + "xdggs convention: no grid name found (`grid_name`)." + " Try choosing a different convention or verify the dataset metadata." + ) from None + if grid_name not in GRID_REGISTRY: raise DecoderError(f"xdggs convention: unknown grid name: {grid_name}") index_cls = GRID_REGISTRY[grid_name] From fd2235af229a7e34513cd012119ce662bab56418 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 12:07:14 +0200 Subject: [PATCH 10/20] forgotten mutation checks --- xdggs/tests/test_conventions.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/xdggs/tests/test_conventions.py b/xdggs/tests/test_conventions.py index aff20b68..a6f4cfd3 100644 --- a/xdggs/tests/test_conventions.py +++ b/xdggs/tests/test_conventions.py @@ -36,6 +36,8 @@ def test_decode(self, grid_info, cell_ids, name, dim): expected = xr.Coordinates.from_xindex(index).to_dataset() obj = xr.Dataset(coords={name: var}) + orig = obj.copy(deep=True) + actual = convention.decode( obj, grid_info=None, @@ -50,6 +52,10 @@ def test_decode(self, grid_info, cell_ids, name, dim): actual = convention.decode( obj, grid_info=grid_info, name=name, index_options={} ) + + # should not modify the original dataset + xr.testing.assert_identical(obj, orig) + xr.testing.assert_identical(actual, expected) assert_indexes_equal(actual[name].xindexes, expected[name].xindexes) @@ -77,10 +83,14 @@ def test_encode(self, grid_info, cell_ids, name, dim): index = index_cls.from_variables({name: var}, options={}) obj = xr.Dataset(coords=xr.Coordinates({name: var}, indexes={name: index})) + orig = obj.copy(deep=True) # no-op encoded = convention.encode(obj) + # should not modify the original dataset + xr.testing.assert_identical(obj, orig) + xr.testing.assert_identical(encoded, obj) assert_indexes_equal(encoded.xindexes, obj.xindexes) @@ -125,12 +135,18 @@ def test_decode(self, grid_info, cell_ids, name, dim): crs_var = xr.Variable((), np.array(0, dtype="int8"), translated_grid_info) obj = xr.Dataset(coords={name: cell_id_var, "crs": crs_var}) + orig = obj.copy(deep=True) + actual = convention.decode( obj, grid_info=None, name=name, index_options={}, ) + + # should not modify the original dataset + xr.testing.assert_identical(obj, orig) + xr.testing.assert_identical(actual, expected) assert_indexes_equal(actual.xindexes, expected.xindexes) @@ -158,6 +174,7 @@ def test_encode(self, grid_info, cell_ids, name, dim): index = index_cls.from_variables({name: var}, options={}) obj = xr.Dataset(coords=xr.Coordinates({name: var}, indexes={name: index})) + orig = obj.copy(deep=True) translated_grid_info = self.translate(grid_info) crs = xr.Variable((), np.int8(0), translated_grid_info) @@ -168,6 +185,9 @@ def test_encode(self, grid_info, cell_ids, name, dim): encoded = convention.encode(obj) + # should not modify the original dataset + xr.testing.assert_identical(obj, orig) + xr.testing.assert_identical(encoded, expected) assert_indexes_equal(encoded.xindexes, expected.xindexes) @@ -258,6 +278,7 @@ def test_encode(self, grid_info, cell_ids, name, dim): index = index_cls.from_variables({name: var}, options={}) obj = xr.Dataset(coords=xr.Coordinates({name: var}, indexes={name: index})) + orig = obj.copy(deep=True) coord = obj.dggs.coord dggs_metadata_object = self.translate(grid_info) | { @@ -273,6 +294,9 @@ def test_encode(self, grid_info, cell_ids, name, dim): encoded = convention.encode(obj) + # should not modify the original dataset + xr.testing.assert_identical(obj, orig) + xr.testing.assert_identical(encoded, expected) assert_indexes_equal(encoded.xindexes, expected.xindexes) From a8c023367e4fc3c8486802137d17f6ad8157c626 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 13:39:20 +0200 Subject: [PATCH 11/20] expect the xdggs encode to remove the index and reinstate the attrs --- xdggs/tests/test_conventions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/xdggs/tests/test_conventions.py b/xdggs/tests/test_conventions.py index a6f4cfd3..aa6c0fc1 100644 --- a/xdggs/tests/test_conventions.py +++ b/xdggs/tests/test_conventions.py @@ -85,14 +85,17 @@ def test_encode(self, grid_info, cell_ids, name, dim): obj = xr.Dataset(coords=xr.Coordinates({name: var}, indexes={name: index})) orig = obj.copy(deep=True) - # no-op + expected = obj.drop_indexes(name).assign_coords( + {name: lambda ds: ds[name].assign_attrs(grid_info)} + ) + encoded = convention.encode(obj) # should not modify the original dataset xr.testing.assert_identical(obj, orig) - xr.testing.assert_identical(encoded, obj) - assert_indexes_equal(encoded.xindexes, obj.xindexes) + xr.testing.assert_identical(encoded, expected) + assert_indexes_equal(encoded.xindexes, expected.xindexes) class TestCfConvention: From 228cd3bdfffac59f90a5d95b1e019e66a8571172 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 13:42:51 +0200 Subject: [PATCH 12/20] expect to drop the attributes after decoding --- xdggs/tests/test_conventions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xdggs/tests/test_conventions.py b/xdggs/tests/test_conventions.py index aa6c0fc1..b6df7af4 100644 --- a/xdggs/tests/test_conventions.py +++ b/xdggs/tests/test_conventions.py @@ -33,7 +33,8 @@ def test_decode(self, grid_info, cell_ids, name, dim): var = xr.Variable(dim, cell_ids, grid_info) index = xdggs.index.DGGSIndex.from_variables({name: var}, options={}) - expected = xr.Coordinates.from_xindex(index).to_dataset() + expected = xr.Coordinates.from_xindex(index).to_dataset().copy(deep=True) + expected[name].attrs = {} obj = xr.Dataset(coords={name: var}) orig = obj.copy(deep=True) From 937272d88fcf89df8d57f5b7753279463d75246a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 13:45:38 +0200 Subject: [PATCH 13/20] clean up the non-mutation check --- xdggs/tests/test_conventions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xdggs/tests/test_conventions.py b/xdggs/tests/test_conventions.py index b6df7af4..aa907bf2 100644 --- a/xdggs/tests/test_conventions.py +++ b/xdggs/tests/test_conventions.py @@ -45,11 +45,15 @@ def test_decode(self, grid_info, cell_ids, name, dim): name=name, index_options={}, ) + # should not modify the original dataset + xr.testing.assert_identical(obj, orig) + print(obj, actual, expected) xr.testing.assert_identical(actual, expected) assert_indexes_equal(actual[name].xindexes, expected[name].xindexes) obj = xr.Dataset(coords={name: (dim, cell_ids)}) + orig = obj.copy(deep=True) actual = convention.decode( obj, grid_info=grid_info, name=name, index_options={} ) From 14c6f5ec0b4544e4ba79a36ebdb790241cadc467 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 13:46:37 +0200 Subject: [PATCH 14/20] only deep-copy the attributes --- xdggs/conventions/xdggs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xdggs/conventions/xdggs.py b/xdggs/conventions/xdggs.py index 074e1daa..21815667 100644 --- a/xdggs/conventions/xdggs.py +++ b/xdggs/conventions/xdggs.py @@ -1,3 +1,4 @@ +import copy from collections.abc import Hashable from typing import Any @@ -40,7 +41,7 @@ def decode( [dim] = var.dims if grid_info is None: - grid_info = var.attrs + grid_info = copy.deepcopy(var.attrs) elif isinstance(grid_info, DGGSInfo): # TODO: avoid serializing / deserializing cycle grid_info = grid_info.to_dict() @@ -57,7 +58,7 @@ def decode( raise DecoderError(f"xdggs convention: unknown grid name: {grid_name}") index_cls = GRID_REGISTRY[grid_name] - var_ = var.copy(deep=True) + var_ = var.copy(deep=False) var_.attrs = grid_info index = index_cls.from_variables({name: var_}, options=index_options) From 3f68b69f38761d2dcd97b10a311640d8a09a8b5b Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 13:53:46 +0200 Subject: [PATCH 15/20] only add the ellipsoid if it is set --- xdggs/conventions/cf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xdggs/conventions/cf.py b/xdggs/conventions/cf.py index 877acecf..ba2dfc04 100644 --- a/xdggs/conventions/cf.py +++ b/xdggs/conventions/cf.py @@ -101,7 +101,10 @@ def decode( ellipsoid = extract_ellipsoid_parameters(crs.attrs) grid_info = self.translate_keys( remove_keys(crs.attrs, ellipsoid), direction="forward" - ) | {"ellipsoid": self.translate_ellipsoid(ellipsoid, direction="forward")} + ) + if parsed_ellipsoid := self.translate_ellipsoid(ellipsoid, direction="forward"): + grid_info["ellipsoid"] = parsed_ellipsoid + grid_name = grid_info["grid_name"] var = ds.variables[name].copy(deep=False) From f027f9f04a23d4c0609625349f4b8974b2a5973e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 13:54:07 +0200 Subject: [PATCH 16/20] return empty definitions unchanged --- xdggs/healpix.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/xdggs/healpix.py b/xdggs/healpix.py index f8973357..6a5af19b 100644 --- a/xdggs/healpix.py +++ b/xdggs/healpix.py @@ -190,6 +190,8 @@ def translate_nside(nside): def translate_ellipsoid(value): if isinstance(value, (str, Sphere, Ellipsoid)): return value + elif value is None or not value: + return value return parse_ellipsoid(value) From e7dcb737ff2eddaa7d4a7c277a4f0af3b912ca2f Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 14:46:11 +0200 Subject: [PATCH 17/20] full reprs for the indexes --- xdggs/h3.py | 3 +++ xdggs/healpix.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/xdggs/h3.py b/xdggs/h3.py index 3a780654..f8175e4f 100644 --- a/xdggs/h3.py +++ b/xdggs/h3.py @@ -242,5 +242,8 @@ def grid_info(self) -> H3Info: def _replace(self, new_index: xr.Index): return type(self)(new_index, self._dim, self._name, self._grid) + def __repr__(self): + return f"" + def _repr_inline_(self, max_width: int): return f"H3Index(level={self._grid.level})" diff --git a/xdggs/healpix.py b/xdggs/healpix.py index 6a5af19b..5f1f37e7 100644 --- a/xdggs/healpix.py +++ b/xdggs/healpix.py @@ -737,5 +737,13 @@ def _replace(self, new_index: xr.Index): def grid_info(self) -> HealpixInfo: return self._grid + def __repr__(self): + return "\n".join( + [ + f"", + repr(self._grid), + ] + ) + def _repr_inline_(self, max_width: int): return f"HealpixIndex(level={self._grid.level}, indexing_scheme={self._grid.indexing_scheme}, kind={self._kind})" From 1623f2f2f60a9b9d7fcb3ab4013765452cd8a9f6 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 14:50:20 +0200 Subject: [PATCH 18/20] check that the cf convention propagates the ellipsoid --- xdggs/tests/test_conventions.py | 74 ++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/xdggs/tests/test_conventions.py b/xdggs/tests/test_conventions.py index aa907bf2..f518b2ce 100644 --- a/xdggs/tests/test_conventions.py +++ b/xdggs/tests/test_conventions.py @@ -104,10 +104,6 @@ def test_encode(self, grid_info, cell_ids, name, dim): class TestCfConvention: - def translate(self, mapping): - translations = {"grid_name": "grid_mapping_name", "level": "refinement_level"} - return {translations.get(name, name): value for name, value in mapping.items()} - def index_metadata(self, grid_info): grid_name = grid_info["grid_name"] @@ -117,19 +113,43 @@ def index_metadata(self, grid_info): ["name", "dim"], [("cell_ids", "cells"), ("zone_ids", "zones")] ) @pytest.mark.parametrize( - ["grid_info", "cell_ids"], + ["crs_attrs", "grid_info", "cell_ids"], ( - ( + pytest.param( + { + "grid_mapping_name": "healpix", + "refinement_level": 1, + "indexing_scheme": "nested", + }, {"grid_name": "healpix", "level": 1, "indexing_scheme": "nested"}, np.array([3, 6, 9], dtype="uint64"), + id="healpix", ), - ( + pytest.param( + {"grid_mapping_name": "h3", "refinement_level": 4}, {"grid_name": "h3", "level": 4}, np.array([0x832830FFFFFFFFF], dtype="uint64"), + id="h3", + ), + pytest.param( + { + "grid_mapping_name": "healpix", + "refinement_level": 1, + "indexing_scheme": "nested", + "earth_radius": 6371000.0, + }, + { + "grid_name": "healpix", + "level": 1, + "indexing_scheme": "nested", + "ellipsoid": {"radius": 6371000.0}, + }, + np.array([3, 6, 9], dtype="uint64"), + id="healpix-ellipsoid_params", ), ), ) - def test_decode(self, grid_info, cell_ids, name, dim): + def test_decode(self, crs_attrs, grid_info, cell_ids, name, dim): convention = Cf() var = xr.Variable(dim, cell_ids, grid_info) @@ -137,10 +157,9 @@ def test_decode(self, grid_info, cell_ids, name, dim): expected = xr.Coordinates.from_xindex(index).to_dataset() metadata = self.index_metadata(grid_info) - translated_grid_info = self.translate(grid_info) cell_id_var = xr.Variable(dim, cell_ids, metadata) - crs_var = xr.Variable((), np.array(0, dtype="int8"), translated_grid_info) + crs_var = xr.Variable((), np.array(0, dtype="int8"), crs_attrs) obj = xr.Dataset(coords={name: cell_id_var, "crs": crs_var}) orig = obj.copy(deep=True) @@ -162,19 +181,43 @@ def test_decode(self, grid_info, cell_ids, name, dim): ["name", "dim"], [("cell_ids", "cells"), ("zone_ids", "zones")] ) @pytest.mark.parametrize( - ["grid_info", "cell_ids"], + ["crs_attrs", "grid_info", "cell_ids"], ( - ( + pytest.param( + { + "grid_mapping_name": "healpix", + "refinement_level": 1, + "indexing_scheme": "nested", + }, {"grid_name": "healpix", "level": 1, "indexing_scheme": "nested"}, np.array([3, 6, 9], dtype="uint64"), + id="healpix", ), - ( + pytest.param( + {"grid_mapping_name": "h3", "refinement_level": 4}, {"grid_name": "h3", "level": 4}, np.array([0x832830FFFFFFFFF], dtype="uint64"), + id="h3", + ), + pytest.param( + { + "grid_mapping_name": "healpix", + "refinement_level": 1, + "indexing_scheme": "nested", + "earth_radius": 6371000.0, + }, + { + "grid_name": "healpix", + "level": 1, + "indexing_scheme": "nested", + "ellipsoid": {"radius": 6371000.0}, + }, + np.array([3, 6, 9], dtype="uint64"), + id="healpix-ellipsoid_params", ), ), ) - def test_encode(self, grid_info, cell_ids, name, dim): + def test_encode(self, crs_attrs, grid_info, cell_ids, name, dim): convention = Cf() index_cls = xdggs.index.GRID_REGISTRY[grid_info["grid_name"]] @@ -184,8 +227,7 @@ def test_encode(self, grid_info, cell_ids, name, dim): obj = xr.Dataset(coords=xr.Coordinates({name: var}, indexes={name: index})) orig = obj.copy(deep=True) - translated_grid_info = self.translate(grid_info) - crs = xr.Variable((), np.int8(0), translated_grid_info) + crs = xr.Variable((), np.int8(0), crs_attrs) index_var = xr.Variable(dim, cell_ids, self.index_metadata(grid_info)) expected = xr.Coordinates( {"crs": crs, name: index_var}, indexes={} From bc8323c23147ad4782ca873bf41704cfb1e12b51 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 15:24:15 +0200 Subject: [PATCH 19/20] create the expected value using `xr.Coordinates.from_xindex` --- xdggs/tests/test_conventions.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/xdggs/tests/test_conventions.py b/xdggs/tests/test_conventions.py index f518b2ce..c7b3eda4 100644 --- a/xdggs/tests/test_conventions.py +++ b/xdggs/tests/test_conventions.py @@ -221,10 +221,11 @@ def test_encode(self, crs_attrs, grid_info, cell_ids, name, dim): convention = Cf() index_cls = xdggs.index.GRID_REGISTRY[grid_info["grid_name"]] - var = xr.Variable(dim, cell_ids, grid_info) - index = index_cls.from_variables({name: var}, options={}) + index = index_cls.from_variables( + {name: xr.Variable(dim, cell_ids, grid_info)}, options={} + ) + obj = xr.Coordinates.from_xindex(index).to_dataset() - obj = xr.Dataset(coords=xr.Coordinates({name: var}, indexes={name: index})) orig = obj.copy(deep=True) crs = xr.Variable((), np.int8(0), crs_attrs) From ef10a816a0b937ab21e1c7934aee491aba5bfa7b Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 3 Jun 2026 15:30:54 +0200 Subject: [PATCH 20/20] changelog --- docs/changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 9274cfa7..64c58d92 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,10 @@ # Changelog +## 0.6.1 (_unreleased_) + +- Do not mutate the input dataset when decoding ({pull}`226`) +- Properly decode ellipsoids for the CF convention ({pull}`228`) + ## 0.6.0 (2026-02-05) - Support variable-sized cells with the `"zuniq"` indexing scheme in `healpix` ({pull}`207`)