Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ classifiers = [
requires-python = ">=3.12"
dependencies = [
"xarray",
"pandas",
Comment thread
keewis marked this conversation as resolved.
"numpy>=2.0",
"cdshealpix",
"healpix-geo>=0.0.9",
Expand Down
60 changes: 46 additions & 14 deletions xdggs/conventions/zarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections.abc import Hashable
from typing import Any, Literal

import pandas as pd
import xarray as xr

from xdggs.conventions.base import Convention
Expand All @@ -27,6 +28,26 @@ def extract_convention_declaration(
return None


def _translate_metadata(
metadata: dict[str, Any],
key_translations: dict[str, str],
direction: Literal["forward", "inverse"] = "forward",
) -> dict[str, Any]:
if direction == "inverse":
key_translations = {v: k for k, v in key_translations.items()}
return {key_translations.get(key, key): value for key, value in metadata.items()}


dggs_attribute_translations = {
"name": "grid_name",
"refinement_level": "level",
}
ellipsoid_attribute_translations = {
"semi_major_axis": "semimajor_axis",
"semi_minor_axis": "semiminor_axis",
}


@register_convention("zarr")
class Zarr(Convention):
uuid = "7b255807-140c-42ca-97f6-7a1cfecdbc38"
Expand All @@ -45,16 +66,13 @@ def translate_metadata(
metadata: dict[str, Any],
direction: Literal["forward", "inverse"] = "forward",
) -> dict[str, Any]:
key_translations = {
"name": "grid_name",
"refinement_level": "level",
}
if direction == "inverse":
key_translations = {v: k for k, v in key_translations.items()}

return {
key_translations.get(key, key): value for key, value in metadata.items()
}
metadata = _translate_metadata(metadata, dggs_attribute_translations, direction)
ellipsoid = metadata.get("ellipsoid")
if ellipsoid is not None:
metadata["ellipsoid"] = _translate_metadata(
ellipsoid, ellipsoid_attribute_translations, direction
)
return metadata

def decode(
self,
Expand Down Expand Up @@ -105,13 +123,27 @@ def decode(
if spatial_dimension is None:
raise DecoderError("Required field `spatial_dimension` is missing or null.")

# optional, but required for now
if "refinement_level" not in metadata:
raise DecoderError("Required field `refinement_level` is missing.")

# optional
coordinate = metadata.pop("coordinate", None)
if name is not None:
# name takes precedence over coordinate
coordinate = name
if coordinate is None:
raise NotImplementedError("missing coordinate is not supported for now")
if "cell_ids" in ds.keys():
raise DecoderError(
"Coordinate not given, but cannot overwrite existing variable 'cell_ids'."
)
# create a memory-efficient range index
ds = ds.assign_coords(cell_ids=pd.RangeIndex(ds.sizes[spatial_dimension]))
coordinate = "cell_ids"
elif coordinate not in ds.keys():
raise DecoderError(f"Coordinate variable {coordinate}, does not exist.")
Comment thread
keewis marked this conversation as resolved.
Outdated

# optional, but required to be `"none"` for now
compression = metadata.pop("compression", None)
compression = metadata.pop("compression", "none")
if compression != "none":
raise NotImplementedError(
"compressed coordinates are not supported for now"
Expand All @@ -126,7 +158,7 @@ def decode(
if grid_name not in GRID_REGISTRY:
raise DecoderError(f"cf convention: unknown grid name: {grid_name}")
index_cls = GRID_REGISTRY[grid_name]
index = index_cls.from_variables({name: var}, options=index_options)
index = index_cls.from_variables({coordinate: var}, options=index_options)

new_ds = ds.assign_coords(xr.Coordinates.from_xindex(index)).assign_attrs(
copy.deepcopy(ds.attrs)
Expand Down
50 changes: 50 additions & 0 deletions xdggs/tests/test_conventions.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,25 @@ def test_encode(self, crs_attrs, grid_info, cell_ids, name, dim):
assert_indexes_equal(encoded.xindexes, expected.xindexes)


@pytest.fixture
def healpix_dataset():
data_vars = {"data": ("healpix_index", np.arange(12) % 2 == 0)}
attrs = {
"zarr_conventions": [Zarr.convention_metadata],
"dggs": {
"name": "healpix",
"refinement_level": 0,
"spatial_dimension": "healpix_index",
"ellipsoid": {
"name": "WGS84",
"semi_major_axis": 6378137.0,
"inverse_flattening": 298.257223563,
},
},
}
return xr.Dataset(data_vars=data_vars, attrs=attrs)


class TestZarrConvention:
def translate(self, mapping):
translations = {"grid_name": "name", "level": "refinement_level"}
Expand Down Expand Up @@ -305,6 +324,37 @@ def test_decode(self, grid_info, cell_ids, name, dim):
xr.testing.assert_identical(actual, expected)
assert_indexes_equal(actual[name].xindexes, expected[name].xindexes)

def test_decode_no_coordinate(self, healpix_dataset):
healpix_dataset.pipe(xdggs.decode, convention="zarr")
Comment thread
keewis marked this conversation as resolved.
Outdated

@pytest.mark.parametrize("key", ["zarr_conventions", "dggs"])
def test_raise_decode_error_missing_convention(self, key, healpix_dataset):
healpix_dataset.attrs.pop(key)
with pytest.raises(DecoderError):
healpix_dataset.pipe(xdggs.decode, convention="zarr")

@pytest.mark.parametrize("key", ["name", "refinement_level", "spatial_dimension"])
def test_raise_decode_error_missing_required(self, key, healpix_dataset):
healpix_dataset.attrs["dggs"].pop(key)
with pytest.raises(DecoderError, match=key):
healpix_dataset.pipe(xdggs.decode, convention="zarr")

def test_raise_decode_error_no_coordinate_but_default_exists(self, healpix_dataset):
# the default coordinate name is "cell_ids"
healpix_dataset["cell_ids"] = ("healpix_index", np.arange(12))
with pytest.raises(DecoderError, match="cell_ids"):
healpix_dataset.pipe(xdggs.decode, convention="zarr")

def test_raise_decode_error_coordinate_not_existing(self, healpix_dataset):
healpix_dataset.attrs["dggs"]["coordinate"] = "healpix_index"
with pytest.raises(DecoderError, match="does not exist"):
healpix_dataset.pipe(xdggs.decode, convention="zarr")

def test_raise_decode_error_unkown_dggs(self, healpix_dataset):
healpix_dataset.attrs["dggs"]["name"] = "DUMMY"
with pytest.raises(DecoderError, match="DUMMY"):
healpix_dataset.pipe(xdggs.decode, convention="zarr")

@pytest.mark.parametrize(
["name", "dim"], [("cell_ids", "cells"), ("zone_ids", "zones")]
)
Expand Down
Loading