diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d544d9a..6364472 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,4 +26,4 @@ repos: additional_dependencies: - types-simplejson - types-attrs - - pydantic~=2.0 \ No newline at end of file + - pydantic>=2.11 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 56c483e..795181b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ requires-python = ">=3.11" dependencies = [ "pydantic-zarr>=0.8.0", + "pydantic>=2.11", "zarr>=3.1.1", "xarray>=2025.7.1", "dask[array,distributed]>=2025.5.1", @@ -111,7 +112,7 @@ use_parentheses = true ensure_newline_before_comments = true [tool.mypy] -python_version = "3.10" +python_version = "3.11" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true diff --git a/src/eopf_geozarr/data_api/geozarr/__init__.py b/src/eopf_geozarr/data_api/geozarr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/eopf_geozarr/data_api/geozarr/common.py b/src/eopf_geozarr/data_api/geozarr/common.py new file mode 100644 index 0000000..52a2db0 --- /dev/null +++ b/src/eopf_geozarr/data_api/geozarr/common.py @@ -0,0 +1,161 @@ +"""Common utilities for GeoZarr data API.""" + +import io +import urllib +import urllib.request +from typing import Annotated, Final, Literal + +from cf_xarray.utils import parse_cf_standard_name_table +from pydantic import AfterValidator, BaseModel + +XARRAY_DIMS_KEY: Final = "_ARRAY_DIMENSIONS" + + +def get_cf_standard_names(url: str) -> tuple[str, ...]: + """Retrieve the set of CF standard names and return them as a tuple.""" + + headers = {"User-Agent": "eopf_geozarr"} + + req = urllib.request.Request(url, headers=headers) + + try: + with urllib.request.urlopen(req) as response: + content = response.read() # Read the entire response body into memory + content_fobj = io.BytesIO(content) + except urllib.error.URLError as e: + raise e + + _info, table, _aliases = parse_cf_standard_name_table(source=content_fobj) + return tuple(table.keys()) + + +# This is a URL to the CF standard names table. +CF_STANDARD_NAME_URL = ( + "https://raw.githubusercontent.com/cf-convention/cf-convention.github.io/" + "master/Data/cf-standard-names/current/src/cf-standard-name-table.xml" +) + +# this does IO against github. consider locally storing this data instead if fetching every time +# is problematic. +CF_STANDARD_NAMES = get_cf_standard_names(url=CF_STANDARD_NAME_URL) + + +def check_standard_name(name: str) -> str: + """ + Check if the standard name is valid according to the CF conventions. + + Parameters + ---------- + name : str + The standard name to check. + + Returns + ------- + str + The validated standard name. + + Raises + ------ + ValueError + If the standard name is not valid. + """ + + if name in CF_STANDARD_NAMES: + return name + raise ValueError( + f"Invalid standard name: {name}. This name was not found in the list of CF standard names." + ) + + +CFStandardName = Annotated[str, AfterValidator(check_standard_name)] + +ResamplingMethod = Literal[ + "nearest", + "average", + "bilinear", + "cubic", + "cubic_spline", + "lanczos", + "mode", + "max", + "min", + "med", + "sum", + "q1", + "q3", + "rms", + "gauss", +] +"""A string literal indicating a resampling method""" + + +class TileMatrixLimit(BaseModel): + """""" + + tileMatrix: str + minTileCol: int + minTileRow: int + maxTileCol: int + maxTileRow: int + + +class TileMatrix(BaseModel): + id: str + scaleDenominator: float + cellSize: float + pointOfOrigin: tuple[float, float] + tileWidth: int + tileHeight: int + matrixWidth: int + matrixHeight: int + + +class TileMatrixSet(BaseModel): + id: str + title: str | None = None + crs: str | None = None + supportedCRS: str | None = None + orderedAxes: tuple[str, str] | None = None + tileMatrices: tuple[TileMatrix, ...] + + +class Multiscales(BaseModel, extra="allow"): + """ + Multiscale metadata for a GeoZarr dataset. + + Attributes + ---------- + tile_matrix_set : str + The tile matrix set identifier for the multiscale dataset. + resampling_method : ResamplingMethod + The name of the resampling method for the multiscale dataset. + tile_matrix_set_limits : dict[str, TileMatrixSetLimits] | None, optional + The tile matrix set limits for the multiscale dataset. + """ + + tile_matrix_set: TileMatrixSet + resampling_method: ResamplingMethod + # TODO: ensure that the keys match tile_matrix_set.tileMatrices[$index].id + # TODO: ensure that the keys match the tileMatrix attribute + tile_matrix_limits: dict[str, TileMatrixLimit] | None = None + + +class DatasetAttrs(BaseModel, extra="allow"): + """ + Attributes for a GeoZarr dataset. + + Attributes + ---------- + multiscales: MultiscaleAttrs + """ + + multiscales: Multiscales + + +class BaseDataArrayAttrs(BaseModel, extra="allow"): + """ + Base attributes for a GeoZarr DataArray. + + Attributes + ---------- + """ diff --git a/src/eopf_geozarr/data_api/geozarr/v2.py b/src/eopf_geozarr/data_api/geozarr/v2.py new file mode 100644 index 0000000..1435b6b --- /dev/null +++ b/src/eopf_geozarr/data_api/geozarr/v2.py @@ -0,0 +1,170 @@ +"""GeoZarr data API for Zarr V2.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Iterable, Literal, Self, TypeVar + +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic_zarr.v2 import ArraySpec, GroupSpec, auto_attributes + +from eopf_geozarr.data_api.geozarr.common import ( + XARRAY_DIMS_KEY, + BaseDataArrayAttrs, + Multiscales, +) + + +class DataArrayAttrs(BaseDataArrayAttrs): + """ + Attributes for a GeoZarr DataArray. + + Attributes + ---------- + array_dimensions : tuple[str, ...] + Alias for the _ARRAY_DIMENSIONS attribute, which lists the dimension names for this array. + """ + + # todo: validate that this names listed here are the names of zarr arrays + # unless the variable is an auxiliary variable + # see https://github.com/zarr-developers/geozarr-spec/blob/main/geozarr-spec.md#geozarr-coordinates + array_dimensions: tuple[str, ...] = Field(alias="_ARRAY_DIMENSIONS") + + model_config = ConfigDict(serialize_by_alias=True) + + +class DataArray(ArraySpec[DataArrayAttrs]): + """ + A GeoZarr DataArray variable. It must have attributes that contain an `"_ARRAY_DIMENSIONS"` + key, with a length that matches the dimensionality of the array. + + References + ---------- + https://github.com/zarr-developers/geozarr-spec/blob/main/geozarr-spec.md#geozarr-dataarray + """ + + @classmethod + def from_array( + cls, + array: Any, + chunks: tuple[int, ...] | Literal["auto"] = "auto", + attributes: Mapping[str, object] | Literal["auto"] = "auto", + fill_value: object | Literal["auto"] = "auto", + order: Literal["C", "F"] | Literal["auto"] = "auto", + filters: tuple[Any, ...] | Literal["auto"] = "auto", + dimension_separator: Literal[".", "/"] | Literal["auto"] = "auto", + compressor: Any | Literal["auto"] = "auto", + dimension_names: Iterable[str] | Literal["auto"] = "auto", + ) -> Self: + if attributes == "auto": + auto_attrs = dict(auto_attributes(array)) + else: + auto_attrs = dict(attributes) + if dimension_names != "auto": + auto_attrs = auto_attrs | {XARRAY_DIMS_KEY: tuple(dimension_names)} + model = super().from_array( + array=array, + chunks=chunks, + attributes=auto_attrs, + fill_value=fill_value, + order=order, + filters=filters, + dimension_separator=dimension_separator, + compressor=compressor, + ) + return model # type: ignore[no-any-return] + + @model_validator(mode="after") + def check_array_dimensions(self) -> Self: + if (len_dim := len(self.attributes.array_dimensions)) != ( + ndim := len(self.shape) + ): + msg = ( + f"The {XARRAY_DIMS_KEY} attribute has length {len_dim}, which does not " + f"match the number of dimensions for this array (got {ndim})." + ) + raise ValueError(msg) + return self + + @property + def array_dimensions(self) -> tuple[str, ...]: + return self.attributes.array_dimensions # type: ignore[no-any-return] + + +T = TypeVar("T", bound=GroupSpec[Any, Any]) + + +def check_valid_coordinates(model: T) -> T: + """ + Check if the coordinates of the DataArrays listed in a GeoZarr DataSet are valid. + + For each DataArray in the model, we check the dimensions associated with the DataArray. + For each dimension associated with a data variable, an array with the name of that data variable + must be present in the members of the group. + + Parameters + ---------- + model : GroupSpec[Any, Any] + The GeoZarr DataArray model to check. + + Returns + ------- + GroupSpec[Any, Any] + The validated GeoZarr DataArray model. + """ + if model.members is None: + raise ValueError("Model members cannot be None") + + arrays: dict[str, DataArray] = { + k: v for k, v in model.members.items() if isinstance(v, DataArray) + } + for key, array in arrays.items(): + for idx, dim in enumerate(array.array_dimensions): + if dim not in model.members: + raise ValueError( + f"Dimension '{dim}' for array '{key}' is not defined in the model members." + ) + member = model.members[dim] + if isinstance(member, GroupSpec): + raise ValueError( + f"Dimension '{dim}' for array '{key}' should be a group. Found an array instead." + ) + if member.shape[0] != array.shape[idx]: + raise ValueError( + f"Dimension '{dim}' for array '{key}' has a shape mismatch: " + f"{member.shape[0]} != {array.shape[idx]}." + ) + return model + + +class DatasetAttrs(BaseModel): + """ + Attributes for a GeoZarr dataset. + + Attributes + ---------- + multiscales: MultiscaleAttrs + """ + + multiscales: Multiscales + + +class Dataset(GroupSpec[DatasetAttrs, GroupSpec[Any, Any] | DataArray]): + """ + A GeoZarr Dataset. + """ + + @model_validator(mode="after") + def check_valid_coordinates(self) -> Self: + """ + Validate the coordinates of the GeoZarr DataSet. + + This method checks that all DataArrays in the dataset have valid coordinates + according to the GeoZarr specification. + + Returns + ------- + GroupSpec[Any, Any] + The validated GeoZarr DataSet. + """ + return check_valid_coordinates(self) diff --git a/src/eopf_geozarr/data_api/geozarr/v3.py b/src/eopf_geozarr/data_api/geozarr/v3.py new file mode 100644 index 0000000..813489b --- /dev/null +++ b/src/eopf_geozarr/data_api/geozarr/v3.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import Any, Self, TypeVar + +from pydantic import model_validator +from pydantic_zarr.v3 import ArraySpec, GroupSpec + +from eopf_geozarr.data_api.geozarr.common import BaseDataArrayAttrs, DatasetAttrs + + +class DataArray(ArraySpec[BaseDataArrayAttrs]): + """ + A Zarr array that represents as GeoZarr DataArray variable. + + The attributes of this array are defined in `BaseDataArrayAttrs`. + + This array has an additional constraint: the dimension_names field must be a tuple of strings. + + References + ---------- + https://github.com/zarr-developers/geozarr-spec/blob/main/geozarr-spec.md#geozarr-dataarray + """ + + dimension_names: tuple[str, ...] + + @property + def array_dimensions(self) -> tuple[str, ...]: + return self.dimension_names + + +T = TypeVar("T", bound=GroupSpec[Any, Any]) + + +def check_valid_coordinates(model: T) -> T: + """ + Check if the coordinates of the DataArrays listed in a GeoZarr DataSet are valid. + + For each DataArray in the model, we check the dimensions associated with the DataArray. + For each dimension associated with a data variable, an array with the name of that data variable + must be present in the members of the group, and the shape of that array must align with the + DataArray shape. + + + Parameters + ---------- + model : GroupSpec[Any, Any] + The GeoZarr DataArray model to check. + + Returns + ------- + GroupSpec[Any, Any] + The validated GeoZarr DataArray model. + """ + if model.members is None: + raise ValueError("Model members cannot be None") + + arrays: dict[str, DataArray] = { + k: v for k, v in model.members.items() if isinstance(v, DataArray) + } + for key, array in arrays.items(): + for idx, dim in enumerate(array.array_dimensions): + if dim not in model.members: + raise ValueError( + f"Dimension '{dim}' for array '{key}' is not defined in the model members." + ) + member = model.members[dim] + if isinstance(member, GroupSpec): + raise ValueError( + f"Dimension '{dim}' for array '{key}' should be a group. Found an array instead." + ) + if member.shape[0] != array.shape[idx]: + raise ValueError( + f"Dimension '{dim}' for array '{key}' has a shape mismatch: " + f"{member.shape[0]} != {array.shape[idx]}." + ) + return model + + +class Dataset(GroupSpec[DatasetAttrs, GroupSpec[Any, Any] | DataArray]): + """ + A GeoZarr Dataset. + """ + + @model_validator(mode="after") + def check_valid_coordinates(self) -> Self: + """ + Validate the coordinates of the GeoZarr DataSet. + + This method checks that all DataArrays in the dataset have valid coordinates + according to the GeoZarr specification. + + Returns + ------- + GroupSpec[Any, Any] + The validated GeoZarr DataSet. + """ + return check_valid_coordinates(self) diff --git a/tests/test_data_api/__init__.py b/tests/test_data_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data_api/conftest.py b/tests/test_data_api/conftest.py new file mode 100644 index 0000000..486a4ca --- /dev/null +++ b/tests/test_data_api/conftest.py @@ -0,0 +1,14153 @@ +from __future__ import annotations + +from zarr import open_group +from zarr.core.buffer import default_buffer_prototype + +example_zarr_json = r"""{ + "attributes": {}, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": { + "conditions": { + "attributes": {}, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/meteorology": { + "attributes": {}, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/meteorology/cams": { + "attributes": { + "Conventions": "CF-1.7", + "GRIB_centre": "ecmf", + "GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts", + "GRIB_edition": 1, + "GRIB_subCentre": 0, + "history": "2025-02-27T07:57 GRIB to CDM+CF via cfgrib-0.9.10.4/ecCodes-2.34.1 with {\"source\": \"tmp/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.SAFE/GRANULE/L1C_T32TLQ_A041032_20250113T103310/AUX_DATA/AUX_CAMSFO\", \"filter_by_keys\": {}, \"encode_cf\": [\"parameter\", \"time\", \"geography\", \"vertical\"]}", + "institution": "European Centre for Medium-Range Weather Forecasts" + }, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/meteorology/cams/surface": { + "shape": [], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "original GRIB coordinate for key: level(surface)", + "units": "1", + "_FillValue": "AAAAAAAA+H8=" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/aod865": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "aod865", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total Aerosol Optical Depth at 865nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210215, + "GRIB_shortName": "aod865", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Total Aerosol Optical Depth at 865nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/latitude": { + "shape": [ + 9 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "latitude", + "standard_name": "latitude", + "stored_direction": "decreasing", + "units": "degrees_north", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/number": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "ensemble member numerical id", + "standard_name": "realization", + "units": "1" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/z": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "geopotential", + "GRIB_cfVarName": "z", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Geopotential", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 129, + "GRIB_shortName": "z", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "m**2 s**-2", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "m**2 s**-2" + }, + "long_name": "Geopotential", + "standard_name": "geopotential", + "units": "m**2 s**-2", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/step": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "time since forecast_reference_time", + "standard_name": "forecast_period", + "dtype": "timedelta64[ns]", + "units": "minutes" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/omaod550": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "omaod550", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Organic Matter Aerosol Optical Depth at 550nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210210, + "GRIB_shortName": "omaod550", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Organic Matter Aerosol Optical Depth at 550nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/aod469": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "aod469", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total Aerosol Optical Depth at 469nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210213, + "GRIB_shortName": "aod469", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Total Aerosol Optical Depth at 469nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/aod670": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "aod670", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total Aerosol Optical Depth at 670nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210214, + "GRIB_shortName": "aod670", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Total Aerosol Optical Depth at 670nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/isobaricInhPa": { + "shape": [], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "pressure", + "positive": "down", + "standard_name": "air_pressure", + "stored_direction": "decreasing", + "units": "hPa", + "_FillValue": "AAAAAAAA+H8=" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/duaod550": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "duaod550", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Dust Aerosol Optical Depth at 550nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210209, + "GRIB_shortName": "duaod550", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "isobaricInhPa", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Dust Aerosol Optical Depth at 550nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/ssaod550": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "ssaod550", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Sea Salt Aerosol Optical Depth at 550nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210208, + "GRIB_shortName": "ssaod550", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Sea Salt Aerosol Optical Depth at 550nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/time": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "_eopf_attrs": { + "_eopf_decode_datetime64": "datetime64[ns]" + }, + "long_name": "initial time of forecast", + "standard_name": "forecast_reference_time", + "units": "days since 2025-01-13 00:00:00", + "calendar": "proleptic_gregorian" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/valid_time": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "_eopf_attrs": { + "_eopf_decode_datetime64": "datetime64[ns]" + }, + "long_name": "time", + "standard_name": "time", + "units": "days since 2025-01-13 10:33:00", + "calendar": "proleptic_gregorian" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/bcaod550": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "bcaod550", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Black Carbon Aerosol Optical Depth at 550nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210211, + "GRIB_shortName": "bcaod550", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Black Carbon Aerosol Optical Depth at 550nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/aod550": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "aod550", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total Aerosol Optical Depth at 550nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210207, + "GRIB_shortName": "aod550", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Total Aerosol Optical Depth at 550nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/longitude": { + "shape": [ + 9 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "longitude", + "standard_name": "longitude", + "units": "degrees_east", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/aod1240": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "aod1240", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total Aerosol Optical Depth at 1240nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210216, + "GRIB_shortName": "aod1240", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Total Aerosol Optical Depth at 1240nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/cams/suaod550": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "suaod550", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Sulphate Aerosol Optical Depth at 550nm", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 210212, + "GRIB_shortName": "suaod550", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "~", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "~" + }, + "long_name": "Sulphate Aerosol Optical Depth at 550nm", + "standard_name": "unknown", + "units": "~", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf": { + "attributes": { + "Conventions": "CF-1.7", + "GRIB_centre": "ecmf", + "GRIB_centreDescription": "European Centre for Medium-Range Weather Forecasts", + "GRIB_edition": 1, + "GRIB_subCentre": 0, + "history": "2025-02-27T07:57 GRIB to CDM+CF via cfgrib-0.9.10.4/ecCodes-2.34.1 with {\"source\": \"tmp/S2B_MSIL1C_20250113T103309_N0511_R108_T32TLQ_20250113T122458.SAFE/GRANULE/L1C_T32TLQ_A041032_20250113T103310/AUX_DATA/AUX_ECMWFT\", \"filter_by_keys\": {}, \"encode_cf\": [\"parameter\", \"time\", \"geography\", \"vertical\"]}", + "institution": "European Centre for Medium-Range Weather Forecasts" + }, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/meteorology/ecmwf/surface": { + "shape": [], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "original GRIB coordinate for key: level(surface)", + "units": "1", + "_FillValue": "AAAAAAAA+H8=" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/v10": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "v10", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "10 metre V wind component", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 166, + "GRIB_shortName": "10v", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "m s**-1", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "m s**-1" + }, + "long_name": "10 metre V wind component", + "standard_name": "unknown", + "units": "m s**-1", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/latitude": { + "shape": [ + 9 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "latitude", + "standard_name": "latitude", + "stored_direction": "decreasing", + "units": "degrees_north", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/number": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "ensemble member numerical id", + "standard_name": "realization", + "units": "1" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/step": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "time since forecast_reference_time", + "standard_name": "forecast_period", + "dtype": "timedelta64[ns]", + "units": "minutes" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/r": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "relative_humidity", + "GRIB_cfVarName": "r", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Relative humidity", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 157, + "GRIB_shortName": "r", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "isobaricInhPa", + "GRIB_units": "%", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "%" + }, + "long_name": "Relative humidity", + "standard_name": "relative_humidity", + "units": "%", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/isobaricInhPa": { + "shape": [], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "pressure", + "positive": "down", + "standard_name": "air_pressure", + "stored_direction": "decreasing", + "units": "hPa", + "_FillValue": "AAAAAAAA+H8=" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/tcwv": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "lwe_thickness_of_atmosphere_mass_content_of_water_vapor", + "GRIB_cfVarName": "tcwv", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total column vertically-integrated water vapour", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 137, + "GRIB_shortName": "tcwv", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "kg m**-2", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "kg m**-2" + }, + "long_name": "Total column vertically-integrated water vapour", + "standard_name": "lwe_thickness_of_atmosphere_mass_content_of_water_vapor", + "units": "kg m**-2", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/u10": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "unknown", + "GRIB_cfVarName": "u10", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "10 metre U wind component", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 165, + "GRIB_shortName": "10u", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "m s**-1", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "m s**-1" + }, + "long_name": "10 metre U wind component", + "standard_name": "unknown", + "units": "m s**-1", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/time": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "_eopf_attrs": { + "_eopf_decode_datetime64": "datetime64[ns]" + }, + "long_name": "initial time of forecast", + "standard_name": "forecast_reference_time", + "units": "days since 2025-01-13 00:00:00", + "calendar": "proleptic_gregorian" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/valid_time": { + "shape": [], + "data_type": "int64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "_eopf_attrs": { + "_eopf_decode_datetime64": "datetime64[ns]" + }, + "long_name": "time", + "standard_name": "time", + "units": "days since 2025-01-13 10:33:00", + "calendar": "proleptic_gregorian" + }, + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/tco3": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "atmosphere_mass_content_of_ozone", + "GRIB_cfVarName": "tco3", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Total column ozone", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 206, + "GRIB_shortName": "tco3", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "kg m**-2", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "kg m**-2" + }, + "long_name": "Total column ozone", + "standard_name": "atmosphere_mass_content_of_ozone", + "units": "kg m**-2", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/msl": { + "shape": [ + 9, + 9 + ], + "data_type": "float32", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9, + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + }, + { + "name": "blosc", + "configuration": { + "typesize": 4, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "GRIB_NV": 0, + "GRIB_Nx": 9, + "GRIB_Ny": 9, + "GRIB_cfName": "air_pressure_at_mean_sea_level", + "GRIB_cfVarName": "msl", + "GRIB_dataType": "fc", + "GRIB_gridDefinitionDescription": "Latitude/Longitude Grid", + "GRIB_gridType": "regular_ll", + "GRIB_iDirectionIncrementInDegrees": 0.177, + "GRIB_iScansNegatively": 0, + "GRIB_jDirectionIncrementInDegrees": 0.121, + "GRIB_jPointsAreConsecutive": 0, + "GRIB_jScansPositively": 0, + "GRIB_latitudeOfFirstGridPointInDegrees": 45.126, + "GRIB_latitudeOfLastGridPointInDegrees": 44.16, + "GRIB_longitudeOfFirstGridPointInDegrees": 6.457, + "GRIB_longitudeOfLastGridPointInDegrees": 7.872, + "GRIB_missingValue": 3.4028234663852886e+38, + "GRIB_name": "Mean sea level pressure", + "GRIB_numberOfPoints": 81, + "GRIB_paramId": 151, + "GRIB_shortName": "msl", + "GRIB_stepType": "instant", + "GRIB_stepUnits": 0, + "GRIB_totalNumber": 0, + "GRIB_typeOfLevel": "surface", + "GRIB_units": "Pa", + "_eopf_attrs": { + "coordinates": [ + "number", + "time", + "step", + "surface", + "latitude", + "longitude", + "valid_time", + "isobaricInhPa" + ], + "dimensions": [ + "latitude", + "longitude" + ], + "units": "Pa" + }, + "long_name": "Mean sea level pressure", + "standard_name": "air_pressure_at_mean_sea_level", + "units": "Pa", + "coordinates": "isobaricInhPa number step surface time valid_time", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "latitude", + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/meteorology/ecmwf/longitude": { + "shape": [ + 9 + ], + "data_type": "float64", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 9 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0.0, + "codecs": [ + { + "name": "bytes", + "configuration": { + "endian": "little" + } + } + ], + "attributes": { + "long_name": "longitude", + "standard_name": "longitude", + "units": "degrees_east", + "_FillValue": "AAAAAAAA+H8=" + }, + "dimension_names": [ + "longitude" + ], + "zarr_format": 3, + "node_type": "array", + "storage_transformers": [] + }, + "conditions/mask": { + "attributes": {}, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/mask/l1c_classification": { + "attributes": {}, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/mask/l1c_classification/r60m": { + "attributes": {}, + "zarr_format": 3, + "consolidated_metadata": { + "kind": "inline", + "must_understand": false, + "metadata": {} + }, + "node_type": "group" + }, + "conditions/mask/l1c_classification/r60m/b00": { + "shape": [ + 1830, + 1830 + ], + "data_type": "uint8", + "chunk_grid": { + "name": "regular", + "configuration": { + "chunk_shape": [ + 1830, + 1830 + ] + } + }, + "chunk_key_encoding": { + "name": "default", + "configuration": { + "separator": "/" + } + }, + "fill_value": 0, + "codecs": [ + { + "name": "bytes" + }, + { + "name": "blosc", + "configuration": { + "typesize": 1, + "cname": "zstd", + "clevel": 3, + "shuffle": "shuffle", + "blocksize": 0 + } + } + ], + "attributes": { + "_eopf_attrs": { + "coordinates": [ + "x", + "y" + ], + "dimensions": [ + "y", + "x" + ], + "flag_masks": [ + 1, + 2, + 4 + ], + "flag_meanings": [ + "OPAQUE", + "CIRRUS", + "SNOW_ICE" + ] + }, + "dtype": " None: + """ + Test the get_cf_standard_names function to ensure it retrieves the CF standard names correctly. + """ + standard_names = get_cf_standard_names(CF_STANDARD_NAME_URL) + assert isinstance(standard_names, tuple) + assert len(standard_names) > 0 + assert all(isinstance(name, str) for name in standard_names) + + +@pytest.mark.parametrize( + "name", ["air_temperature", "sea_surface_temperature", "precipitation_flux"] +) +def test_check_standard_name_valid(name: str) -> None: + """ + Test the check_standard_name function with valid standard names. + """ + assert check_standard_name(name) == name + + +def test_check_standard_name_invalid() -> None: + """ + Test the check_standard_name function with an invalid standard name. + """ + with pytest.raises(ValueError): + check_standard_name("invalid_standard_name") + + +def test_multiscales_round_trip() -> None: + """ + Ensure that we can round-trip multiscale metadata through the `Multiscales` model. + """ + from eopf_geozarr.data_api.geozarr.common import Multiscales + + source_untyped = GroupSpec_V3.from_zarr(example_group) + flat = source_untyped.to_flat() + meta = flat["/measurements/reflectance/r60m"].attributes["multiscales"] + assert Multiscales(**meta).model_dump() == tuplify_json(meta) diff --git a/tests/test_data_api/test_v2.py b/tests/test_data_api/test_v2.py new file mode 100644 index 0000000..bacfbb9 --- /dev/null +++ b/tests/test_data_api/test_v2.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest +from pydantic import ValidationError +from pydantic_zarr.v2 import ArraySpec, GroupSpec + +from eopf_geozarr.data_api.geozarr.v2 import ( + DataArray, + DataArrayAttrs, + check_valid_coordinates, +) + +from .conftest import example_group + + +def test_invalid_dimension_names() -> None: + msg = r"The _ARRAY_DIMENSIONS attribute has length 3, which does not match the number of dimensions for this array \(got 2\)" + with pytest.raises(ValidationError, match=msg): + DataArray.from_array(np.zeros((10, 10)), dimension_names=["x", "y", "z"]) + + +class TestCheckValidCoordinates: + @staticmethod + @pytest.mark.parametrize("data_shape", [(10,), (10, 12)]) + def test_valid(data_shape: tuple[int, ...]) -> None: + """ + Test the check_valid_coordinates function to ensure it validates coordinates correctly. + """ + + base_array = DataArray.from_array( + np.zeros((data_shape), dtype="uint8"), + dimension_names=[f"dim_{s}" for s in range(len(data_shape))], + ) + coords_arrays = { + f"dim_{idx}": DataArray.from_array( + np.arange(s), dimension_names=(f"dim_{idx}",) + ) + for idx, s in enumerate(data_shape) + } + group = GroupSpec[Any, DataArray](members={"base": base_array, **coords_arrays}) + assert check_valid_coordinates(group) == group + + @staticmethod + @pytest.mark.parametrize("data_shape", [(10,), (10, 12)]) + def test_invalid_coordinates( + data_shape: tuple[int, ...], + ) -> None: + """ + Test the check_valid_coordinates function to ensure it validates coordinates correctly. + + This test checks that the function raises a ValueError when the dimensions of the data variable + do not match the dimensions of the coordinate arrays. + """ + base_array = DataArray.from_array( + np.zeros((data_shape), dtype="uint8"), + dimension_names=[f"dim_{s}" for s in range(len(data_shape))], + ) + coords_arrays = { + f"dim_{idx}": DataArray.from_array( + np.arange(s + 1), dimension_names=(f"dim_{idx}",) + ) + for idx, s in enumerate(data_shape) + } + group = GroupSpec[Any, DataArray](members={"base": base_array, **coords_arrays}) + msg = "Dimension .* for array 'base' has a shape mismatch:" + with pytest.raises(ValueError, match=msg): + check_valid_coordinates(group) + + +@pytest.mark.skip(reason="We don't have a v2 example group yet") +def test_dataarray_attrs_round_trip() -> None: + """ + Ensure that we can round-trip dataarray attributes through the `Multiscales` model. + """ + source_untyped = GroupSpec.from_zarr(example_group) + flat = source_untyped.to_flat() + for key, val in flat.items(): + if isinstance(val, ArraySpec): + model_json = val.model_dump()["attributes"] + assert DataArrayAttrs(**model_json).model_dump() == model_json diff --git a/tests/test_data_api/test_v3.py b/tests/test_data_api/test_v3.py new file mode 100644 index 0000000..e9d9887 --- /dev/null +++ b/tests/test_data_api/test_v3.py @@ -0,0 +1,83 @@ +from typing import Any + +import numpy as np +import pytest +import zarr +from pydantic_zarr.core import tuplify_json +from pydantic_zarr.v3 import ArraySpec, GroupSpec + +from eopf_geozarr.data_api.geozarr.v3 import DataArray, Dataset, check_valid_coordinates + +from .conftest import example_group + + +class TestCheckValidCoordinates: + @staticmethod + @pytest.mark.parametrize("data_shape", [(10,), (10, 12)]) + def test_valid(data_shape: tuple[int, ...]) -> None: + """ + Test the check_valid_coordinates function to ensure it validates coordinates correctly. + """ + + base_array = DataArray.from_array( + np.zeros((data_shape), dtype="uint8"), + dimension_names=[f"dim_{s}" for s in range(len(data_shape))], + ) + coords_arrays = { + f"dim_{idx}": DataArray.from_array( + np.arange(s), dimension_names=(f"dim_{idx}",) + ) + for idx, s in enumerate(data_shape) + } + group = GroupSpec[Any, DataArray](members={"base": base_array, **coords_arrays}) + assert check_valid_coordinates(group) == group + + @staticmethod + @pytest.mark.parametrize("data_shape", [(10,), (10, 12)]) + def test_invalid_coordinates( + data_shape: tuple[int, ...], + ) -> None: + """ + Test the check_valid_coordinates function to ensure it validates coordinates correctly. + + This test checks that the function raises a ValueError when the dimensions of the data variable + do not match the dimensions of the coordinate arrays. + """ + base_array = DataArray.from_array( + np.zeros((data_shape), dtype="uint8"), + dimension_names=[f"dim_{s}" for s in range(len(data_shape))], + ) + coords_arrays = { + f"dim_{idx}": DataArray.from_array( + np.arange(s + 1), dimension_names=(f"dim_{idx}",) + ) + for idx, s in enumerate(data_shape) + } + group = GroupSpec[Any, DataArray](members={"base": base_array, **coords_arrays}) + msg = "Dimension .* for array 'base' has a shape mismatch:" + with pytest.raises(ValueError, match=msg): + check_valid_coordinates(group) + + +def test_dataarray_round_trip() -> None: + """ + Ensure that we can round-trip dataarray attributes through the `Multiscales` model. + """ + source_untyped = GroupSpec.from_zarr(example_group) + flat = source_untyped.to_flat() + for key, val in flat.items(): + if isinstance(val, ArraySpec) and val.dimension_names is not None: + model_json = val.model_dump() + assert DataArray(**model_json).model_dump() == model_json + + +def test_multiscale_attrs_round_trip() -> None: + """ + Test that multiscale datasets round-trip through the `Multiscales` model + """ + source_group_members = dict(example_group.members(max_depth=None)) + for key, val in source_group_members.items(): + if isinstance(val, zarr.Group): + if "multiscales" in val.attrs.asdict(): + model_json = GroupSpec.from_zarr(val).model_dump() + assert Dataset(**model_json).model_dump() == tuplify_json(model_json)