From 58195096ba1d4ecaec8c294928c09c42fe652a0f Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 16 Sep 2025 18:11:55 +0200 Subject: [PATCH 01/27] initial version of a convention converter --- xdggs/accessor.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index d0656c35..6db69eea 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -1,6 +1,7 @@ import numpy.typing as npt import xarray as xr +from xdggs import conventions from xdggs.grid import DGGSInfo from xdggs.index import DGGSIndex from xdggs.plotting import explore @@ -234,3 +235,31 @@ def explore(self, *, cmap="viridis", center=None, alpha=None, coords=None): alpha=alpha, coords=coords, ) + + def as_convention(self, *, convention: str): + """Convert the dataset to a specific convention + + Parameters + ---------- + convention : str + The name of the convention. Supported are: + - "easygems": ``grid_mapping`` coordinate and ``cell`` dimension and ``cell`` coordinate with a `pandas` index. + - "cf": ``grid_mapping`` coordinate with ``cell_index`` coordinate and ``cell`` dimension. + - "xdggs": ``cell_ids`` coordinate with grid metadata and a ``cells`` coordinate. + + Returns + ------- + obj : xr.DataArray or xr.Dataset + The object converted to the given dimension. + """ + converters = { + "easygems": conventions.easygems, + "cf": conventions.cf, + "xdggs": conventions.xdggs, + } + + converter = converters.get(convention) + if converter is None: + raise ValueError(f"unknown convention: {convention}") + + return converter(self._obj) From f3a51ab8f8ea8ef289457473c38d2f23cdedce80 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Tue, 16 Sep 2025 18:12:07 +0200 Subject: [PATCH 02/27] implement the `easygems` convention --- xdggs/conventions.py | 51 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 xdggs/conventions.py diff --git a/xdggs/conventions.py b/xdggs/conventions.py new file mode 100644 index 00000000..f920ec26 --- /dev/null +++ b/xdggs/conventions.py @@ -0,0 +1,51 @@ +import xarray as xr + + +def call_on_dataset(func, obj, name, *args, kwargs=None): + if kwargs is None: + kwargs = {} + + if isinstance(obj, xr.DataArray): + ds = obj._to_temp_dataset() + else: + ds = obj + + result = func(ds, *args, **kwargs) + + if isinstance(obj, xr.DataArray): + return xr.DataArray._from_temp_dataset(result, name=obj.name) + else: + return result + + +def easygems(obj): + orders = {"nested": "nest", "ring": "ring"} + + def _convert(ds): + grid_info = ds.dggs.grid_info + dim = ds.dggs.index._dim + coord = ds.dggs._name + + order = orders.get(grid_info.indexing_scheme) + if order is None: + raise ValueError(f"easygems: unsupported indexing scheme: {order}") + + crs = xr.Variable( + (), + 0, + { + "grid_mapping_name": "healpix", + "healpix_nside": grid_info.nside, + "healpix_order": order, + }, + ) + + return ( + ds.assign_coords(crs=crs) + .drop_indexes(coord) + .rename_dims({dim: "cell"}) + .rename_vars({coord: "cell"}) + .set_xindex("cell") + ) + + return call_on_dataset(_convert, obj) From d7636d3d67fa05e4f1b5ef3dfd8d9e5ead7152a5 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 17 Sep 2025 12:05:30 +0200 Subject: [PATCH 03/27] implement a generalized cf convention --- xdggs/conventions.py | 57 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/xdggs/conventions.py b/xdggs/conventions.py index f920ec26..b513bbd1 100644 --- a/xdggs/conventions.py +++ b/xdggs/conventions.py @@ -1,7 +1,18 @@ +import numpy as np import xarray as xr +from xdggs.utils import GRID_REGISTRY -def call_on_dataset(func, obj, name, *args, kwargs=None): + +def infer_grid_name(index): + for name, cls in GRID_REGISTRY.items(): + if cls is type(index): + return name + + raise ValueError("unknown index") + + +def call_on_dataset(func, obj, *args, kwargs=None): if kwargs is None: kwargs = {} @@ -30,15 +41,12 @@ def _convert(ds): if order is None: raise ValueError(f"easygems: unsupported indexing scheme: {order}") - crs = xr.Variable( - (), - 0, - { - "grid_mapping_name": "healpix", - "healpix_nside": grid_info.nside, - "healpix_order": order, - }, - ) + metadata = { + "grid_mapping_name": "healpix", + "healpix_nside": grid_info.nside, + "healpix_order": order, + } + crs = xr.Variable((), np.int8(0), metadata) return ( ds.assign_coords(crs=crs) @@ -49,3 +57,32 @@ def _convert(ds): ) return call_on_dataset(_convert, obj) + + +def cf(obj): + def _convert(ds): + grid_info = ds.dggs.grid_info + dim = ds.dggs.index._dim + coord = ds.dggs._name + + grid_name = infer_grid_name(ds.dggs.index) + metadata = grid_info.to_dict() | {"grid_mapping_name": grid_name} + metadata["refinement_level"] = metadata.pop("level") + + crs = xr.Variable((), np.int8(0), metadata) + + additional_var_attrs = {"coordinates": coord, "grid_mapping": "crs"} + coord_attrs = {"standard_name": "healpix_index", "units": "1"} + + new = ds.copy(deep=False) + for key, var in new.variables.items(): + if key == coord or dim not in var.dims: + continue + + var.attrs |= additional_var_attrs + + new[coord].attrs |= coord_attrs + + return new.assign_coords({"crs": crs}) + + return call_on_dataset(_convert, obj) From 433e75986deccb503eeedfe6b1ba44ac5148275d Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 17 Sep 2025 12:08:09 +0200 Subject: [PATCH 04/27] implement a converter to the `xdggs` convention --- xdggs/conventions.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/xdggs/conventions.py b/xdggs/conventions.py index b513bbd1..01e59481 100644 --- a/xdggs/conventions.py +++ b/xdggs/conventions.py @@ -86,3 +86,15 @@ def _convert(ds): return new.assign_coords({"crs": crs}) return call_on_dataset(_convert, obj) + + +def xdggs(obj): + def _convert(ds): + coord = ds.dggs._name + + grid_name = infer_grid_name(ds.dggs.index) + metadata = {"grid_name": grid_name} | ds.dggs.grid_info.to_dict() + + return ds.assign_coords({coord: lambda ds: ds[coord].assign_attrs(metadata)}) + + return call_on_dataset(_convert, obj) From 7234042491658aefab2287e4f8582f069229328a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 17 Sep 2025 12:08:27 +0200 Subject: [PATCH 05/27] don't require `convention` as a public kwarg --- xdggs/accessor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index 6db69eea..a6170c7e 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -236,7 +236,7 @@ def explore(self, *, cmap="viridis", center=None, alpha=None, coords=None): coords=coords, ) - def as_convention(self, *, convention: str): + def as_convention(self, convention: str): """Convert the dataset to a specific convention Parameters From 55b05668251b35cb7db163cb995d99f86316994e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 14:47:49 +0200 Subject: [PATCH 06/27] refactor the conventions module --- xdggs/conventions/__init__.py | 23 ++++++++++++ xdggs/conventions/registry.py | 36 ++++++++++++++++++ .../writers.py} | 37 ++++++------------- xdggs/utils.py | 19 ++++++++++ 4 files changed, 90 insertions(+), 25 deletions(-) create mode 100644 xdggs/conventions/__init__.py create mode 100644 xdggs/conventions/registry.py rename xdggs/{conventions.py => conventions/writers.py} (84%) diff --git a/xdggs/conventions/__init__.py b/xdggs/conventions/__init__.py new file mode 100644 index 00000000..f624f47e --- /dev/null +++ b/xdggs/conventions/__init__.py @@ -0,0 +1,23 @@ +from xdggs.conventions.registry import decoders as _decoders +from xdggs.conventions.registry import encoders as _encoders # noqa: F401 +from xdggs.conventions.registry import ( + register_decoder, + register_encoder, +) + + +class DecoderError(Exception): + pass + + +def detect_convention_decoder(obj, grid_info, name): + for name, decoder in _decoders.items(): + try: + return decoder(obj, grid_info=grid_info, name=name) + except DecoderError: + pass + + raise ValueError("cannot detect a matching convention") + + +__all__ = ["register_decoder", "register_encoder"] diff --git a/xdggs/conventions/registry.py b/xdggs/conventions/registry.py new file mode 100644 index 00000000..c28913f6 --- /dev/null +++ b/xdggs/conventions/registry.py @@ -0,0 +1,36 @@ +import warnings + +decoders = {} +encoders = {} + + +class DecoderWarning(UserWarning): + pass + + +def register_decoder(name): + def register(func): + if name in decoders: + warnings.warn( + DecoderWarning(f"Overwriting existing convention decoder {name!r}.") + ) + + decoders[name] = func + + return func + + return register + + +def register_encoder(name): + def register(func): + if name in decoders: + warnings.warn( + DecoderWarning(f"Overwriting existing convention encoder {name!r}.") + ) + + encoders[name] = func + + return func + + return register diff --git a/xdggs/conventions.py b/xdggs/conventions/writers.py similarity index 84% rename from xdggs/conventions.py rename to xdggs/conventions/writers.py index 01e59481..507b728f 100644 --- a/xdggs/conventions.py +++ b/xdggs/conventions/writers.py @@ -1,7 +1,8 @@ import numpy as np import xarray as xr -from xdggs.utils import GRID_REGISTRY +from xdggs.registry import register_encoder +from xdggs.utils import GRID_REGISTRY, call_on_dataset def infer_grid_name(index): @@ -12,23 +13,20 @@ def infer_grid_name(index): raise ValueError("unknown index") -def call_on_dataset(func, obj, *args, kwargs=None): - if kwargs is None: - kwargs = {} +@register_encoder("xdggs") +def xdggs(obj): + def _convert(ds): + coord = ds.dggs._name - if isinstance(obj, xr.DataArray): - ds = obj._to_temp_dataset() - else: - ds = obj + grid_name = infer_grid_name(ds.dggs.index) + metadata = {"grid_name": grid_name} | ds.dggs.grid_info.to_dict() - result = func(ds, *args, **kwargs) + return ds.assign_coords({coord: lambda ds: ds[coord].assign_attrs(metadata)}) - if isinstance(obj, xr.DataArray): - return xr.DataArray._from_temp_dataset(result, name=obj.name) - else: - return result + return call_on_dataset(_convert, obj) +@register_encoder("easygems") def easygems(obj): orders = {"nested": "nest", "ring": "ring"} @@ -59,6 +57,7 @@ def _convert(ds): return call_on_dataset(_convert, obj) +@register_encoder("cf") def cf(obj): def _convert(ds): grid_info = ds.dggs.grid_info @@ -86,15 +85,3 @@ def _convert(ds): return new.assign_coords({"crs": crs}) return call_on_dataset(_convert, obj) - - -def xdggs(obj): - def _convert(ds): - coord = ds.dggs._name - - grid_name = infer_grid_name(ds.dggs.index) - metadata = {"grid_name": grid_name} | ds.dggs.grid_info.to_dict() - - return ds.assign_coords({coord: lambda ds: ds[coord].assign_attrs(metadata)}) - - return call_on_dataset(_convert, obj) diff --git a/xdggs/utils.py b/xdggs/utils.py index aec20428..d24ff5aa 100644 --- a/xdggs/utils.py +++ b/xdggs/utils.py @@ -1,3 +1,5 @@ +import xarray as xr + GRID_REGISTRY = {} @@ -17,3 +19,20 @@ def _extract_cell_id_variable(variables): dim = next(iter(var.dims)) return name, var, dim + + +def call_on_dataset(func, obj, *args, kwargs=None): + if kwargs is None: + kwargs = {} + + if isinstance(obj, xr.DataArray): + ds = obj._to_temp_dataset() + else: + ds = obj + + result = func(ds, *args, **kwargs) + + if isinstance(obj, xr.DataArray): + return xr.DataArray._from_temp_dataset(result, name=obj.name) + else: + return result From e9474b91ca0b74de524dc92ce6ada094db988fdd Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 16:05:02 +0200 Subject: [PATCH 07/27] refactor the decoding machinery --- xdggs/accessor.py | 26 ++++++++++----- xdggs/conventions/__init__.py | 6 ++-- xdggs/conventions/decoders.py | 33 +++++++++++++++++++ xdggs/conventions/{writers.py => encoders.py} | 2 +- 4 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 xdggs/conventions/decoders.py rename xdggs/conventions/{writers.py => encoders.py} (97%) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index a6170c7e..651380ae 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -30,7 +30,9 @@ def __init__(self, obj: xr.Dataset | xr.DataArray): self._name = name self._index = index - def decode(self, grid_info=None, *, name="cell_ids") -> xr.Dataset | xr.DataArray: + def decode( + self, grid_info=None, *, name="cell_ids", convention=None + ) -> xr.Dataset | xr.DataArray: """decode the DGGS cell ids Parameters @@ -46,13 +48,21 @@ def decode(self, grid_info=None, *, name="cell_ids") -> xr.Dataset | xr.DataArra obj : xarray.DataArray or xarray.Dataset The object with a DGGS index on the cell id coordinate. """ - var = self._obj[name] - if isinstance(grid_info, DGGSInfo): - grid_info = grid_info.to_dict() - if isinstance(grid_info, dict): - var.attrs = grid_info - - return self._obj.drop_indexes(name, errors="ignore").set_xindex(name, DGGSIndex) + if convention is None: + decoder = conventions.detect_decoder + elif callable(convention): + decoder = convention + else: + decoder = conventions.decoders.get(convention) + if decoder is None: + valid_names = conventions.decoders.keys() + raise ValueError( + f"unknown convention: {convention}." + f" Choose a known convention: {', '.join(valid_names)}" + ) + + coords = decoder(self._obj, grid_info=grid_info, name=name) + return self._obj.assign_coords(coords) @property def index(self) -> DGGSIndex: diff --git a/xdggs/conventions/__init__.py b/xdggs/conventions/__init__.py index f624f47e..4674b43c 100644 --- a/xdggs/conventions/__init__.py +++ b/xdggs/conventions/__init__.py @@ -1,5 +1,5 @@ +from xdggs.conventions import decoders, encoders # noqa: F401 from xdggs.conventions.registry import decoders as _decoders -from xdggs.conventions.registry import encoders as _encoders # noqa: F401 from xdggs.conventions.registry import ( register_decoder, register_encoder, @@ -10,8 +10,8 @@ class DecoderError(Exception): pass -def detect_convention_decoder(obj, grid_info, name): - for name, decoder in _decoders.items(): +def detect_decoder(obj, grid_info, name): + for decoder_name, decoder in _decoders.items(): try: return decoder(obj, grid_info=grid_info, name=name) except DecoderError: diff --git a/xdggs/conventions/decoders.py b/xdggs/conventions/decoders.py new file mode 100644 index 00000000..e9cd2918 --- /dev/null +++ b/xdggs/conventions/decoders.py @@ -0,0 +1,33 @@ +import xarray as xr + +from xdggs.conventions.registry import register_decoder +from xdggs.grid import DGGSInfo +from xdggs.utils import GRID_REGISTRY + + +@register_decoder("xdggs") +def xdggs(obj, grid_info, name): + try: + var = obj[name] + except IndexError: + raise ValueError("Cannot find the cell ids coordinate") + + if len(var.dims) != 1: + # TODO: allow 0D + raise ValueError("cell id coordinate must be 1D") + [dim] = var.dims + + if grid_info is None: + grid_info = var.attrs + elif isinstance(grid_info, DGGSInfo): + # TODO: avoid serializing / deserializing cycle + grid_info = grid_info.to_dict() + + grid_name = grid_info["grid_name"] + if grid_name not in GRID_REGISTRY: + raise ValueError(f"unknown grid name: {grid_name}") + index_cls = GRID_REGISTRY[grid_name] + + index = index_cls.from_variables({name: var}, options=grid_info) + + return xr.Coordinates({name: var.variable}, indexes={name: index}) diff --git a/xdggs/conventions/writers.py b/xdggs/conventions/encoders.py similarity index 97% rename from xdggs/conventions/writers.py rename to xdggs/conventions/encoders.py index 507b728f..06203dc9 100644 --- a/xdggs/conventions/writers.py +++ b/xdggs/conventions/encoders.py @@ -1,7 +1,7 @@ import numpy as np import xarray as xr -from xdggs.registry import register_encoder +from xdggs.conventions.registry import register_encoder from xdggs.utils import GRID_REGISTRY, call_on_dataset From 8e33eb5ef40ef7d02ca5ae4d9e6bc0f414267100 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 16:05:53 +0200 Subject: [PATCH 08/27] typo --- xdggs/conventions/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdggs/conventions/registry.py b/xdggs/conventions/registry.py index c28913f6..391ca01b 100644 --- a/xdggs/conventions/registry.py +++ b/xdggs/conventions/registry.py @@ -24,7 +24,7 @@ def register(func): def register_encoder(name): def register(func): - if name in decoders: + if name in encoders: warnings.warn( DecoderWarning(f"Overwriting existing convention encoder {name!r}.") ) From 27e3a19153077d368ff9782c162447ee5732d5d3 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 18:59:01 +0200 Subject: [PATCH 09/27] use the registry objects in the accessor --- xdggs/accessor.py | 12 +++--------- xdggs/conventions/__init__.py | 1 + 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index 651380ae..5d577e70 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -53,9 +53,9 @@ def decode( elif callable(convention): decoder = convention else: - decoder = conventions.decoders.get(convention) + decoder = conventions._decoders.get(convention) if decoder is None: - valid_names = conventions.decoders.keys() + valid_names = conventions._decoders.keys() raise ValueError( f"unknown convention: {convention}." f" Choose a known convention: {', '.join(valid_names)}" @@ -262,13 +262,7 @@ def as_convention(self, convention: str): obj : xr.DataArray or xr.Dataset The object converted to the given dimension. """ - converters = { - "easygems": conventions.easygems, - "cf": conventions.cf, - "xdggs": conventions.xdggs, - } - - converter = converters.get(convention) + converter = conventions._encoders.get(convention) if converter is None: raise ValueError(f"unknown convention: {convention}") diff --git a/xdggs/conventions/__init__.py b/xdggs/conventions/__init__.py index 4674b43c..8ad165df 100644 --- a/xdggs/conventions/__init__.py +++ b/xdggs/conventions/__init__.py @@ -1,5 +1,6 @@ from xdggs.conventions import decoders, encoders # noqa: F401 from xdggs.conventions.registry import decoders as _decoders +from xdggs.conventions.registry import encoders as _encoders # noqa: F401 from xdggs.conventions.registry import ( register_decoder, register_encoder, From 4635a027a884d233548871092a179024a75c5c30 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 18:59:35 +0200 Subject: [PATCH 10/27] remove the `grid_name` attr from the metadata --- xdggs/conventions/encoders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xdggs/conventions/encoders.py b/xdggs/conventions/encoders.py index 06203dc9..dfab0738 100644 --- a/xdggs/conventions/encoders.py +++ b/xdggs/conventions/encoders.py @@ -67,6 +67,7 @@ def _convert(ds): grid_name = infer_grid_name(ds.dggs.index) metadata = grid_info.to_dict() | {"grid_mapping_name": grid_name} metadata["refinement_level"] = metadata.pop("level") + metadata.pop("grid_name", None) crs = xr.Variable((), np.int8(0), metadata) From cbe7698287c82999248cab8047cdec78a011698f Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 20:34:45 +0200 Subject: [PATCH 11/27] implement the cf convention decoder --- xdggs/conventions/decoders.py | 43 ++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/xdggs/conventions/decoders.py b/xdggs/conventions/decoders.py index e9cd2918..b05f2456 100644 --- a/xdggs/conventions/decoders.py +++ b/xdggs/conventions/decoders.py @@ -2,7 +2,7 @@ from xdggs.conventions.registry import register_decoder from xdggs.grid import DGGSInfo -from xdggs.utils import GRID_REGISTRY +from xdggs.utils import GRID_REGISTRY, call_on_dataset @register_decoder("xdggs") @@ -31,3 +31,44 @@ def xdggs(obj, grid_info, name): index = index_cls.from_variables({name: var}, options=grid_info) return xr.Coordinates({name: var.variable}, indexes={name: index}) + + +@register_decoder("cf") +def cf(obj, grid_info, name): + vars_ = call_on_dataset( + lambda ds: ds.variables, + obj, + ) + grid_mapping_vars = { + name: var for name, var in vars_.items() if "grid_mapping_name" in var.attrs + } + if len(grid_mapping_vars) != 1: + raise ValueError("needs exactly one grid mapping variable for now") + crs = next(iter(grid_mapping_vars.values())) + + if name is None: + coords = list( + dict.fromkeys( + var.attrs["coordinates"] + for name, var in vars_.items() + if "coordinates" in var.attrs + ) + ) + name = coords[0] + var = vars_[name].copy(deep=False) + var.attrs.pop("standard_name", None) + var.attrs.pop("units", None) + + translations = {"refinement_level": "level"} + grid_info = { + translations.get(name, name): value for name, value in crs.attrs.items() + } + + grid_name = grid_info.pop("grid_mapping_name") + if grid_name not in GRID_REGISTRY: + raise ValueError(f"unknown grid name: {grid_name}") + index_cls = GRID_REGISTRY[grid_name] + + index = index_cls.from_variables({name: var}, options=grid_info) + + return xr.Coordinates({name: var}, indexes={name: index}) From 012da3154b527a4ec9df23d415bfb8fec77659ce Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 20:34:58 +0200 Subject: [PATCH 12/27] default to `name=None` and override it in the xdggs convention --- xdggs/accessor.py | 2 +- xdggs/conventions/decoders.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index 5d577e70..7c7e2049 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -31,7 +31,7 @@ def __init__(self, obj: xr.Dataset | xr.DataArray): self._index = index def decode( - self, grid_info=None, *, name="cell_ids", convention=None + self, grid_info=None, *, name=None, convention=None ) -> xr.Dataset | xr.DataArray: """decode the DGGS cell ids diff --git a/xdggs/conventions/decoders.py b/xdggs/conventions/decoders.py index b05f2456..0be0bdd9 100644 --- a/xdggs/conventions/decoders.py +++ b/xdggs/conventions/decoders.py @@ -7,6 +7,9 @@ @register_decoder("xdggs") def xdggs(obj, grid_info, name): + if name is None: + name = "cell_ids" + try: var = obj[name] except IndexError: From ac4ebe53f532f2632996d38bd93eaf298f126674 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 20:36:03 +0200 Subject: [PATCH 13/27] don't try to guess the convention --- xdggs/accessor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index 7c7e2049..24450468 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -31,7 +31,7 @@ def __init__(self, obj: xr.Dataset | xr.DataArray): self._index = index def decode( - self, grid_info=None, *, name=None, convention=None + self, grid_info=None, *, name=None, convention="xdggs" ) -> xr.Dataset | xr.DataArray: """decode the DGGS cell ids @@ -48,9 +48,7 @@ def decode( obj : xarray.DataArray or xarray.Dataset The object with a DGGS index on the cell id coordinate. """ - if convention is None: - decoder = conventions.detect_decoder - elif callable(convention): + if callable(convention): decoder = convention else: decoder = conventions._decoders.get(convention) From 45c7927dbf22390066c0ec52d698e414eed41af9 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Wed, 8 Oct 2025 23:00:19 +0200 Subject: [PATCH 14/27] extend the docstring of `decode` --- xdggs/accessor.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/xdggs/accessor.py b/xdggs/accessor.py index 24450468..e6f1d8d0 100644 --- a/xdggs/accessor.py +++ b/xdggs/accessor.py @@ -40,8 +40,22 @@ def decode( grid_info : dict or DGGSInfo, optional Override the grid parameters on the dataset. Useful to set attributes on the dataset. - name : str, default: "cell_ids" - The name of the coordinate containing the cell ids. + name : str, optional + The name of the coordinate containing the cell ids. The default name + depends on the convention. + convention : str, default: "xdggs" + The name of the metadata convention. Built-in conventions are: + + - "xdggs": the existing xdggs convention. ``name`` points to the + coordinate containing cell ids, and which has all the grid + metadata. The ``name`` parameter defaults to ``"cell_ids"``. + - "cf": the upcoming CF convention standardization. While the + convention extension is specialized on ``healpix`` for now, the + decoder can work with other DGGS as well. For this, all metadata + lives on a variable with a ``grid_mapping_name`` attribute, and + the cell ids coordinate is indicated by the ``coordinates`` + attribute on data variables / other coordinates (this can be + overridden by the ``name`` parameter). Returns ------- From 5dc6272f4cfbc0f363bf2f2dcbb1f58cab81ed9e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 11:46:32 +0200 Subject: [PATCH 15/27] tests for the xdggs convention decoder --- xdggs/tests/conventions/test_decoders.py | 107 +++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 xdggs/tests/conventions/test_decoders.py diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py new file mode 100644 index 00000000..230d0690 --- /dev/null +++ b/xdggs/tests/conventions/test_decoders.py @@ -0,0 +1,107 @@ +import numpy as np +import pytest +import xarray as xr + +import xdggs +from xdggs.conventions import decoders + + +def clear_attrs(var): + new_var = var.copy(deep=False) + new_var.attrs.clear() + + return new_var + + +def generate_h3_cells(level): + from h3ronpy import change_resolution + + base_cells = [ + 576495936675512319, + 576531121047601151, + 576566305419689983, + 576601489791778815, + 576636674163867647, + 576671858535956479, + 576707042908045311, + 576742227280134143, + 576777411652222975, + 576812596024311807, + ] + + return change_resolution(base_cells, level) + + +def create_coordinate(grid_name, dim, level, **options): + generators = { + "healpix": lambda level: np.arange(12 * 4**level), + "h3": generate_h3_cells, + } + + cell_ids = generators[grid_name](level) + grid_info = {"grid_name": grid_name, "refinement_level": level, **options} + return xr.Variable(dim, cell_ids, grid_info) + + +def create_index(name, coord, grid_info): + translations = {"refinement_level": "level"} + + if grid_info is None: + grid_info = { + translations.get(name, name): value for name, value in coord.attrs.items() + } + + var = coord.copy(deep=False) + var.attrs = grid_info + + return xdggs.HealpixIndex.from_variables({name: var}, options={}) + + +@pytest.mark.parametrize("obj_type", ["DataArray", "Dataset"]) +@pytest.mark.parametrize( + ["coord_name", "coord", "grid_info", "name"], + ( + pytest.param( + "cell_ids", + create_coordinate("healpix", "cells", 0, indexing_scheme="ring"), + None, + None, + id="healpix-all_defaults", + ), + pytest.param( + "cell_ids", + create_coordinate("h3", "cells", 0), + None, + None, + id="h3-all_defaults", + ), + pytest.param( + "zone_ids", + create_coordinate("healpix", "zones", 0, indexing_scheme="nested"), + None, + "zone_ids", + id="healpix-override_name", + ), + pytest.param( + "cell_ids", + clear_attrs(create_coordinate("h3", "cells", 2)), + {"grid_name": "h3", "level": 2}, + None, + id="h3-override_grid_info", + ), + ), +) +def test_xdggs(obj_type, coord_name, coord, grid_info, name): + if obj_type == "Dataset": + obj = xr.Dataset(coords={coord_name: coord}) + else: + obj = xr.DataArray( + np.zeros_like(coord.data), coords={coord_name: coord}, dims=coord.dims + ) + + expected = xr.Coordinates( + {coord_name: coord}, + indexes={coord_name: create_index(coord_name, coord, grid_info)}, + ) + actual = decoders.xdggs(obj, grid_info, name) + xr.testing.assert_identical(actual.to_dataset(), expected.to_dataset()) From 145dbed0cf90f466a8b9deb64744cbfb1de6ad27 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 11:46:59 +0200 Subject: [PATCH 16/27] add `h3-py` to the dev deps --- pixi.lock | 208 ++++++++++++++++++++++++++++++++++++------------- pyproject.toml | 1 + 2 files changed, 154 insertions(+), 55 deletions(-) diff --git a/pixi.lock b/pixi.lock index d8b5a274..7479bfac 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2388,6 +2388,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h3-4.3.0-h3e4d06c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/h3-py-4.3.0-py313h5d5ffb9_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h3ronpy-0.22.0-py313h920b4c0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h5netcdf-1.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/h5py-3.14.0-nompi_py313h253c126_101.conda @@ -2684,6 +2686,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glog-0.7.1-heb240a5_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h3-4.3.0-hc039f4b_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h3-py-4.3.0-py313hab38a8b_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h3ronpy-0.22.0-py313hdde674f_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h5netcdf-1.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h5py-3.14.0-nompi_py313ha10fd41_101.conda @@ -2967,6 +2971,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.13.1-h9ea8674_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/h3-4.3.0-h2854ae6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/h3-py-4.3.0-py313h927ade5_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/h3ronpy-0.22.0-py313hf3b5b86_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/h5netcdf-1.6.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/h5py-3.14.0-nompi_py313hf7f959b_101.conda @@ -4039,16 +4045,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.25.0-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda - - pypi: git+https://github.com/astropy/astropy.git#0519229eb0f0a1ff3fdbe4c5a0565c33b101bd9f - - pypi: https://files.pythonhosted.org/packages/03/8c/a556b71e4603d6156814da968b7cefa9bd882e59dc691168584418f4de77/astropy_iers_data-0.2025.9.15.0.37.0-py3-none-any.whl - - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#54d3467b28a87cdfdb696aacbd225ae1db81def1 + - pypi: git+https://github.com/astropy/astropy.git#0bc65a152cbc43f8e9f16ac7671f6b8e22fa755b + - pypi: https://files.pythonhosted.org/packages/c5/38/be921eaf8fc67333fd2c2657ea965103ccc4050c29a66d5fd5f3db881c0f/astropy_iers_data-0.2025.10.6.0.35.25-py3-none-any.whl + - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#1d34a649bfa7270562a5def7322abfdd0f6c9e4e - pypi: git+https://github.com/keewis/h3ronpy.git?subdirectory=h3ronpy&rev=version#98fa563245092e2578cc6f1f0d860a9f511475f1 - - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#7ad4a7652363b5f9b97e143434de4680f5dfd93f - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2396.gcc40732889/pandas-3.0.0.dev0+2396.gcc40732889-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#2c79f767ec4b9fe9634574c0f7f5857fa12230c6 + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2520.g10102e611d/pandas-3.0.0.dev0+2520.g10102e611d-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e5/e0/050018d855d26d3c0b4a7d1b2ed692be758ce276d8289e2a2b44ba1014a5/pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev19+g4394792ab/xarray-2025.9.1.dev19+g4394792ab-py3-none-any.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev47+gc703ce4ef/xarray-2025.9.1.dev47+gc703ce4ef-py3-none-any.whl - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/anywidget-0.9.18-pyhd8ed1ab_0.conda @@ -4248,16 +4254,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.1-h8359307_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.25.0-py313h9734d34_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda - - pypi: git+https://github.com/astropy/astropy.git#0519229eb0f0a1ff3fdbe4c5a0565c33b101bd9f - - pypi: https://files.pythonhosted.org/packages/03/8c/a556b71e4603d6156814da968b7cefa9bd882e59dc691168584418f4de77/astropy_iers_data-0.2025.9.15.0.37.0-py3-none-any.whl - - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#54d3467b28a87cdfdb696aacbd225ae1db81def1 + - pypi: git+https://github.com/astropy/astropy.git#0bc65a152cbc43f8e9f16ac7671f6b8e22fa755b + - pypi: https://files.pythonhosted.org/packages/c5/38/be921eaf8fc67333fd2c2657ea965103ccc4050c29a66d5fd5f3db881c0f/astropy_iers_data-0.2025.10.6.0.35.25-py3-none-any.whl + - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#1d34a649bfa7270562a5def7322abfdd0f6c9e4e - pypi: git+https://github.com/keewis/h3ronpy.git?subdirectory=h3ronpy&rev=version#98fa563245092e2578cc6f1f0d860a9f511475f1 - - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#7ad4a7652363b5f9b97e143434de4680f5dfd93f - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2396.gcc40732889/pandas-3.0.0.dev0+2396.gcc40732889-cp313-cp313-macosx_11_0_arm64.whl + - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#2c79f767ec4b9fe9634574c0f7f5857fa12230c6 + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2520.g10102e611d/pandas-3.0.0.dev0+2520.g10102e611d-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/11/4a/31a363370478b63c6289a34743f2ba2d3ae1bd8223e004d18ab28fb92385/pyerfa-2.0.1.5-cp39-abi3-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev19+g4394792ab/xarray-2025.9.1.dev19+g4394792ab-py3-none-any.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev47+gc703ce4ef/xarray-2025.9.1.dev47+gc703ce4ef-py3-none-any.whl - pypi: ./ win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda @@ -4449,16 +4455,16 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.1-h2466b09_2.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py313h5fd188c_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda - - pypi: git+https://github.com/astropy/astropy.git#0519229eb0f0a1ff3fdbe4c5a0565c33b101bd9f - - pypi: https://files.pythonhosted.org/packages/03/8c/a556b71e4603d6156814da968b7cefa9bd882e59dc691168584418f4de77/astropy_iers_data-0.2025.9.15.0.37.0-py3-none-any.whl - - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#54d3467b28a87cdfdb696aacbd225ae1db81def1 + - pypi: git+https://github.com/astropy/astropy.git#0bc65a152cbc43f8e9f16ac7671f6b8e22fa755b + - pypi: https://files.pythonhosted.org/packages/c5/38/be921eaf8fc67333fd2c2657ea965103ccc4050c29a66d5fd5f3db881c0f/astropy_iers_data-0.2025.10.6.0.35.25-py3-none-any.whl + - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#1d34a649bfa7270562a5def7322abfdd0f6c9e4e - pypi: git+https://github.com/keewis/h3ronpy.git?subdirectory=h3ronpy&rev=version#98fa563245092e2578cc6f1f0d860a9f511475f1 - - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#7ad4a7652363b5f9b97e143434de4680f5dfd93f - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2396.gcc40732889/pandas-3.0.0.dev0+2396.gcc40732889-cp313-cp313-win_amd64.whl + - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#2c79f767ec4b9fe9634574c0f7f5857fa12230c6 + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2520.g10102e611d/pandas-3.0.0.dev0+2520.g10102e611d-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b4/11/97233cf23ad5411ac6f13b1d6ee3888f90ace4f974d9bf9db887aa428912/pyerfa-2.0.1.5-cp39-abi3-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev19+g4394792ab/xarray-2025.9.1.dev19+g4394792ab-py3-none-any.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev47+gc703ce4ef/xarray-2025.9.1.dev47+gc703ce4ef-py3-none-any.whl - pypi: ./ packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -5099,35 +5105,35 @@ packages: - pkg:pypi/arrow?source=hash-mapping size: 99951 timestamp: 1733584345583 - - pypi: git+https://github.com/astropy/astropy.git#0519229eb0f0a1ff3fdbe4c5a0565c33b101bd9f + - pypi: git+https://github.com/astropy/astropy.git#0bc65a152cbc43f8e9f16ac7671f6b8e22fa755b name: astropy - version: 7.2.dev538+g0519229eb + version: 7.2.dev635+g0bc65a152 requires_dist: - numpy>=1.24 - pyerfa>=2.0.1.1 - - astropy-iers-data>=0.2025.9.1.0.42.11 + - astropy-iers-data>=0.2025.9.29.0.35.48 - pyyaml>=6.0.0 - packaging>=22.0.0 - scipy>=1.9.2 ; extra == 'recommended' - matplotlib>=3.8.0 ; extra == 'recommended' + - narwhals>=1.42.0 ; extra == 'recommended' - ipython>=8.0.0 ; extra == 'ipython' - astropy[ipython] ; extra == 'jupyter' - ipywidgets>=7.7.3 ; extra == 'jupyter' - ipykernel>=6.16.0 ; extra == 'jupyter' - ipydatagrid>=1.1.13 ; extra == 'jupyter' - jupyter-core>=4.11.2 ; extra == 'jupyter' - - pandas>=1.5.0 ; extra == 'jupyter' + - pandas>=2.0.0 ; extra == 'jupyter' - astropy[recommended] ; extra == 'all' - astropy[ipython] ; extra == 'all' - astropy[jupyter] ; extra == 'all' - certifi>=2022.6.15.1 ; extra == 'all' - - dask[array]>=2022.5.1 ; extra == 'all' + - dask[dataframe]>=2024.8.0 ; extra == 'all' - h5py>=3.9.0 ; extra == 'all' - - pyarrow>=10.0.1 ; extra == 'all' + - pyarrow>=14.0.2 ; extra == 'all' - beautifulsoup4>=4.9.3 ; extra == 'all' - html5lib>=1.1 ; extra == 'all' - bleach>=3.2.1 ; extra == 'all' - - pandas>=2.0 ; extra == 'all' - sortedcontainers>=1.5.7 ; extra == 'all' - pytz>=2016.10 ; extra == 'all' - jplephem>=2.15 ; extra == 'all' @@ -5153,7 +5159,8 @@ packages: - sgp4>=2.3 ; extra == 'test-all' - array-api-strict>=1.0 ; extra == 'test-all' - array-api-strict<2.4 ; python_full_version < '3.12' and extra == 'test-all' - - pandas-stubs>=2.0 ; extra == 'typing' + - pandas-stubs>=2.0.0 ; extra == 'typing' + - narwhals>=1.42.0 ; extra == 'typing' - astropy[recommended] ; extra == 'docs' - sphinx>=8.2.0 ; extra == 'docs' - sphinx-astropy[confv2]>=1.9.1 ; extra == 'docs' @@ -5372,10 +5379,10 @@ packages: - pkg:pypi/astropy?source=hash-mapping size: 9464202 timestamp: 1757120315355 - - pypi: https://files.pythonhosted.org/packages/03/8c/a556b71e4603d6156814da968b7cefa9bd882e59dc691168584418f4de77/astropy_iers_data-0.2025.9.15.0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c5/38/be921eaf8fc67333fd2c2657ea965103ccc4050c29a66d5fd5f3db881c0f/astropy_iers_data-0.2025.10.6.0.35.25-py3-none-any.whl name: astropy-iers-data - version: 0.2025.9.15.0.37.0 - sha256: 4ea19813150f2d6dfb0257c5e96d14ae9eac6e5a80af27ec79e3aaef8aadfa93 + version: 0.2025.10.6.0.35.25 + sha256: b6f572ee3ae4e02e4ae377229b533ed89d6f8eb027ae31e380cbc8096735ba49 requires_dist: - pytest ; extra == 'docs' - hypothesis ; extra == 'test' @@ -6671,7 +6678,7 @@ packages: - pkg:pypi/cached-property?source=hash-mapping size: 11065 timestamp: 1615209567874 - - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#54d3467b28a87cdfdb696aacbd225ae1db81def1 + - pypi: git+https://github.com/cds-astro/cds-healpix-python.git#1d34a649bfa7270562a5def7322abfdd0f6c9e4e name: cdshealpix version: 0.7.1 requires_dist: @@ -8125,6 +8132,98 @@ packages: - pkg:pypi/h2?source=compressed-mapping size: 95967 timestamp: 1756364871835 + - conda: https://conda.anaconda.org/conda-forge/linux-64/h3-4.3.0-h3e4d06c_1.conda + sha256: b9f46e0f9a24c321b065bc0d2563604a1b0fb7e02a06105e264411f74010a54e + md5: 7a9b1ee49ab4ac8ee2a0bda9e9f666d5 + depends: + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 132463 + timestamp: 1751353376725 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h3-4.3.0-hc039f4b_1.conda + sha256: 088306a565263030bb5c281a6c39a75a98c3db3a846ecbe2697a17d9119d688f + md5: cf54e9f5b7d0abd3060b10b2123be427 + depends: + - libcxx >=18 + - __osx >=11.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 129453 + timestamp: 1751353401084 + - conda: https://conda.anaconda.org/conda-forge/win-64/h3-4.3.0-h2854ae6_1.conda + sha256: 3f1d375d6dcd95cb9cf34cc7aa638c63f7fbcc23362c1aa80e7a0a1c578dff45 + md5: 3fd116e60190e3940f3dcae5a21a9fd0 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 125555 + timestamp: 1751353416300 + - conda: https://conda.anaconda.org/conda-forge/linux-64/h3-py-4.3.0-py313h5d5ffb9_2.conda + sha256: 872fc4bea50c561302cfdf2186199818744dec1111c451d432d6765a8f599c68 + md5: e82220be2170a7c6cabec5d84a980ed3 + depends: + - python + - numpy + - h3 ==4.3.0 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/h3?source=hash-mapping + size: 369574 + timestamp: 1756762902201 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/h3-py-4.3.0-py313hab38a8b_2.conda + sha256: 43bc37ba2a97d62a8793a6489894b5d0f6b3424aed8775a99d09824459241867 + md5: 9df00ebbf3438029049698ff234b5c13 + depends: + - python + - numpy + - h3 ==4.3.0 + - libcxx >=19 + - __osx >=11.0 + - python 3.13.* *_cp313 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/h3?source=hash-mapping + size: 292854 + timestamp: 1756762925790 + - conda: https://conda.anaconda.org/conda-forge/win-64/h3-py-4.3.0-py313h927ade5_2.conda + sha256: 99ce3204be895392d16d634bfc82c7b7a7256c61f5e33a10c44b1d641f63ce48 + md5: f840d9c02f75adaed96f89ac922916d1 + depends: + - python + - numpy + - h3 ==4.3.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/h3?source=hash-mapping + size: 303912 + timestamp: 1756762936753 - pypi: git+https://github.com/keewis/h3ronpy.git?subdirectory=h3ronpy&rev=version#98fa563245092e2578cc6f1f0d860a9f511475f1 name: h3ronpy version: 0.22.0 @@ -8590,7 +8689,7 @@ packages: purls: [] size: 2031491 timestamp: 1753357255237 - - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#7ad4a7652363b5f9b97e143434de4680f5dfd93f + - pypi: git+https://github.com/eopf-dggs/healpix-geo.git#2c79f767ec4b9fe9634574c0f7f5857fa12230c6 name: healpix-geo version: 0.0.7 requires_dist: @@ -13294,16 +13393,16 @@ packages: - pkg:pypi/packaging?source=hash-mapping size: 62477 timestamp: 1745345660407 - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2396.gcc40732889/pandas-3.0.0.dev0+2396.gcc40732889-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2520.g10102e611d/pandas-3.0.0.dev0+2520.g10102e611d-cp313-cp313-macosx_11_0_arm64.whl name: pandas - version: 3.0.0.dev0+2396.gcc40732889 + version: 3.0.0.dev0+2520.g10102e611d requires_dist: - numpy>=1.26.0 - python-dateutil>=2.8.2 - tzdata>=2023.3 - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=3.4.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' - pyarrow>=13.0.0 ; extra == 'pyarrow' - bottleneck>=1.4.2 ; extra == 'performance' - numba>=0.60.0 ; extra == 'performance' @@ -13365,8 +13464,8 @@ packages: - pymysql>=1.1.1 ; extra == 'all' - pyqt5>=5.15.9 ; extra == 'all' - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=3.4.0 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' - python-calamine>=0.3.0 ; extra == 'all' - pytz>=2024.2 ; extra == 'all' - pyxlsb>=1.0.10 ; extra == 'all' @@ -13381,16 +13480,16 @@ packages: - xlsxwriter>=3.2.0 ; extra == 'all' - zstandard>=0.23.0 ; extra == 'all' requires_python: ">=3.11" - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2396.gcc40732889/pandas-3.0.0.dev0+2396.gcc40732889-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2520.g10102e611d/pandas-3.0.0.dev0+2520.g10102e611d-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl name: pandas - version: 3.0.0.dev0+2396.gcc40732889 + version: 3.0.0.dev0+2520.g10102e611d requires_dist: - numpy>=1.26.0 - python-dateutil>=2.8.2 - tzdata>=2023.3 - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=3.4.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' - pyarrow>=13.0.0 ; extra == 'pyarrow' - bottleneck>=1.4.2 ; extra == 'performance' - numba>=0.60.0 ; extra == 'performance' @@ -13452,8 +13551,8 @@ packages: - pymysql>=1.1.1 ; extra == 'all' - pyqt5>=5.15.9 ; extra == 'all' - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=3.4.0 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' - python-calamine>=0.3.0 ; extra == 'all' - pytz>=2024.2 ; extra == 'all' - pyxlsb>=1.0.10 ; extra == 'all' @@ -13468,16 +13567,16 @@ packages: - xlsxwriter>=3.2.0 ; extra == 'all' - zstandard>=0.23.0 ; extra == 'all' requires_python: ">=3.11" - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2396.gcc40732889/pandas-3.0.0.dev0+2396.gcc40732889-cp313-cp313-win_amd64.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/pandas/3.0.0.dev0+2520.g10102e611d/pandas-3.0.0.dev0+2520.g10102e611d-cp313-cp313-win_amd64.whl name: pandas - version: 3.0.0.dev0+2396.gcc40732889 + version: 3.0.0.dev0+2520.g10102e611d requires_dist: - numpy>=1.26.0 - python-dateutil>=2.8.2 - tzdata>=2023.3 - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=7.3.2 ; extra == 'test' - - pytest-xdist>=3.4.0 ; extra == 'test' + - pytest>=8.3.4 ; extra == 'test' + - pytest-xdist>=3.6.1 ; extra == 'test' - pyarrow>=13.0.0 ; extra == 'pyarrow' - bottleneck>=1.4.2 ; extra == 'performance' - numba>=0.60.0 ; extra == 'performance' @@ -13539,8 +13638,8 @@ packages: - pymysql>=1.1.1 ; extra == 'all' - pyqt5>=5.15.9 ; extra == 'all' - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=7.3.2 ; extra == 'all' - - pytest-xdist>=3.4.0 ; extra == 'all' + - pytest>=8.3.4 ; extra == 'all' + - pytest-xdist>=3.6.1 ; extra == 'all' - python-calamine>=0.3.0 ; extra == 'all' - pytz>=2024.2 ; extra == 'all' - pyxlsb>=1.0.10 ; extra == 'all' @@ -17562,9 +17661,9 @@ packages: - pkg:pypi/wrapt?source=hash-mapping size: 63385 timestamp: 1756851987645 - - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev19+g4394792ab/xarray-2025.9.1.dev19+g4394792ab-py3-none-any.whl + - pypi: https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/xarray/2025.9.1.dev47+gc703ce4ef/xarray-2025.9.1.dev47+gc703ce4ef-py3-none-any.whl name: xarray - version: 2025.9.1.dev19+g4394792ab + version: 2025.9.1.dev47+gc703ce4ef requires_dist: - numpy>=1.26 - packaging>=24.1 @@ -17572,10 +17671,9 @@ packages: - scipy>=1.13 ; extra == 'accel' - bottleneck ; extra == 'accel' - numbagg>=0.8 ; extra == 'accel' - - numba>=0.59 ; extra == 'accel' + - numba>=0.62 ; extra == 'accel' - flox>=0.9 ; extra == 'accel' - opt-einsum ; extra == 'accel' - - numpy<2.3 ; extra == 'accel' - xarray[accel,etc,io,parallel,viz] ; extra == 'complete' - netcdf4>=1.6.0 ; extra == 'io' - h5netcdf ; extra == 'io' @@ -17646,8 +17744,8 @@ packages: timestamp: 1756981236899 - pypi: ./ name: xdggs - version: 0.2.3.dev0+g6e574369a.d20250915 - sha256: 25ee4943bca97abde42e762b8dc1ff5ab22160b173c644dd9dcdbc85eaede470 + version: 0.2.3.dev15+g45c7927db.d20251009 + sha256: 3468e2264737d22c8b940aebdb85ce9d909ba407e3c6870f3f1fd323539294db requires_dist: - arro3-core>=0.4.0 - cdshealpix diff --git a/pyproject.toml b/pyproject.toml index a8f7c32a..525ff2e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -213,6 +213,7 @@ jupyterlab = "*" jupyter-resource-usage = "*" jupyterlab_code_formatter = "*" python-build = ">=1.3.0,<2" +h3-py = ">=4.3.0,<5" [tool.pixi.environments] nightly = { features = ["tests", "nightly"], no-default-feature = true } From 156b1e5fef67b16c72b912286b566a08cc56bd7e Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 11:48:58 +0200 Subject: [PATCH 17/27] override the variable metadata --- xdggs/conventions/decoders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xdggs/conventions/decoders.py b/xdggs/conventions/decoders.py index 0be0bdd9..7732188f 100644 --- a/xdggs/conventions/decoders.py +++ b/xdggs/conventions/decoders.py @@ -31,7 +31,9 @@ def xdggs(obj, grid_info, name): raise ValueError(f"unknown grid name: {grid_name}") index_cls = GRID_REGISTRY[grid_name] - index = index_cls.from_variables({name: var}, options=grid_info) + var_ = var.copy(deep=True) + var_.attrs = grid_info + index = index_cls.from_variables({name: var_}, options={}) return xr.Coordinates({name: var.variable}, indexes={name: index}) From c9b190ba54786b19516486eef1f0669bbb0b70d6 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 11:49:13 +0200 Subject: [PATCH 18/27] use the correct metadata for the convention --- xdggs/tests/conventions/test_decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py index 230d0690..6a7e86c7 100644 --- a/xdggs/tests/conventions/test_decoders.py +++ b/xdggs/tests/conventions/test_decoders.py @@ -39,7 +39,7 @@ def create_coordinate(grid_name, dim, level, **options): } cell_ids = generators[grid_name](level) - grid_info = {"grid_name": grid_name, "refinement_level": level, **options} + grid_info = {"grid_name": grid_name, "level": level, **options} return xr.Variable(dim, cell_ids, grid_info) From 7317f6eda6628d7e6704c1658f1f7cc54e39e28b Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 14:22:17 +0200 Subject: [PATCH 19/27] add jupyterlab-myst --- pixi.lock | 21 +++++++++++++++++++-- pyproject.toml | 1 + 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pixi.lock b/pixi.lock index d8c3228b..fe93b294 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2664,6 +2664,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-myst-2.4.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_code_formatter-3.0.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda @@ -3010,6 +3011,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-myst-2.4.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_code_formatter-3.0.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda @@ -3340,6 +3342,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.4.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-myst-2.4.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_code_formatter-3.0.2-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.27.3-pyhd8ed1ab_1.conda @@ -10430,6 +10433,20 @@ packages: - pkg:pypi/jupyterlab?source=compressed-mapping size: 8454849 timestamp: 1758914033168 + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-myst-2.4.2-pyhd8ed1ab_1.conda + sha256: 9ac0455d6ed74d317e7394dc89158d44d99089f35a144eee8964015903699475 + md5: 1a76cdfb2080ae8007cd58b79707fb6b + depends: + - jupyter_server >=2.0.1,<3 + - python >=3.9 + constrains: + - jupyterlab >=4,<5 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-myst?source=hash-mapping + size: 2125353 + timestamp: 1736260161474 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_code_formatter-3.0.2-pyhd8ed1ab_1.conda sha256: 43f2eab266e856785b4554681eb03561872a37aaee2688f4f8a389ed66d04ce3 md5: f41368c2e6ca4338e6d8a05eaeda35cf @@ -20032,8 +20049,8 @@ packages: timestamp: 1759875189286 - pypi: ./ name: xdggs - version: 0.2.3.dev19+gc9b190ba5.d20251009 - sha256: 39223f51784c0474137d916a8b202c7c2099e035f76ec6eebb501b25ac4dac2b + version: 0.3.1.dev22+g6e87ff742.d20251009 + sha256: daf76a1e03f7ead0a2b2438d4afc6c6a471f9f167d39d8316f40df96c6478dae requires_dist: - arro3-core>=0.4.0 - cdshealpix diff --git a/pyproject.toml b/pyproject.toml index f97135ce..f265caf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,6 +218,7 @@ 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" [tool.pixi.environments] nightly = { features = ["tests", "nightly"], no-default-feature = true } From d40bc5677192e5023760bec2d55a4256a70e5ba5 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 15:14:20 +0200 Subject: [PATCH 20/27] pass index options along --- xdggs/conventions/decoders.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/xdggs/conventions/decoders.py b/xdggs/conventions/decoders.py index 7732188f..deda4c14 100644 --- a/xdggs/conventions/decoders.py +++ b/xdggs/conventions/decoders.py @@ -6,7 +6,7 @@ @register_decoder("xdggs") -def xdggs(obj, grid_info, name): +def xdggs(obj, grid_info, name, index_options): if name is None: name = "cell_ids" @@ -33,13 +33,13 @@ def xdggs(obj, grid_info, name): var_ = var.copy(deep=True) var_.attrs = grid_info - index = index_cls.from_variables({name: var_}, options={}) + index = index_cls.from_variables({name: var_}, options=index_options) return xr.Coordinates({name: var.variable}, indexes={name: index}) @register_decoder("cf") -def cf(obj, grid_info, name): +def cf(obj, grid_info, name, index_options): vars_ = call_on_dataset( lambda ds: ds.variables, obj, @@ -60,20 +60,19 @@ def cf(obj, grid_info, name): ) ) name = coords[0] - var = vars_[name].copy(deep=False) - var.attrs.pop("standard_name", None) - var.attrs.pop("units", None) translations = {"refinement_level": "level"} grid_info = { translations.get(name, name): value for name, value in crs.attrs.items() } - grid_name = grid_info.pop("grid_mapping_name") + var = vars_[name].copy(deep=False) + var.attrs = grid_info + if grid_name not in GRID_REGISTRY: raise ValueError(f"unknown grid name: {grid_name}") index_cls = GRID_REGISTRY[grid_name] - index = index_cls.from_variables({name: var}, options=grid_info) + index = index_cls.from_variables({name: var}, options=index_options) return xr.Coordinates({name: var}, indexes={name: index}) From c1fbd1638a3115b591d5291dd439c1499d78223c Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 16:11:25 +0200 Subject: [PATCH 21/27] bump lock file --- pixi.lock | 47 +++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/pixi.lock b/pixi.lock index fe93b294..7ea8a030 100644 --- a/pixi.lock +++ b/pixi.lock @@ -229,7 +229,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py311h49ec1c0_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -460,7 +460,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py311h3696347_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -679,7 +679,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py311h3485c13_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -937,7 +937,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py312h4c3975b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -1168,7 +1168,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py312h163523d_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -1387,7 +1387,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py312he06e257_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -1644,7 +1644,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py313h07c4f96_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -1875,7 +1875,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py313hcdf3177_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -2094,7 +2094,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -2850,7 +2850,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py313h07c4f96_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -3189,7 +3189,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py313hcdf3177_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -3513,7 +3513,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -4424,7 +4424,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.2-py313h07c4f96_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -4658,7 +4658,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.2-py313hcdf3177_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -4880,7 +4880,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h18a62a1_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.2-py313h5ea7bf4_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda @@ -19224,18 +19224,17 @@ packages: purls: [] size: 3466348 timestamp: 1748388121356 - - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhe01879c_2.conda - sha256: 040a5a05c487647c089ad5e05ad5aff5942830db2a4e656f1e300d73436436f1 - md5: 30a0a26c8abccf4b7991d590fe17c699 + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.3.0-pyhcf101f3_0.conda + sha256: cb77c660b646c00a48ef942a9e1721ee46e90230c7c570cdeb5a893b5cce9bff + md5: d2732eb636c264dc9aa4cbee404b1a53 depends: - - python >=3.9 + - python >=3.10 - python license: MIT - license_family: MIT purls: - pkg:pypi/tomli?source=compressed-mapping - size: 21238 - timestamp: 1753796677376 + size: 20973 + timestamp: 1760014679845 - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.0.0-pyhd8ed1ab_1.conda sha256: eda38f423c33c2eaeca49ed946a8d3bf466cc3364970e083a65eb2fd85258d87 md5: 40d0ed782a8aaa16ef248e68c06c168d @@ -20049,8 +20048,8 @@ packages: timestamp: 1759875189286 - pypi: ./ name: xdggs - version: 0.3.1.dev22+g6e87ff742.d20251009 - sha256: daf76a1e03f7ead0a2b2438d4afc6c6a471f9f167d39d8316f40df96c6478dae + version: 0.3.1.dev24+gd40bc5677.d20251009 + sha256: d7ea91d0c2feb8856b041600f45c5cf5a143d51cadaa250154f3d0e1a13d57f8 requires_dist: - arro3-core>=0.4.0 - cdshealpix From 1906e86f912eb86f3bca94129034a05b71e74415 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 16:12:44 +0200 Subject: [PATCH 22/27] pass index options along --- xdggs/tests/conventions/test_decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py index 6a7e86c7..1f8847a7 100644 --- a/xdggs/tests/conventions/test_decoders.py +++ b/xdggs/tests/conventions/test_decoders.py @@ -103,5 +103,5 @@ def test_xdggs(obj_type, coord_name, coord, grid_info, name): {coord_name: coord}, indexes={coord_name: create_index(coord_name, coord, grid_info)}, ) - actual = decoders.xdggs(obj, grid_info, name) + actual = decoders.xdggs(obj, grid_info, name, index_options={}) xr.testing.assert_identical(actual.to_dataset(), expected.to_dataset()) From f29e55d282338baf3e3a52b4e4b6eb28244c4820 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 17:14:03 +0200 Subject: [PATCH 23/27] only try to convert dataset objects --- xdggs/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdggs/utils.py b/xdggs/utils.py index d24ff5aa..b94dbefc 100644 --- a/xdggs/utils.py +++ b/xdggs/utils.py @@ -32,7 +32,7 @@ def call_on_dataset(func, obj, *args, kwargs=None): result = func(ds, *args, **kwargs) - if isinstance(obj, xr.DataArray): + if isinstance(obj, xr.DataArray) and isinstance(result, xr.Dataset): return xr.DataArray._from_temp_dataset(result, name=obj.name) else: return result From 401866830f231a0fb7a17093d37d900be46e06e0 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 17:14:59 +0200 Subject: [PATCH 24/27] move the cell id creation into a separate function --- xdggs/tests/conventions/test_decoders.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py index 1f8847a7..6b6c9921 100644 --- a/xdggs/tests/conventions/test_decoders.py +++ b/xdggs/tests/conventions/test_decoders.py @@ -32,14 +32,19 @@ def generate_h3_cells(level): return change_resolution(base_cells, level) -def create_coordinate(grid_name, dim, level, **options): +def generate_cell_ids(grid_name, level): generators = { "healpix": lambda level: np.arange(12 * 4**level), "h3": generate_h3_cells, } - cell_ids = generators[grid_name](level) + return generators[grid_name](level) + + +def create_coordinate(grid_name, dim, level, **options): + cell_ids = generate_cell_ids(grid_name, level) grid_info = {"grid_name": grid_name, "level": level, **options} + return xr.Variable(dim, cell_ids, grid_info) From 60dc4e8c23b9e214766c974f119d822293845ff8 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 17:15:19 +0200 Subject: [PATCH 25/27] refactor the index creation function --- xdggs/tests/conventions/test_decoders.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py index 6b6c9921..18b13bb4 100644 --- a/xdggs/tests/conventions/test_decoders.py +++ b/xdggs/tests/conventions/test_decoders.py @@ -48,18 +48,15 @@ def create_coordinate(grid_name, dim, level, **options): return xr.Variable(dim, cell_ids, grid_info) -def create_index(name, coord, grid_info): - translations = {"refinement_level": "level"} +def create_index(name, coord, options): + translations = {"refinement_level": "level", "grid_mapping_name": "grid_name"} - if grid_info is None: - grid_info = { - translations.get(name, name): value for name, value in coord.attrs.items() - } + if not options: + options = coord.attrs - var = coord.copy(deep=False) - var.attrs = grid_info + grid_info = {translations.get(name, name): value for name, value in options.items()} - return xdggs.HealpixIndex.from_variables({name: var}, options={}) + return xdggs.HealpixIndex.from_variables({name: coord}, options=grid_info) @pytest.mark.parametrize("obj_type", ["DataArray", "Dataset"]) From 0acb855ca7e6ee56374bd28f109562dc11dfa098 Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 17:16:05 +0200 Subject: [PATCH 26/27] add tests for the cf convention decoder --- xdggs/tests/conventions/test_decoders.py | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py index 18b13bb4..ca47cfb8 100644 --- a/xdggs/tests/conventions/test_decoders.py +++ b/xdggs/tests/conventions/test_decoders.py @@ -107,3 +107,75 @@ def test_xdggs(obj_type, coord_name, coord, grid_info, name): ) actual = decoders.xdggs(obj, grid_info, name, index_options={}) xr.testing.assert_identical(actual.to_dataset(), expected.to_dataset()) + + +@pytest.mark.parametrize("obj_type", ["DataArray", "Dataset"]) +@pytest.mark.parametrize( + ["coord_name", "dim", "metadata", "grid_info", "name"], + ( + pytest.param( + "cell_ids", + "cells", + { + "grid_mapping_name": "healpix", + "refinement_level": 1, + "indexing_scheme": "nested", + }, + None, + None, + id="healpix-all_defaults", + ), + pytest.param( + "cell_ids", + "cells", + {"grid_mapping_name": "h3", "refinement_level": 2}, + None, + None, + id="h3-all_defaults", + ), + pytest.param( + "zone_ids", + "zones", + { + "grid_mapping_name": "healpix", + "refinement_level": 1, + "indexing_scheme": "nested", + }, + None, + "zone_ids", + id="healpix-override_name", + ), + pytest.param( + "cell_ids", + "cells", + {"grid_mapping_name": "h3", "refinement_level": 2}, + {"grid_name": "h3", "level": 2}, + None, + id="h3-override_grid_info", + ), + ), +) +def test_cf(obj_type, coord_name, dim, metadata, grid_info, name): + cell_ids = generate_cell_ids("healpix", level=metadata["refinement_level"]) + coord = xr.Variable( + dim, + cell_ids, + {"standard_name": f"{metadata['grid_mapping_name']}_index", "units": 1}, + ) + crs = xr.Variable((), np.array(0, dtype="uint8"), metadata) + + if obj_type == "Dataset": + obj = xr.Dataset(coords={coord_name: coord, "crs": crs}) + else: + obj = xr.DataArray( + np.zeros_like(coord.data), + coords={coord_name: coord, "crs": crs}, + dims=coord.dims, + ) + + expected = xr.Coordinates( + {coord_name: coord}, + indexes={coord_name: create_index(coord_name, coord, grid_info=metadata)}, + ) + actual = decoders.cf(obj, grid_info, name, index_options={}) + xr.testing.assert_identical(actual.to_dataset(), expected.to_dataset()) From ebef9606695a48cdf198f830259a5ff8f5be305a Mon Sep 17 00:00:00 2001 From: Justus Magin Date: Thu, 9 Oct 2025 17:25:01 +0200 Subject: [PATCH 27/27] refactor the index creation --- xdggs/tests/conventions/test_decoders.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/xdggs/tests/conventions/test_decoders.py b/xdggs/tests/conventions/test_decoders.py index ca47cfb8..1dcf713a 100644 --- a/xdggs/tests/conventions/test_decoders.py +++ b/xdggs/tests/conventions/test_decoders.py @@ -48,15 +48,17 @@ def create_coordinate(grid_name, dim, level, **options): return xr.Variable(dim, cell_ids, grid_info) -def create_index(name, coord, options): +def create_index(name, coord, options=None): translations = {"refinement_level": "level", "grid_mapping_name": "grid_name"} - if not options: - options = coord.attrs + if options is None: + options = {} - grid_info = {translations.get(name, name): value for name, value in options.items()} + grid_info = { + translations.get(name, name): value for name, value in coord.attrs.items() + } - return xdggs.HealpixIndex.from_variables({name: coord}, options=grid_info) + return xdggs.HealpixIndex.from_variables({name: coord}, options=grid_info | options) @pytest.mark.parametrize("obj_type", ["DataArray", "Dataset"]) @@ -175,7 +177,7 @@ def test_cf(obj_type, coord_name, dim, metadata, grid_info, name): expected = xr.Coordinates( {coord_name: coord}, - indexes={coord_name: create_index(coord_name, coord, grid_info=metadata)}, + indexes={coord_name: create_index(coord_name, coord, options=metadata)}, ) actual = decoders.cf(obj, grid_info, name, index_options={}) xr.testing.assert_identical(actual.to_dataset(), expected.to_dataset())