From 7519b38deacc9c3ad07580c0925b1bd8b75e9386 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Thu, 15 May 2025 10:03:34 +0100 Subject: [PATCH 01/17] Improve chunking for Xarray generated with the earthkit engine (#700) * Improve chunking for Xarray generated with earthkit engine --- docs/examples/index.rst | 1 + docs/examples/xarray_engine_chunks.ipynb | 1245 ++++++++++++++++++ docs/examples/xarray_engine_temporal.ipynb | 6 +- src/earthkit/data/core/config.py | 5 + src/earthkit/data/core/fieldlist.py | 1 + src/earthkit/data/indexing/tensor.py | 44 +- src/earthkit/data/readers/grib/codes.py | 15 + src/earthkit/data/readers/grib/file.py | 58 +- src/earthkit/data/readers/grib/index/file.py | 4 +- src/earthkit/data/readers/grib/memory.py | 6 + src/earthkit/data/utils/message.py | 4 + src/earthkit/data/utils/xarray/builder.py | 49 +- src/earthkit/data/utils/xarray/fieldlist.py | 26 + tests/grib/test_grib_serialise.py | 19 + tests/xr_engine/test_xr_builder.py | 127 ++ tests/xr_engine/test_xr_chunks.py | 2 +- 16 files changed, 1552 insertions(+), 60 deletions(-) create mode 100644 docs/examples/xarray_engine_chunks.ipynb create mode 100644 tests/xr_engine/test_xr_builder.py diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 6b8574d72..65614ad8c 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -163,6 +163,7 @@ Xarray engine xarray_engine_to_grib.ipynb xarray_engine_split.ipynb xarray_engine_seasonal.ipynb + xarray_engine_chunks.ipynb Targets and encoders +++++++++++++++++++++ diff --git a/docs/examples/xarray_engine_chunks.ipynb b/docs/examples/xarray_engine_chunks.ipynb new file mode 100644 index 000000000..39eb8f6f9 --- /dev/null +++ b/docs/examples/xarray_engine_chunks.ipynb @@ -0,0 +1,1245 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f3568669-9884-491d-8597-5130ad273337", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Xarray engine: chunks" + ] + }, + { + "cell_type": "raw", + "id": "b42eccf8-abcc-44a1-8406-f8aa966b1bf5", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This notebook demonstrates how to use chunking in computations when a GRIB fieldlist is converted to to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. Chunking can be used to handle data that does not fit into memory." + ] + }, + { + "cell_type": "markdown", + "id": "8b1ceb8a-967d-4324-9af3-3b6eec468da1", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "First, we get 2m temperature data for a whole year on a low resolution regular latitude-longitude grid. It contains 2 fields per day (at 0 and 12 UTC). This data obviously fit into memory, so only used for demonstration purposes." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3a4f7dd0-f443-4cda-8725-cd61927d1409", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "98299fdfafa74aa5b8cbc0f95188b8d5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "t2_1_year_hourly.grib: 0%| | 0.00/429k [00:00\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray '2t' (valid_time: 732, latitude: 13, longitude: 24)> Size: 2MB\n",
+       "dask.array<open_dataset-2t, shape=(732, 13, 24), dtype=float64, chunksize=(10, 13, 24), chunktype=numpy.ndarray>\n",
+       "Coordinates:\n",
+       "  * valid_time  (valid_time) datetime64[ns] 6kB 2020-01-01 ... 2020-12-31T06:...\n",
+       "  * latitude    (latitude) float64 104B 90.0 75.0 60.0 ... -60.0 -75.0 -90.0\n",
+       "  * longitude   (longitude) float64 192B 0.0 15.0 30.0 ... 315.0 330.0 345.0\n",
+       "Attributes:\n",
+       "    standard_name:  air_temperature\n",
+       "    long_name:      2 metre temperature\n",
+       "    units:          K
" + ], + "text/plain": [ + " Size: 2MB\n", + "dask.array\n", + "Coordinates:\n", + " * valid_time (valid_time) datetime64[ns] 6kB 2020-01-01 ... 2020-12-31T06:...\n", + " * latitude (latitude) float64 104B 90.0 75.0 60.0 ... -60.0 -75.0 -90.0\n", + " * longitude (longitude) float64 192B 0.0 15.0 30.0 ... 315.0 330.0 345.0\n", + "Attributes:\n", + " standard_name: air_temperature\n", + " long_name: 2 metre temperature\n", + " units: K" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = ds_fl.to_xarray(time_dim_mode=\"valid_time\", \n", + " chunks={\"valid_time\": 10}, \n", + " add_earthkit_attrs=False)\n", + "ds[\"2t\"]" + ] + }, + { + "cell_type": "markdown", + "id": "d5caa260-5e6c-432b-96b7-ea84cb261432", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We compute the mean along the temporal dimension. Xarray will load data in chunks for this computation keeping the memory usage low." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "46e0abd9-7866-4e9f-9c89-c9234b372bc2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.DataArray '2t' (latitude: 13, longitude: 24)> Size: 2kB\n",
+       "array([[259.17798273, 259.17798273, 259.17798273, 259.17798273,\n",
+       "        259.17798273, 259.17798273, 259.17798273, 259.17798273,\n",
+       "        259.17798273, 259.17798273, 259.17798273, 259.17798273,\n",
+       "        259.17798273, 259.17798273, 259.17798273, 259.17798273,\n",
+       "        259.17798273, 259.17798273, 259.17798273, 259.17798273,\n",
+       "        259.17798273, 259.17798273, 259.17798273, 259.17798273],\n",
+       "       [273.2611026 , 275.61228088, 275.48984236, 274.29307835,\n",
+       "        268.16812105, 267.89195131, 264.09208792, 262.4144496 ,\n",
+       "        262.67648853, 261.67375629, 261.81749775, 261.75990725,\n",
+       "        261.65672248, 261.12205718, 260.31177713, 259.69160124,\n",
+       "        259.44480308, 258.91397999, 256.69544345, 261.1351634 ,\n",
+       "        263.80255581, 245.3709899 , 246.22366237, 263.91035124],\n",
+       "       [281.80054932, 277.3069957 , 278.84242945, 276.02408075,\n",
+       "        274.49351381, 274.21627678, 274.1331996 , 272.61215281,\n",
+       "        271.49176346, 269.63533416, 273.44181469, 275.6214595 ,\n",
+       "        276.75602234, 275.00730308, 276.87611285, 273.58944106,\n",
+       "        271.92337207, 268.99705718, 266.13354113, 265.23450595,\n",
+       "        271.60276073, 273.63473648, 279.19937105, 281.8119052 ],\n",
+       "       [284.15830206, 283.85715793, 286.20103601, 283.92187788,\n",
+       "        283.76810397, 284.21051346, 282.39472624, 279.20961695,\n",
+       "...\n",
+       "        283.64451278, 283.35801176, 282.91684031, 282.9554759 ,\n",
+       "        282.14711695, 282.26140144, 281.0409011 , 280.42200595],\n",
+       "       [269.24513607, 270.23454864, 271.63341305, 271.82437105,\n",
+       "        271.60942057, 270.65992432, 270.95410978, 271.93656367,\n",
+       "        273.56741237, 274.12688129, 273.18558177, 275.06365554,\n",
+       "        275.63789564, 274.6907901 , 272.28731504, 273.95432323,\n",
+       "        275.29179762, 275.50107016, 275.76251141, 276.15411831,\n",
+       "        273.34336866, 269.42683006, 268.963758  , 268.6785804 ],\n",
+       "       [234.73941544, 228.72784611, 229.33594038, 225.86723307,\n",
+       "        238.04060226, 241.2718608 , 229.15774707, 226.06100868,\n",
+       "        224.74338573, 228.59588744, 232.7254554 , 258.2240039 ,\n",
+       "        257.83465964, 258.4833106 , 262.08999605, 259.26575595,\n",
+       "        256.58991066, 260.79205376, 251.32129728, 250.12530172,\n",
+       "        253.13652952, 256.13648682, 258.81438129, 254.06318594],\n",
+       "       [227.70048102, 227.70048102, 227.70048102, 227.70048102,\n",
+       "        227.70048102, 227.70048102, 227.70048102, 227.70048102,\n",
+       "        227.70048102, 227.70048102, 227.70048102, 227.70048102,\n",
+       "        227.70048102, 227.70048102, 227.70048102, 227.70048102,\n",
+       "        227.70048102, 227.70048102, 227.70048102, 227.70048102,\n",
+       "        227.70048102, 227.70048102, 227.70048102, 227.70048102]])\n",
+       "Coordinates:\n",
+       "  * latitude   (latitude) float64 104B 90.0 75.0 60.0 45.0 ... -60.0 -75.0 -90.0\n",
+       "  * longitude  (longitude) float64 192B 0.0 15.0 30.0 45.0 ... 315.0 330.0 345.0
" + ], + "text/plain": [ + " Size: 2kB\n", + "array([[259.17798273, 259.17798273, 259.17798273, 259.17798273,\n", + " 259.17798273, 259.17798273, 259.17798273, 259.17798273,\n", + " 259.17798273, 259.17798273, 259.17798273, 259.17798273,\n", + " 259.17798273, 259.17798273, 259.17798273, 259.17798273,\n", + " 259.17798273, 259.17798273, 259.17798273, 259.17798273,\n", + " 259.17798273, 259.17798273, 259.17798273, 259.17798273],\n", + " [273.2611026 , 275.61228088, 275.48984236, 274.29307835,\n", + " 268.16812105, 267.89195131, 264.09208792, 262.4144496 ,\n", + " 262.67648853, 261.67375629, 261.81749775, 261.75990725,\n", + " 261.65672248, 261.12205718, 260.31177713, 259.69160124,\n", + " 259.44480308, 258.91397999, 256.69544345, 261.1351634 ,\n", + " 263.80255581, 245.3709899 , 246.22366237, 263.91035124],\n", + " [281.80054932, 277.3069957 , 278.84242945, 276.02408075,\n", + " 274.49351381, 274.21627678, 274.1331996 , 272.61215281,\n", + " 271.49176346, 269.63533416, 273.44181469, 275.6214595 ,\n", + " 276.75602234, 275.00730308, 276.87611285, 273.58944106,\n", + " 271.92337207, 268.99705718, 266.13354113, 265.23450595,\n", + " 271.60276073, 273.63473648, 279.19937105, 281.8119052 ],\n", + " [284.15830206, 283.85715793, 286.20103601, 283.92187788,\n", + " 283.76810397, 284.21051346, 282.39472624, 279.20961695,\n", + "...\n", + " 283.64451278, 283.35801176, 282.91684031, 282.9554759 ,\n", + " 282.14711695, 282.26140144, 281.0409011 , 280.42200595],\n", + " [269.24513607, 270.23454864, 271.63341305, 271.82437105,\n", + " 271.60942057, 270.65992432, 270.95410978, 271.93656367,\n", + " 273.56741237, 274.12688129, 273.18558177, 275.06365554,\n", + " 275.63789564, 274.6907901 , 272.28731504, 273.95432323,\n", + " 275.29179762, 275.50107016, 275.76251141, 276.15411831,\n", + " 273.34336866, 269.42683006, 268.963758 , 268.6785804 ],\n", + " [234.73941544, 228.72784611, 229.33594038, 225.86723307,\n", + " 238.04060226, 241.2718608 , 229.15774707, 226.06100868,\n", + " 224.74338573, 228.59588744, 232.7254554 , 258.2240039 ,\n", + " 257.83465964, 258.4833106 , 262.08999605, 259.26575595,\n", + " 256.58991066, 260.79205376, 251.32129728, 250.12530172,\n", + " 253.13652952, 256.13648682, 258.81438129, 254.06318594],\n", + " [227.70048102, 227.70048102, 227.70048102, 227.70048102,\n", + " 227.70048102, 227.70048102, 227.70048102, 227.70048102,\n", + " 227.70048102, 227.70048102, 227.70048102, 227.70048102,\n", + " 227.70048102, 227.70048102, 227.70048102, 227.70048102,\n", + " 227.70048102, 227.70048102, 227.70048102, 227.70048102,\n", + " 227.70048102, 227.70048102, 227.70048102, 227.70048102]])\n", + "Coordinates:\n", + " * latitude (latitude) float64 104B 90.0 75.0 60.0 45.0 ... -60.0 -75.0 -90.0\n", + " * longitude (longitude) float64 192B 0.0 15.0 30.0 45.0 ... 315.0 330.0 345.0" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "m = ds[\"2t\"].mean(dim=\"valid_time\").load()\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb9d85ea-a52f-4dc6-b081-7688f9c90536", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/xarray_engine_temporal.ipynb b/docs/examples/xarray_engine_temporal.ipynb index ad8945b35..1b1a76d57 100644 --- a/docs/examples/xarray_engine_temporal.ipynb +++ b/docs/examples/xarray_engine_temporal.ipynb @@ -2651,9 +2651,9 @@ ], "metadata": { "kernelspec": { - "display_name": "dev_ecc", + "display_name": "dev", "language": "python", - "name": "dev_ecc" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -2665,7 +2665,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/src/earthkit/data/core/config.py b/src/earthkit/data/core/config.py index 7464decb7..8ce1c2f71 100644 --- a/src/earthkit/data/core/config.py +++ b/src/earthkit/data/core/config.py @@ -246,6 +246,11 @@ def validate(self, name, value): fieldlists with data on disk. See :doc:`/guide/misc/grib_memory` for more information.""", ), + "grib-file-serialisation-policy": _( + "path", + """GRIB file serialisation policy for fieldlists with data on disk. {validator}""", + validator=ListValidator(["path", "memory"]), + ), } diff --git a/src/earthkit/data/core/fieldlist.py b/src/earthkit/data/core/fieldlist.py index 899c81131..20c858ae5 100644 --- a/src/earthkit/data/core/fieldlist.py +++ b/src/earthkit/data/core/fieldlist.py @@ -1093,6 +1093,7 @@ def _vals(f): first = next(it) is_property = not callable(getattr(first, accessor, None)) vals = _vals(first) + first = None ns = array_namespace(vals) shape = (n, *vals.shape) r = ns.empty(shape, dtype=vals.dtype) diff --git a/src/earthkit/data/indexing/tensor.py b/src/earthkit/data/indexing/tensor.py index 4d38c31c3..533f9d4f6 100644 --- a/src/earthkit/data/indexing/tensor.py +++ b/src/earthkit/data/indexing/tensor.py @@ -404,6 +404,13 @@ def field_indexes(self, indexes): assert len(indexes) == len(self._full_shape) return indexes[len(self._user_shape) :] + def is_full_field(self, indexes): + assert len(indexes) == len(self._field_shape) + for i, s in enumerate(indexes): + if not (s is None or s == slice(None, None, None) or s == slice(0, self._field_shape[i], 1)): + return False + return True + def _subset(self, indexes): """Only allow subsetting for the user coordinates. Indices for the field coordinates are ignored. @@ -496,19 +503,26 @@ def make_valid_datetime(self, dtype="datetime64[ns]"): return tuple(dims), vals.reshape(shape) return None, None + def __getstate__(self): + r = {} + r["source"] = self.source + r["user_coords"] = self.user_coords + r["user_shape"] = self.user_shape + r["user_dims"] = self.user_dims + r["field_coords"] = self.field_coords + r["field_shape"] = self.field_shape + r["field_dims"] = self.field_dims + r["full_shape"] = self.full_shape + r["flatten_values"] = self.flatten_values + return r -# class ArrayTensor(TensorCore): -# def __init__(self, array, coords, field_shape): -# self._array = array -# self._coords = coords -# self._shape = self._array.shape -# self._field_shape = field_shape - -# def to_numpy(self, **kwargs): -# return self._array - -# def _subset(self, indexes): -# coords = self._subset_coords(indexes) -# # print(f"{indexes=}") -# data = self._array[indexes] -# return ArrayTensor(data, coords, self.field_shape) + def __setstate__(self, state): + self.source = state["source"] + self._user_coords = state["user_coords"] + self._user_shape = state["user_shape"] + self._user_dims = state["user_dims"] + self._field_coords = state["field_coords"] + self._field_shape = state["field_shape"] + self._field_dims = state["field_dims"] + self._full_shape = state["full_shape"] + self.flatten_values = state["flatten_values"] diff --git a/src/earthkit/data/readers/grib/codes.py b/src/earthkit/data/readers/grib/codes.py index ed1c4be0f..f7f0ebf88 100644 --- a/src/earthkit/data/readers/grib/codes.py +++ b/src/earthkit/data/readers/grib/codes.py @@ -329,6 +329,21 @@ def message(self): def clone(self, **kwargs): return ClonedGribField(self, **kwargs) + def __getstate__(self): + state = super().__getstate__() + state["path"] = self.path + state["offset"] = self._offset + state["length"] = self._length + state["use_metadata_cache"] = self._use_metadata_cache + return state + + def __setstate__(self, state): + self.path = state["path"] + self._offset = state["offset"] + self._length = state["length"] + self._use_metadata_cache = state["use_metadata_cache"] + self._handle_manager = None + class ClonedGribField(ClonedFieldCore, GribField): def __init__(self, field, **kwargs): diff --git a/src/earthkit/data/readers/grib/file.py b/src/earthkit/data/readers/grib/file.py index 8449a1d90..1acf5a00a 100644 --- a/src/earthkit/data/readers/grib/file.py +++ b/src/earthkit/data/readers/grib/file.py @@ -18,7 +18,7 @@ class GRIBReader(GribFieldListInOneFile, Reader): appendable = True # GRIB messages can be added to the same file - def __init__(self, source, path, parts=None): + def __init__(self, source, path, parts=None, positions=None): _kwargs = {} for k in [ # "array_backend", @@ -34,7 +34,7 @@ def __init__(self, source, path, parts=None): raise KeyError(f"Invalid option {k} in GRIBReader. Option names must not contain '-'.") Reader.__init__(self, source, path) - GribFieldListInOneFile.__init__(self, path, parts=parts, **_kwargs) + GribFieldListInOneFile.__init__(self, path, parts=parts, positions=positions, **_kwargs) def __repr__(self): return "GRIBReader(%s)" % (self.path,) @@ -47,24 +47,42 @@ def is_streamable_file(self): return True def __getstate__(self): - r = {"kwargs": self.source._kwargs, "messages": []} - for f in self: - r["messages"].append(f.message()) + from earthkit.data.core.config import CONFIG + + policy = CONFIG.get("grib-file-serialisation-policy") + r = {"serialisation_policy": policy, "kwargs": self.source._kwargs} + + if policy == "path": + r["path"] = self.path + r["positions"] = self._positions + else: + r["messages"] = [f.message() for f in self] + return r def __setstate__(self, state): - from earthkit.data import from_source - from earthkit.data.core.caching import cache_file - - def _create(path, args): - with open(path, "wb") as f: - for message in state["messages"]: - f.write(message) - - path = cache_file( - "GRIBReader", - _create, - [], - ) - ds = from_source("file", path) - self.__init__(ds.source, path) + policy = state["serialisation_policy"] + if policy == "path": + from earthkit.data import from_source + + path = state["path"] + ds = from_source("file", path, **state["kwargs"]) + self.__init__(ds.source, path, positions=state["positions"]) + elif policy == "memory": + from earthkit.data import from_source + from earthkit.data.core.caching import cache_file + + def _create(path, args): + with open(path, "wb") as f: + for message in state["messages"]: + f.write(message) + + path = cache_file( + "GRIBReader", + _create, + [], + ) + ds = from_source("file", path) + self.__init__(ds.source, path) + else: + raise ValueError(f"Unknown serialisation policy {policy}") diff --git a/src/earthkit/data/readers/grib/index/file.py b/src/earthkit/data/readers/grib/index/file.py index 7e602781c..d8e493bfc 100644 --- a/src/earthkit/data/readers/grib/index/file.py +++ b/src/earthkit/data/readers/grib/index/file.py @@ -25,12 +25,12 @@ class GribFieldListInOneFile(GribFieldListInFiles): def availability_path(self): return os.path.join(self.path, ".availability.pickle") - def __init__(self, path, parts=None, **kwargs): + def __init__(self, path, parts=None, positions=None, **kwargs): assert isinstance(path, str), path self.path = path self._file_parts = parts - self.__positions = None + self.__positions = positions super().__init__(**kwargs) @property diff --git a/src/earthkit/data/readers/grib/memory.py b/src/earthkit/data/readers/grib/memory.py index b72b1b304..5087bf22d 100644 --- a/src/earthkit/data/readers/grib/memory.py +++ b/src/earthkit/data/readers/grib/memory.py @@ -168,6 +168,12 @@ def _release(self): def clone(self, **kwargs): return ClonedGribFieldInMemory(self, **kwargs) + def __getstate__(self): + return {"message": self.message()} + + def __setstate__(self, state): + self.__init__(GribCodesHandle.from_message(state["message"])) + class ClonedGribFieldInMemory(ClonedFieldCore, GribFieldInMemory): def __init__(self, field, **kwargs): diff --git a/src/earthkit/data/utils/message.py b/src/earthkit/data/utils/message.py index d925f179a..e9e2e74d6 100644 --- a/src/earthkit/data/utils/message.py +++ b/src/earthkit/data/utils/message.py @@ -201,6 +201,10 @@ def from_sample(cls, name): def _from_raw_handle(cls, handle): return cls(handle, None, None) + @classmethod + def from_message(cls, message): + return cls(eccodes.codes_new_from_message(message), None, None) + # TODO: just a wrapper around the base class implementation to handle the # s,l,d qualifiers. Once these are implemented in the base class this method can # be removed. md5GridSection is also handled! diff --git a/src/earthkit/data/utils/xarray/builder.py b/src/earthkit/data/utils/xarray/builder.py index 4d0276c82..6cb0b24a1 100644 --- a/src/earthkit/data/utils/xarray/builder.py +++ b/src/earthkit/data/utils/xarray/builder.py @@ -8,7 +8,6 @@ # import logging -import threading from abc import ABCMeta from abc import abstractmethod @@ -33,7 +32,14 @@ class VariableBuilder: def __init__( - self, name, var_dims, data_maker, tensor, remapping, local_attr_keys=None, fixed_local_attrs=None + self, + name, + var_dims, + data_maker, + tensor, + remapping, + local_attr_keys=None, + fixed_local_attrs=None, ): """ Create a builder for a single variable in the dataset. @@ -166,18 +172,21 @@ def attrs(self): class TensorBackendArray(xarray.backends.common.BackendArray): - def __init__(self, tensor, dims, shape, xp, dtype, variable): + def __init__(self, tensor, dims, shape, xp, dtype, var_name): super().__init__() self.tensor = tensor self.dims = dims self.shape = shape + self._var_name = var_name # xp and dtype must be set for xarray self.xp = xp if xp is not None else numpy if dtype is None: dtype = numpy.dtype("float64") self.dtype = xp.dtype(dtype) - self.lock = threading.Lock() + from dask.utils import SerializableLock + + self.lock = SerializableLock() @property def nbytes(self): @@ -206,16 +215,20 @@ def __getitem__(self, key: xarray.core.indexing.ExplicitIndexer): def _raw_indexing_method(self, key: tuple): with self.lock: - # print("_var", self._var) - # print(f"dims: {self.dims} key: {key} shape: {self.shape}") - # print(f"t-coords={self.tensor.user_coords}") + # LOG.debug(f"TensorBackendArray._raw_indexing_method var={self._var_name}") + # LOG.debug(f" dims={self.dims} key={key} shape={self.shape}") + # LOG.debug(f" tensor.user_coords={self.tensor.user_coords}") + r = self.tensor[key] - # print(r.source.ls()) - # print(f"r-shape: {r.user_shape}") + # LOG.debug(f" cubelet user_shape={r.user_shape}") + # LOG.debug(f" {r.user_shape=}") field_index = r.field_indexes(key) - # print(f"field.index={field_index} coords={r.user_coords}") - # result = r.to_numpy(index=field_index).squeeze() + if self.tensor.is_full_field(field_index): + field_index = None + + # LOG.debug(f" {field_index=}") + result = r.to_numpy(index=field_index, dtype=self.dtype) # ensure axes are squeezed when needed @@ -223,15 +236,9 @@ def _raw_indexing_method(self, key: tuple): if singles: result = result.squeeze(axis=tuple(singles)) - # print("result", result.shape) - # result = self.ekds.isel(**isels).to_numpy() - - # print("result", result.shape) - # print(f"Loaded {self.xp.__name__} with shape: {result.shape}") + # LOG.debug(f" {result.shape=}") - # Loading as numpy but then converting. This needs to be changed upstream (eccodes) - # to load directly into cupy. - # Maybe some incompatibilities when trying to copy from FFI to cupy directly + # Loading as numpy but then converting to the target array module if self.xp and self.xp != numpy: result = self.xp.asarray(result) @@ -444,6 +451,10 @@ def pre_build_variables(self): def build_values(self, tensor, var_dims, name): """Generate the data object stored in the xarray variable""" + # There is no need for the extra structures in the wrapped source in the + # tensor any longer. It is replaced by the original unwrapped fieldlist. + tensor.source = tensor.source.unwrap() + backend_array = TensorBackendArray( tensor, var_dims, diff --git a/src/earthkit/data/utils/xarray/fieldlist.py b/src/earthkit/data/utils/xarray/fieldlist.py index c4186da90..474cd62e5 100644 --- a/src/earthkit/data/utils/xarray/fieldlist.py +++ b/src/earthkit/data/utils/xarray/fieldlist.py @@ -103,6 +103,12 @@ def __repr__(self) -> str: class XArrayInputFieldList(FieldList): + """ + A wrapper around a fieldlist that stores unique values. + + Only for internal use for building Xarray datasets. + """ + def __init__(self, fieldlist, keys=None, db=None, remapping=None, scan_only=False, component=True): super().__init__() self.ds = fieldlist @@ -228,6 +234,26 @@ def unique_values(self, names, component=False): else: return indices, None + def unwrap(self): + ds = self.ds + while isinstance(ds, XArrayInputFieldList): + ds = ds.ds + return ds + + def __getstate__(self): + """As a simplification, only serialise the unwrapped fieldlist. + We can assume that when there is a need for serialisation the wrapper + structure can be discarded. + """ + r = {} + r["ds"] = self.unwrap() + return r + + def __setstate__(self, state): + self.ds = state["ds"] + self.db = None + self.remapping = None + class ReleasableField: def __init__(self, field): diff --git a/tests/grib/test_grib_serialise.py b/tests/grib/test_grib_serialise.py index 3eb4a75de..9cf0d47ab 100644 --- a/tests/grib/test_grib_serialise.py +++ b/tests/grib/test_grib_serialise.py @@ -16,6 +16,7 @@ import numpy as np import pytest +from earthkit.data import config from earthkit.data import from_source from earthkit.data.core.temporary import temp_file from earthkit.data.readers.grib.metadata import StandAloneGribMetadata @@ -25,6 +26,7 @@ here = os.path.dirname(__file__) sys.path.insert(0, here) +from grib_fixtures import FL_FILE # noqa: E402 from grib_fixtures import FL_NUMPY # noqa: E402 from grib_fixtures import load_grib_data # noqa: E402 @@ -215,3 +217,20 @@ def test_grib_serialise_file_parts(): assert len(ds2) == 1 assert ds2[0].metadata(["param", "level"]) == ["u", 1000] + + +@pytest.mark.parametrize("fl_type", FL_FILE) +@pytest.mark.parametrize("representation", ["file", "memory"]) +@pytest.mark.parametrize("policy", ["path", "memory"]) +def test_grib_serialise_policy(fl_type, representation, policy): + ds, _ = load_grib_data("test.grib", fl_type) + + with config.temporary({"grib-file-serialisation-policy": policy}): + ds2 = _pickle(ds, representation) + + assert len(ds2) == len(ds) + assert ds2.values.shape == ds.values.shape + if policy == "path": + assert ds2.path == ds.path + else: + assert ds2.path != ds.path diff --git a/tests/xr_engine/test_xr_builder.py b/tests/xr_engine/test_xr_builder.py new file mode 100644 index 000000000..d004a3cc2 --- /dev/null +++ b/tests/xr_engine/test_xr_builder.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import os +import pickle +import sys + +import pytest + +from earthkit.data import from_source +from earthkit.data.core.temporary import temp_file +from earthkit.data.testing import earthkit_remote_test_data_file + +here = os.path.dirname(__file__) +sys.path.insert(0, here) + +# Testing internal structures in the xarray engine + + +def _pickle(data, representation): + if representation == "file": + with temp_file() as tmp: + with open(tmp, "wb") as f: + pickle.dump(data, f) + + with open(tmp, "rb") as f: + data_res = pickle.load(f) + elif representation == "memory": + pickled_data = pickle.dumps(data) + data_res = pickle.loads(pickled_data) + else: + raise ValueError(f"Invalid representation: {representation}") + return data_res + + +@pytest.mark.cache +@pytest.mark.parametrize("representation", ["file", "memory"]) +def test_xr_engine_builder_fieldlist(representation): + ds_in = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl_small.grib")) + + from earthkit.data.utils.xarray.fieldlist import XArrayInputFieldList + + r = XArrayInputFieldList(ds_in) + assert not isinstance(r.ds, XArrayInputFieldList) + assert r.unwrap() is ds_in + r_p = _pickle(r, representation) + assert r_p is not r + assert r_p.ds is not r.ds + assert r_p.ds.metadata("time", astype=int) == [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1200, + 1200, + 1200, + 1200, + 1200, + 1200, + 1200, + 1200, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1200, + 1200, + 1200, + 1200, + 1200, + 1200, + 1200, + 1200, + ] + + r0 = r.sel(param="t", level=500) + assert not isinstance(r0.ds, XArrayInputFieldList) + assert len(r0) == 8 + assert r0.ds.metadata("time", astype=int) == [0, 0, 1200, 1200, 0, 0, 1200, 1200] + r0_uw = r0.unwrap() + assert not isinstance(r0_uw, XArrayInputFieldList) + assert r0_uw.metadata("time", astype=int) == [0, 0, 1200, 1200, 0, 0, 1200, 1200] + r0_p = _pickle(r0, representation) + assert r0_p is not r0 + assert r0_p.ds is not r0.ds + assert r0_p.ds.metadata("time", astype=int) == [0, 0, 1200, 1200, 0, 0, 1200, 1200] + + r1 = r0.order_by("time") + assert not isinstance(r1.ds, XArrayInputFieldList) + assert r1.ds.metadata("time", astype=int) == [0, 0, 0, 0, 1200, 1200, 1200, 1200] + r1_uw = r1.unwrap() + assert not isinstance(r1_uw, XArrayInputFieldList) + assert r1_uw.metadata("time", astype=int) == [0, 0, 0, 0, 1200, 1200, 1200, 1200] + r1_p = _pickle(r1, representation) + assert r1_p is not r1 + assert r1_p.ds is not r1.ds + assert r1_p.ds.metadata("time", astype=int) == [0, 0, 0, 0, 1200, 1200, 1200, 1200] + + r2 = r1.order_by("step") + assert not isinstance(r2.ds, XArrayInputFieldList) + assert r2.ds.metadata("time", astype=int) == [0, 0, 1200, 1200, 0, 0, 1200, 1200] + assert r2.ds.metadata("step", astype=int) == [0, 0, 0, 0, 6, 6, 6, 6] + r2_uw = r2.unwrap() + assert not isinstance(r2_uw, XArrayInputFieldList) + assert r2_uw.metadata("time", astype=int) == [0, 0, 1200, 1200, 0, 0, 1200, 1200] + assert r2_uw.metadata("step", astype=int) == [0, 0, 0, 0, 6, 6, 6, 6] + r2_p = _pickle(r2, representation) + assert r2_p is not r2 + assert r2_p.ds is not r2.ds + assert r2_p.ds.metadata("time", astype=int) == [0, 0, 1200, 1200, 0, 0, 1200, 1200] + assert r2_p.ds.metadata("step", astype=int) == [0, 0, 0, 0, 6, 6, 6, 6] diff --git a/tests/xr_engine/test_xr_chunks.py b/tests/xr_engine/test_xr_chunks.py index b5bf4267d..92bace7ab 100644 --- a/tests/xr_engine/test_xr_chunks.py +++ b/tests/xr_engine/test_xr_chunks.py @@ -96,7 +96,6 @@ def test_xr_engine_chunk_2(_kwargs): assert np.isclose(r.values.mean(), 275.9938876277779) -@pytest.mark.skipif(True, reason="Needs to be fixed") @pytest.mark.cache @pytest.mark.parametrize( "_kwargs", @@ -106,6 +105,7 @@ def test_xr_engine_chunk_2(_kwargs): {"chunks": {"valid_time": 1}}, {"chunks": {"valid_time": 10}}, {"chunks": {"valid_time": (100, 200, 432), "latitude": (4, 5, 4), "longitude": (13, 3, 8)}}, + {"chunks": {"valid_time": 100, "latitude": 4, "longitude": 7}}, {"chunks": -1}, ], ) From f86585815c0219afc33b22c2536c7b4b83a50cc8 Mon Sep 17 00:00:00 2001 From: Milton Gomez <87750447+msgomez06@users.noreply.github.com> Date: Thu, 15 May 2025 11:19:04 +0200 Subject: [PATCH 02/17] handle timezone-aware datetimes in forcings.py (#693) --- src/earthkit/data/sources/forcings.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/earthkit/data/sources/forcings.py b/src/earthkit/data/sources/forcings.py index 2495562ce..67661f1d0 100644 --- a/src/earthkit/data/sources/forcings.py +++ b/src/earthkit/data/sources/forcings.py @@ -141,7 +141,11 @@ def ecef_z(self, date): def julian_day(self, date): date = to_datetime(date) - delta = date - datetime.datetime(date.year, 1, 1) + if date.tzinfo is not None and date.tzinfo.utcoffset(date) is not None: + year_start = datetime.datetime(date.year, 1, 1, tzinfo=date.tzinfo) + else: + year_start = datetime.datetime(date.year, 1, 1) + delta = date - year_start julian_day = delta.days + delta.seconds / 86400.0 return np.full((np.prod(self.field.shape),), julian_day) @@ -156,7 +160,11 @@ def sin_julian_day(self, date): def local_time(self, date): lon = self.longitude(date) date = to_datetime(date) - delta = date - datetime.datetime(date.year, date.month, date.day) + if date.tzinfo is not None and date.tzinfo.utcoffset(date) is not None: + day_start = datetime.datetime(date.year, date.month, date.day, tzinfo=date.tzinfo) + else: + day_start = datetime.datetime(date.year, date.month, date.day) + delta = date - day_start hours_since_midnight = (delta.days + delta.seconds / 86400.0) * 24 return (lon / 360.0 * 24.0 + hours_since_midnight) % 24 From 6077f6dab389478294d7a56da835c031fb1e9e14 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Thu, 15 May 2025 18:01:31 +0100 Subject: [PATCH 03/17] Enable converting fields with UserMetadata into Xarray (#701) --- src/earthkit/data/indexing/fieldlist.py | 2 +- src/earthkit/data/utils/metadata/dict.py | 19 +++- src/earthkit/data/utils/xarray/builder.py | 25 +++-- src/earthkit/data/utils/xarray/grid.py | 21 +++- tests/xr_engine/test_xr_lod.py | 121 ++++++++++++++++++++++ 5 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 tests/xr_engine/test_xr_lod.py diff --git a/src/earthkit/data/indexing/fieldlist.py b/src/earthkit/data/indexing/fieldlist.py index 50f4d1662..686b05aa4 100644 --- a/src/earthkit/data/indexing/fieldlist.py +++ b/src/earthkit/data/indexing/fieldlist.py @@ -55,7 +55,7 @@ class _C(PandasMixIn, SimpleFieldList): def to_xarray(self, *args, **kwargs): # TODO make it generic if len(self) > 0: - if self[0]._metadata.data_format() == "grib": + if self[0]._metadata.data_format() in ("grib", "dict"): from earthkit.data.readers.grib.xarray import XarrayMixIn class _C(XarrayMixIn, SimpleFieldList): diff --git a/src/earthkit/data/utils/metadata/dict.py b/src/earthkit/data/utils/metadata/dict.py index 7623750cb..a598d8c19 100644 --- a/src/earthkit/data/utils/metadata/dict.py +++ b/src/earthkit/data/utils/metadata/dict.py @@ -180,6 +180,9 @@ def mars_area(self): def mars_grid(self): raise NotImplementedError("mars_grid is not implemented for this geography") + def grid_type(self): + return "none" + class UserGeography(Geography): def __init__(self, metadata, shape=None): @@ -252,6 +255,9 @@ def mars_area(self): def mars_grid(self): raise NotImplementedError("mars_grid is not implemented for this geography") + def grid_type(self): + return "_unstructured" + class DistinctLLGeography(UserGeography): def __init__(self, metadata): @@ -298,9 +304,11 @@ def shape(self): Ni = len(self._distinct_longitudes()) return (Nj, Ni) + def grid_type(self): + return "_distinct_ll" -class RegularDistinctLLGeography(DistinctLLGeography): +class RegularDistinctLLGeography(DistinctLLGeography): def dx(self): x = self.metadata.get("DxInDegrees", None) if x is None: @@ -326,6 +334,9 @@ def resolution(self): def mars_grid(self): return [self.dx(), self.dy()] + def grid_type(self): + return "_regular_ll" + class UserMetadata(Metadata): ALIASES = [ @@ -342,6 +353,7 @@ class UserMetadata(Metadata): "valid_datetime": "valid_datetime", "step_timedelta": "step_timedelta", "param_level": "param_level", + "_grid_type": "gridType", } LS_KEYS = ["param", "level", "base_datetime", "valid_datetime", "step", "number"] @@ -443,6 +455,11 @@ def _datetime(self, date_key, time_key): def param_level(self): return f"{self.get('param')}{self.get('level', default='')}" + def _grid_type(self): + if "gridType" in self._data: + return self._data["gridType"] + return self.geography.grid_type() + def _get_one(self, keys): for k in keys: if k in self._data: diff --git a/src/earthkit/data/utils/xarray/builder.py b/src/earthkit/data/utils/xarray/builder.py index 6cb0b24a1..f752ab3e3 100644 --- a/src/earthkit/data/utils/xarray/builder.py +++ b/src/earthkit/data/utils/xarray/builder.py @@ -74,12 +74,13 @@ def __init__( def build(self, add_earthkit_attrs=True): if add_earthkit_attrs: - md = self.tensor.source[0].metadata().override() - attrs = { - "message": md._handle.get_buffer(), - "bitsPerValue": md.get("bitsPerValue", 0), - } - self._attrs["_earthkit"] = attrs + if hasattr(self.tensor.source[0], "handle"): + md = self.tensor.source[0].metadata().override() + attrs = { + "message": md._handle.get_buffer(), + "bitsPerValue": md.get("bitsPerValue", 0), + } + self._attrs["_earthkit"] = attrs self._attrs.update(self.fixed_local_attrs) data = self.data_maker(self.tensor, self.var_dims, self.name) @@ -567,15 +568,19 @@ def parse(self, ds, profile=None, full=False): def grid(self, ds): grids = ds.index("md5GridSection") - if len(grids) != 1: - raise ValueError(f"Expected one grid, got {len(grids)}") - grid = grids[0] + if not grids: + grid = "_custom_" + str(id(ds)) + else: + # if len(grids) != 1: + # raise ValueError(f"Expected one grid, got {len(grids)}") + grid = grids[0] + key = (grid, self.profile.flatten_values) if key not in self.grids: from .grid import TensorGrid - self.grids = {key: TensorGrid(ds[0], self.profile.flatten_values)} + self.grids[key] = TensorGrid(ds[0], self.profile.flatten_values) return self.grids[key] diff --git a/src/earthkit/data/utils/xarray/grid.py b/src/earthkit/data/utils/xarray/grid.py index d8ac28cea..98adf30ef 100644 --- a/src/earthkit/data/utils/xarray/grid.py +++ b/src/earthkit/data/utils/xarray/grid.py @@ -15,19 +15,26 @@ LOG = logging.getLogger(__name__) +# TODO: refactor this when earthkit.geo grid support is implemented class Grid: def __init__(self, field): self.field = field @staticmethod def make(field): + # NOTE: underscore grid types are coming from UserMetadata grid_type = field.metadata("gridType", default=None) - if grid_type == "regular_ll": + + if grid_type in ["regular_ll", "_regular_ll"]: return RegularLLGrid(field) - elif grid_type in ["regular_gg", "mercator"]: + elif grid_type in ["regular_gg", "mercator", "_rectified_ll"]: return RectifiedLLGrid(field) elif grid_type in ["sh"]: return SpectralGrid(field) + elif grid_type is None or grid_type == "none": + return NonGrid(field) + elif grid_type == "_unstructured": + return Grid(field) else: return Grid(field) @@ -71,13 +78,21 @@ def to_distinct_latlon(self, field_shape): class SpectralGrid(Grid): - def to_latlon(self): + def to_latlon(self, field_shape=None): return None, None def is_spectral(self): return True +class NonGrid(Grid): + def to_latlon(self, field_shape=None): + return None, None + + def is_spectral(self): + return False + + class TensorGrid: def __init__(self, field, flatten_values=False): self.dims, self.coords, self.coords_dim = self.build(field, flatten_values) diff --git a/tests/xr_engine/test_xr_lod.py b/tests/xr_engine/test_xr_lod.py new file mode 100644 index 000000000..855ca7c77 --- /dev/null +++ b/tests/xr_engine/test_xr_lod.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import os +import sys + +import numpy as np +import pytest + +from earthkit.data import from_source + +here = os.path.dirname(__file__) +sys.path.insert(0, here) +from xr_engine_fixtures import compare_dims # noqa: E402 + + +@pytest.fixture +def xr_lod_latlon(): + prototype = { + "latitudes": [10.0, 0.0, -10.0], + "longitudes": [20, 40.0], + "values": [1, 2, 3, 4, 5, 6], + "valid_datetime": "2018-08-01T09:00:00Z", + } + + d = [ + {"param": "t", "level": 500, **prototype}, + {"param": "t", "level": 850, **prototype}, + {"param": "u", "level": 500, **prototype}, + {"param": "u", "level": 850, **prototype}, + ] + ds = from_source("list-of-dicts", d) + return ds + + +@pytest.fixture +def xr_lod_nongeo(): + prototype = { + "values": [1, 2, 3, 4, 5, 6], + "valid_datetime": "2018-08-01T09:00:00Z", + } + + d = [ + {"param": "t", "level": 500, **prototype}, + {"param": "t", "level": 850, **prototype}, + {"param": "u", "level": 500, **prototype}, + {"param": "u", "level": 850, **prototype}, + ] + ds = from_source("list-of-dicts", d) + return ds + + +@pytest.fixture +def xr_lod_forecast(): + prototype = { + "latitudes": [10.0, 0.0, -10.0], + "longitudes": [20, 40.0], + "values": [1, 2, 3, 4, 5, 6], + "base_datetime": "2018-08-01T09:00:00Z", + } + + d = [ + {"param": "t", "level": 500, "step": 0, **prototype}, + {"param": "t", "level": 500, "step": 6, **prototype}, + {"param": "u", "level": 500, "step": 0, **prototype}, + {"param": "u", "level": 500, "step": 6, **prototype}, + ] + ds = from_source("list-of-dicts", d) + return ds + + +def test_xr_engine_lod_latlon(xr_lod_latlon): + ds_in = xr_lod_latlon + ds = ds_in.to_xarray(time_dim_mode="raw") + + assert ds is not None + assert ds["t"].shape == (2, 3, 2) + assert ds["u"].shape == (2, 3, 2) + assert np.allclose(ds["latitude"].values, np.array([10.0, 0.0, -10.0])) + assert np.allclose(ds["longitude"].values, np.array([20.0, 40.0])) + + +def test_xr_engine_lod_nongeo(xr_lod_nongeo): + ds_in = xr_lod_nongeo + ds = ds_in.to_xarray(time_dim_mode="raw") + + assert ds is not None + assert ds["t"].shape == (2, 6) + assert ds["u"].shape == (2, 6) + + ref = np.array( + [ + [1, 2, 3, 4, 5, 6], + [1, 2, 3, 4, 5, 6], + ] + ) + assert np.allclose(ds["t"].values, ref) + assert np.allclose(ds["u"].values, ref) + + +def test_xr_engine_lod_forecast(xr_lod_forecast): + ds_in = xr_lod_forecast + ds = ds_in.to_xarray(time_dim_mode="forecast") + + assert ds is not None + assert ds["t"].shape == (2, 3, 2) + assert ds["u"].shape == (2, 3, 2) + + dims = {"step": [np.timedelta64(0, "h"), np.timedelta64(6, "h")]} + compare_dims(ds, dims, order_ref_var="t") + + assert np.allclose(ds["latitude"].values, np.array([10.0, 0.0, -10.0])) + assert np.allclose(ds["longitude"].values, np.array([20.0, 40.0])) From dc2a48f7e3757f46f89cf2adac518ff4d8227345 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Mon, 19 May 2025 09:50:56 +0100 Subject: [PATCH 04/17] Add release notes for 0.15 --- docs/examples/index.rst | 1 + docs/examples/list_of_dicts_to_xarray.ipynb | 968 ++++++++++++++++++++ docs/release_notes/index.rst | 1 + docs/release_notes/version_0.15_updates.rst | 26 + 4 files changed, 996 insertions(+) create mode 100644 docs/examples/list_of_dicts_to_xarray.ipynb create mode 100644 docs/release_notes/version_0.15_updates.rst diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 65614ad8c..f9f5af4a9 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -133,6 +133,7 @@ Dictionary input fields_from_dict_in_loop.ipynb list_of_dicts_overview list_of_dicts_geography + list_of_dicts_to_xarray Other inputs diff --git a/docs/examples/list_of_dicts_to_xarray.ipynb b/docs/examples/list_of_dicts_to_xarray.ipynb new file mode 100644 index 000000000..955d60420 --- /dev/null +++ b/docs/examples/list_of_dicts_to_xarray.ipynb @@ -0,0 +1,968 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "ee0f0104-8077-45f1-9746-58f29b64db92", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## list-of-dict: converting to Xarray" + ] + }, + { + "cell_type": "raw", + "id": "6cadbfbf-c7af-4927-8927-c320d9160c4f", + "metadata": { + "editable": true, + "raw_mimetype": "text/x-rst", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This example demonstrates how :ref:`data-sources-lod` fieldlists can be converted into Xarray." + ] + }, + { + "cell_type": "markdown", + "id": "2e087423-8c96-49b4-984c-f15472fa8381", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "#### Data containing geography" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1e5ebf7a-2fc6-453a-9e14-6b04b5135810", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 248B\n",
+       "Dimensions:    (levelist: 2, latitude: 3, longitude: 2)\n",
+       "Coordinates:\n",
+       "  * levelist   (levelist) int64 16B 500 850\n",
+       "  * latitude   (latitude) float64 24B 10.0 0.0 -10.0\n",
+       "  * longitude  (longitude) float64 16B 20.0 40.0\n",
+       "Data variables:\n",
+       "    t          (levelist, latitude, longitude) float64 96B ...\n",
+       "    u          (levelist, latitude, longitude) float64 96B ...\n",
+       "Attributes:\n",
+       "    Conventions:  CF-1.8\n",
+       "    institution:  ECMWF
" + ], + "text/plain": [ + " Size: 248B\n", + "Dimensions: (levelist: 2, latitude: 3, longitude: 2)\n", + "Coordinates:\n", + " * levelist (levelist) int64 16B 500 850\n", + " * latitude (latitude) float64 24B 10.0 0.0 -10.0\n", + " * longitude (longitude) float64 16B 20.0 40.0\n", + "Data variables:\n", + " t (levelist, latitude, longitude) float64 96B ...\n", + " u (levelist, latitude, longitude) float64 96B ...\n", + "Attributes:\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import earthkit.data as ekd\n", + "\n", + "prototype = {\n", + " \"latitudes\": [10.0, 0.0, -10.0],\n", + " \"longitudes\": [20, 40.0],\n", + " \"values\": [1, 2, 3, 4, 5, 6],\n", + " \"valid_datetime\": \"2018-08-01T09:00:00Z\",\n", + " }\n", + "\n", + "d = [\n", + " {\"param\": \"t\", \"level\": 500, **prototype},\n", + " {\"param\": \"t\", \"level\": 850, **prototype},\n", + " {\"param\": \"u\", \"level\": 500, **prototype},\n", + " {\"param\": \"u\", \"level\": 850, **prototype},\n", + " ]\n", + "\n", + "ds = ekd.from_source(\"list-of-dicts\", d)\n", + "ds.to_xarray()" + ] + }, + { + "cell_type": "markdown", + "id": "94b46ec8-614b-480a-8ffe-0b1dd4e344bb", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "#### Data without geography" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "7ea3d8bf-a432-4aef-94d9-5ac0c6b19503", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 208B\n",
+       "Dimensions:   (levelist: 2, values: 6)\n",
+       "Coordinates:\n",
+       "  * levelist  (levelist) int64 16B 500 850\n",
+       "Dimensions without coordinates: values\n",
+       "Data variables:\n",
+       "    t         (levelist, values) float64 96B ...\n",
+       "    u         (levelist, values) float64 96B ...\n",
+       "Attributes:\n",
+       "    Conventions:  CF-1.8\n",
+       "    institution:  ECMWF
" + ], + "text/plain": [ + " Size: 208B\n", + "Dimensions: (levelist: 2, values: 6)\n", + "Coordinates:\n", + " * levelist (levelist) int64 16B 500 850\n", + "Dimensions without coordinates: values\n", + "Data variables:\n", + " t (levelist, values) float64 96B ...\n", + " u (levelist, values) float64 96B ...\n", + "Attributes:\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prototype = {\n", + " \"values\": [1, 2, 3, 4, 5, 6],\n", + " \"valid_datetime\": \"2018-08-01T09:00:00Z\",\n", + " }\n", + "\n", + "d = [\n", + " {\"param\": \"t\", \"level\": 500, **prototype},\n", + " {\"param\": \"t\", \"level\": 850, **prototype},\n", + " {\"param\": \"u\", \"level\": 500, **prototype},\n", + " {\"param\": \"u\", \"level\": 850, **prototype},\n", + " ]\n", + "\n", + "ds = ekd.from_source(\"list-of-dicts\", d)\n", + "ds.to_xarray()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6463409-7686-4d90-8cab-00a04b7119bb", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst index 773423b42..f18ea27b4 100644 --- a/docs/release_notes/index.rst +++ b/docs/release_notes/index.rst @@ -15,6 +15,7 @@ Version updates .. toctree:: :maxdepth: 1 + version_0.15_updates version_0.14_updates version_0.13_updates version_0.12_updates diff --git a/docs/release_notes/version_0.15_updates.rst b/docs/release_notes/version_0.15_updates.rst new file mode 100644 index 000000000..bc8a5d815 --- /dev/null +++ b/docs/release_notes/version_0.15_updates.rst @@ -0,0 +1,26 @@ +Version 0.15 Updates +///////////////////////// + + +Version 0.15.0 +=============== + +Xarray engine +++++++++++++++++++++++++++++++ + +- Improved the serialisation of GRIB fieldlists to reduce memory usage when Xarray is generated with chunks (:pr:`700`). See the :ref:`/examples/xarray_engine_chunks.ipynb` notebook example. +- TensorBackendArray, which implements the lazy loading of DataArrays in the Xarray engine, now uses a ``dask.utils.SerializableLock`` when accessing the data (:pr:`700`). +- Enabled converting :ref:`data-sources-lod` fieldlists into Xarray (:pr:`701`). See the :ref:`/examples/list_of_dicts_to_xarray.ipynb` notebook example. + + +New features ++++++++++++++++++ + +- Added new config option ``grib-file-serialisation-policy`` to control how GRIB data on disk is pickled. The options are "path" and "memory". The default is "path". Previously, only "memory" was implemented (:pr:`700`). +- Added serialisation to GRIB fields (both on disk and in-memory) (:pr:`700`) + + +Fixes ++++++++++++++++++ + +- Fixed issue when the :ref:`data-sources-forcings` source did not handle time-zone aware datetimes correctly (:pr:`693`). From a94bccfad633bd78f6b313852fa08a8eb664587f Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Wed, 21 May 2025 13:57:17 +0100 Subject: [PATCH 05/17] Remove bitmapPresent key from GRIB namespace tests --- tests/grib/test_grib_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/grib/test_grib_summary.py b/tests/grib/test_grib_summary.py index e0c187043..c52e92b2b 100644 --- a/tests/grib/test_grib_summary.py +++ b/tests/grib/test_grib_summary.py @@ -475,7 +475,7 @@ def test_grib_dump(fl_type): "data": { # "Ni": 12, # "Nj": 7, - "bitmapPresent": 0, + # "bitmapPresent": 0, "latitudeOfFirstGridPointInDegrees": 90.0, "longitudeOfFirstGridPointInDegrees": 0.0, "latitudeOfLastGridPointInDegrees": -90.0, @@ -547,6 +547,7 @@ def test_grib_dump(fl_type): if ns == "geography": d["data"].pop("Ni", None) d["data"].pop("Nj", None) + d["data"].pop("bitmapPresent", None) if ns not in ("default", "statistics"): assert d == [x for x in ref if x["title"] == ns][0], ns From 75f086e2ac7f5f9ccd666bfb6a2caacaccf982c5 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Thu, 29 May 2025 11:13:20 +0100 Subject: [PATCH 06/17] Handle hdate in UserMetadata (#715) * Handle hdate in UserMetadata --- src/earthkit/data/utils/metadata/dict.py | 4 + .../test_array_field_usermetadata.py | 57 ---- tests/utils/test_usermetadata.py | 251 ++++++++++++++++++ 3 files changed, 255 insertions(+), 57 deletions(-) create mode 100644 tests/utils/test_usermetadata.py diff --git a/src/earthkit/data/utils/metadata/dict.py b/src/earthkit/data/utils/metadata/dict.py index a598d8c19..220f32270 100644 --- a/src/earthkit/data/utils/metadata/dict.py +++ b/src/earthkit/data/utils/metadata/dict.py @@ -416,6 +416,10 @@ def base_datetime(self): v = to_datetime(v) return v + v = self._datetime("hdate", "time") + if v is not None: + return v + v = self._datetime("date", "time") if v is not None: return v diff --git a/tests/array_fieldlist/test_array_field_usermetadata.py b/tests/array_fieldlist/test_array_field_usermetadata.py index ff1774e40..2ac6146f8 100644 --- a/tests/array_fieldlist/test_array_field_usermetadata.py +++ b/tests/array_fieldlist/test_array_field_usermetadata.py @@ -9,7 +9,6 @@ # nor does it submit to any jurisdiction. # -import copy import datetime import numpy as np @@ -74,59 +73,3 @@ def test_array_field_usermetadata_geom(_kwargs): assert np.allclose(lat, meta["latitudes"]) assert np.allclose(lon, meta["longitudes"]) assert np.allclose(f.values, vals) - - -@pytest.mark.parametrize( - "initial, update, expected", - [ - ({"shortName": "2t"}, {}, {"shortName": "2t"}), # No update - ({"shortName": "2t"}, {"shortName": "msl"}, {"shortName": "msl"}), # Update existing key - ( - {"shortName": "2t"}, - {"longName": "Temperature"}, - {"shortName": "2t", "longName": "Temperature"}, - ), # Add new key - ({}, {"shortName": "2t"}, {"shortName": "2t"}), # Add key to empty metadata - ( - {"shortName": "2t", "longName": "Temperature"}, - {"shortName": "temperature"}, - {"shortName": "temperature", "longName": "Temperature"}, - ), # Update one key, keep others - ], -) -def test_array_field_usermetadata_override(initial, update, expected): - - initial_copied = copy.deepcopy(initial) - update_copied = copy.deepcopy(update) - - meta = UserMetadata(initial) - new_meta = meta.override(update) - _ = meta.override(**update) # Check that the override method works with keyword arguments - - # Check that the updated metadata matches the expected result - for k, v in expected.items(): - assert new_meta[k] == v - - # Ensure the original metadata remains unchanged - for k, v in initial_copied.items(): - assert meta[k] == v - - # Check that the updated metadata contains all expected keys - for k in update_copied: - if k in initial: - assert meta[k] == initial[k] - else: - assert k not in meta - - -def test_array_field_usermetadata_override_shape(): - meta = UserMetadata({}, shape=(10, 1)) - new_meta = meta.override( - { - "shortName": "2t", - "longName": "Temperature", - } - ) - new_meta._shape = None - assert new_meta._shape is None - assert meta._shape is not None diff --git a/tests/utils/test_usermetadata.py b/tests/utils/test_usermetadata.py new file mode 100644 index 000000000..ccb6f4c7c --- /dev/null +++ b/tests/utils/test_usermetadata.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 + +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import copy +import datetime + +import numpy as np +import pytest + +from earthkit.data.utils.metadata.dict import UserMetadata + + +def test_usermetadata_nogeom(): + meta = UserMetadata( + { + "shortName": "test", + "longName": "Test", + "date": 20180801, + "time": 300, + } + ) + + assert meta["shortName"] == "test" + assert meta["longName"] == "Test" + assert meta["date"] == 20180801 + assert meta["time"] == 300 + assert meta["base_datetime"] == "2018-08-01T03:00:00" + assert meta.base_datetime() == datetime.datetime(2018, 8, 1, 3, 0) + assert meta.geography.latitudes() is None + assert meta.geography.longitudes() is None + + +@pytest.mark.parametrize("_kwargs", [{}, {"shape": None}, {"shape": (10,)}]) +def test_usermetadata_geom(_kwargs): + meta = UserMetadata( + { + "shortName": "test", + "longName": "Test", + "date": 20180801, + "time": 300, + "latitudes": np.linspace(-10.0, 10.0, 10), + "longitudes": np.linspace(20.0, 40.0, 10), + }, + **_kwargs, + ) + + assert meta["shortName"] == "test" + assert meta["longName"] == "Test" + assert meta["date"] == 20180801 + assert meta["time"] == 300 + assert meta["base_datetime"] == "2018-08-01T03:00:00" + assert meta.base_datetime() == datetime.datetime(2018, 8, 1, 3, 0) + assert meta.geography.shape() == (10,) + + lat = meta.geography.latitudes() + lon = meta.geography.longitudes() + assert lat.shape == (10,) + assert lon.shape == (10,) + assert np.allclose(lat, meta["latitudes"]) + assert np.allclose(lon, meta["longitudes"]) + + +@pytest.mark.parametrize( + "initial, update, expected", + [ + ({"shortName": "2t"}, {}, {"shortName": "2t"}), # No update + ({"shortName": "2t"}, {"shortName": "msl"}, {"shortName": "msl"}), # Update existing key + ( + {"shortName": "2t"}, + {"longName": "Temperature"}, + {"shortName": "2t", "longName": "Temperature"}, + ), # Add new key + ({}, {"shortName": "2t"}, {"shortName": "2t"}), # Add key to empty metadata + ( + {"shortName": "2t", "longName": "Temperature"}, + {"shortName": "temperature"}, + {"shortName": "temperature", "longName": "Temperature"}, + ), # Update one key, keep others + ], +) +def test_usermetadata_override(initial, update, expected): + + initial_copied = copy.deepcopy(initial) + update_copied = copy.deepcopy(update) + + meta = UserMetadata(initial) + new_meta = meta.override(update) + _ = meta.override(**update) # Check that the override method works with keyword arguments + + # Check that the updated metadata matches the expected result + for k, v in expected.items(): + assert new_meta[k] == v + + # Ensure the original metadata remains unchanged + for k, v in initial_copied.items(): + assert meta[k] == v + + # Check that the updated metadata contains all expected keys + for k in update_copied: + if k in initial: + assert meta[k] == initial[k] + else: + assert k not in meta + + +def test_usermetadata_override_shape(): + meta = UserMetadata({}, shape=(10, 1)) + new_meta = meta.override( + { + "shortName": "2t", + "longName": "Temperature", + } + ) + new_meta._shape = None + assert new_meta._shape is None + assert meta._shape is not None + + +@pytest.mark.parametrize("data,ref_base", [({"base_datetime": "2018-08-01T09:00:00"}, "2018-08-01T09:00:00")]) +def test_usermetadata_base_date_only(data, ref_base): + meta = UserMetadata(data) + + ref_base_dt = datetime.datetime.fromisoformat(ref_base) + assert meta["base_datetime"] == ref_base + assert meta.base_datetime() == ref_base_dt + assert meta["valid_datetime"] == ref_base + assert meta.valid_datetime() == ref_base_dt + assert meta.datetime() == {"base_time": ref_base_dt, "valid_time": ref_base_dt} + + +@pytest.mark.parametrize( + "data,ref_base,ref_valid", [({"valid_datetime": "2018-08-01T09:00:00"}, None, "2018-08-01T09:00:00")] +) +def test_usermetadata_valid_date_only(data, ref_base, ref_valid): + meta = UserMetadata(data) + + ref_valid_dt = datetime.datetime.fromisoformat(ref_valid) + assert meta["valid_datetime"] == ref_valid + assert meta.valid_datetime() == ref_valid_dt + assert meta.datetime() == {"base_time": ref_base, "valid_time": ref_valid_dt} + + +@pytest.mark.parametrize( + "data,ref_base,ref_valid,ref_step", + [ + ( + { + "base_datetime": "2018-08-01T03:00:00", + "step": 6, + }, + "2018-08-01T03:00:00", + "2018-08-01T09:00:00", + 6, + ), + ( + { + "valid_datetime": "2018-08-01T09:00:00", + "step": 6, + }, + "2018-08-01T03:00:00", + "2018-08-01T09:00:00", + 6, + ), + ( + { + "forecast_reference_time": "2018-08-01T03:00:00", + "step": 6, + }, + "2018-08-01T03:00:00", + "2018-08-01T09:00:00", + 6, + ), + ( + { + "forecast_reference_time": "2018-08-01T03:00:00", + "step": datetime.timedelta(hours=6), + }, + "2018-08-01T03:00:00", + "2018-08-01T09:00:00", + datetime.timedelta(hours=6), + ), + ( + { + "date": 20180801, + "time": 300, + "step": datetime.timedelta(hours=6), + }, + "2018-08-01T03:00:00", + "2018-08-01T09:00:00", + datetime.timedelta(hours=6), + ), + ], +) +def test_usermetadata_forecast(data, ref_base, ref_valid, ref_step): + meta = UserMetadata(data) + + ref_base_dt = datetime.datetime.fromisoformat(ref_base) + ref_valid_dt = datetime.datetime.fromisoformat(ref_valid) + + if isinstance(ref_step, int): + ref_step_td = datetime.timedelta(hours=ref_step) + else: + ref_step_td = ref_step + + assert meta["valid_datetime"] == ref_valid + assert meta.valid_datetime() == ref_valid_dt + assert meta["base_datetime"] == ref_base + assert meta.base_datetime() == ref_base_dt + assert meta.datetime() == {"base_time": ref_base_dt, "valid_time": ref_valid_dt} + assert meta["step"] == ref_step + assert meta["step_timedelta"] == ref_step_td + assert meta.step_timedelta() == ref_step_td + assert meta["forecast_reference_time"] == ref_base + + +def test_usermetadata_hdate_from_mars(): + meta = UserMetadata( + { + "class": "od", + "expver": "0001", + "stream": "enfh", + "type": "cf", + "levtype": "sfc", + "param": "167.128", + "date": "20180830", # Model version + "hdate": "20100830", # Start date of the forecasts + "time": "0000", # Forecast starts at 0am + "step": 12, # Forecast 12 hours ahead + } + ) + + ref_base_dt = datetime.datetime(2010, 8, 30, 0, 0) + ref_valid_dt = datetime.datetime(2010, 8, 30, 12, 0) + + assert meta.base_datetime() == ref_base_dt + assert meta.valid_datetime() == ref_valid_dt + assert meta.datetime() == {"base_time": ref_base_dt, "valid_time": ref_valid_dt} + assert meta["step"] == 12 + assert meta["step_timedelta"] == datetime.timedelta(hours=12) + assert meta["date"] == "20180830" + assert meta["hdate"] == "20100830" + assert meta["time"] == "0000" + assert meta["forecast_reference_time"] == "2010-08-30T00:00:00" From 1eae98b7f9b40d2b5e23cd60ffed74e126d7abb6 Mon Sep 17 00:00:00 2001 From: Oisin-M <60450429+Oisin-M@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:13:11 +0200 Subject: [PATCH 07/17] Feature/zarr (#675) * Add minimal zarr source and reader * Access via XArrayFieldList instead of XArrayDatasetWrapper * update pyproject and envs * make zarr optional * skip zarr test if zarr not installed --------- Co-authored-by: Christopher Polster --- pyproject.toml | 1 + src/earthkit/data/readers/__init__.py | 27 ++- src/earthkit/data/readers/directory.py | 20 ++- src/earthkit/data/readers/numpy.py | 14 +- src/earthkit/data/readers/tar.py | 2 +- src/earthkit/data/readers/text.py | 3 - src/earthkit/data/readers/zarr.py | 45 +++++ src/earthkit/data/sources/xarray_zarr.py | 29 +++ tests/data/test_zarr/.zattrs | 4 + tests/data/test_zarr/.zgroup | 3 + tests/data/test_zarr/.zmetadata | 213 +++++++++++++++++++++++ tests/data/test_zarr/latitude/.zarray | 22 +++ tests/data/test_zarr/latitude/.zattrs | 7 + tests/data/test_zarr/latitude/0 | Bin 0 -> 282 bytes tests/data/test_zarr/level/.zarray | 22 +++ tests/data/test_zarr/level/.zattrs | 7 + tests/data/test_zarr/level/0 | Bin 0 -> 24 bytes tests/data/test_zarr/longitude/.zarray | 22 +++ tests/data/test_zarr/longitude/.zattrs | 7 + tests/data/test_zarr/longitude/0 | Bin 0 -> 457 bytes tests/data/test_zarr/t/.zarray | 28 +++ tests/data/test_zarr/t/.zattrs | 14 ++ tests/data/test_zarr/t/0.0.0.0 | Bin 0 -> 111851 bytes tests/data/test_zarr/t/0.1.0.0 | Bin 0 -> 118131 bytes tests/data/test_zarr/time/.zarray | 22 +++ tests/data/test_zarr/time/.zattrs | 8 + tests/data/test_zarr/time/0 | Bin 0 -> 20 bytes tests/data/test_zarr/z/.zarray | 28 +++ tests/data/test_zarr/z/.zattrs | 14 ++ tests/data/test_zarr/z/0.0.0.0 | Bin 0 -> 90953 bytes tests/data/test_zarr/z/0.1.0.0 | Bin 0 -> 88518 bytes tests/sources/test_zarr.py | 22 +++ 32 files changed, 555 insertions(+), 29 deletions(-) create mode 100644 src/earthkit/data/readers/zarr.py create mode 100644 src/earthkit/data/sources/xarray_zarr.py create mode 100644 tests/data/test_zarr/.zattrs create mode 100644 tests/data/test_zarr/.zgroup create mode 100644 tests/data/test_zarr/.zmetadata create mode 100644 tests/data/test_zarr/latitude/.zarray create mode 100644 tests/data/test_zarr/latitude/.zattrs create mode 100644 tests/data/test_zarr/latitude/0 create mode 100644 tests/data/test_zarr/level/.zarray create mode 100644 tests/data/test_zarr/level/.zattrs create mode 100644 tests/data/test_zarr/level/0 create mode 100644 tests/data/test_zarr/longitude/.zarray create mode 100644 tests/data/test_zarr/longitude/.zattrs create mode 100644 tests/data/test_zarr/longitude/0 create mode 100644 tests/data/test_zarr/t/.zarray create mode 100644 tests/data/test_zarr/t/.zattrs create mode 100644 tests/data/test_zarr/t/0.0.0.0 create mode 100644 tests/data/test_zarr/t/0.1.0.0 create mode 100644 tests/data/test_zarr/time/.zarray create mode 100644 tests/data/test_zarr/time/.zattrs create mode 100644 tests/data/test_zarr/time/0 create mode 100644 tests/data/test_zarr/z/.zarray create mode 100644 tests/data/test_zarr/z/.zattrs create mode 100644 tests/data/test_zarr/z/0.0.0.0 create mode 100644 tests/data/test_zarr/z/0.1.0.0 create mode 100644 tests/sources/test_zarr.py diff --git a/pyproject.toml b/pyproject.toml index 803d32709..ae71d4b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ optional-dependencies.test = [ "pytest-timeout", ] optional-dependencies.wekeo = [ "hda>=2.22" ] +optional-dependencies.zarr = [ "zarr>=3" ] urls.Documentation = "https://earthkit-data.readthedocs.io/" urls.Homepage = "https://github.com/ecmwf/earthkit-data/" urls.Issues = "https://github.com/ecmwf/earthkit-data.issues" diff --git a/src/earthkit/data/readers/__init__.py b/src/earthkit/data/readers/__init__.py index 9faf10a99..35ba5b38f 100644 --- a/src/earthkit/data/readers/__init__.py +++ b/src/earthkit/data/readers/__init__.py @@ -204,27 +204,26 @@ def reader(source, path, **kwargs): raise TypeError("Provided reader must be a callable or a string, not %s" % type(reader)) - if os.path.isdir(path): - from .directory import DirectoryReader - - return DirectoryReader(source, path).mutate() - LOG.debug("Reader for %s", path) - if not os.path.exists(path): r = _non_existing(source, path, **kwargs) if r is not None: return r raise FileNotFoundError(f"No such file exists: '{path}'") - if os.path.getsize(path) == 0: - r = _empty(source, path, **kwargs) - if r is not None: - return r - raise Exception(f"File is empty: '{path}'") + LOG.debug("Reader for %s", path) - n_bytes = CONFIG.get("reader-type-check-bytes") - with open(path, "rb") as f: - magic = f.read(n_bytes) + if os.path.isdir(path): + magic = None + else: + if os.path.getsize(path) == 0: + r = _empty(source, path, **kwargs) + if r is not None: + return r + raise Exception(f"File is empty: '{path}'") + + n_bytes = CONFIG.get("reader-type-check-bytes") + with open(path, "rb") as f: + magic = f.read(n_bytes) LOG.debug("Looking for a reader for %s (%s)", path, magic) diff --git a/src/earthkit/data/readers/directory.py b/src/earthkit/data/readers/directory.py index 1a280bc08..a15af57be 100644 --- a/src/earthkit/data/readers/directory.py +++ b/src/earthkit/data/readers/directory.py @@ -61,10 +61,15 @@ def mutate(self): return self def mutate_source(self): - if os.path.exists(os.path.join(self.path, ".zattrs")): + if ( + os.path.exists(os.path.join(self.path, ".zarray")) + or os.path.exists(os.path.join(self.path, ".zgroup")) + or os.path.exists(os.path.join(self.path, ".zmetadata")) + or os.path.exists(os.path.join(self.path, ".zattrs")) + ): if self.stream: raise ValueError("Cannot stream zarr directories") - return from_source("zarr", self.path) + return from_source("xarray-zarr", self.path) if len(self._content) == 1: return from_source( @@ -103,5 +108,14 @@ def write(self, f, **kwargs): def reader(source, path, *, magic=None, deeper_check=False, **kwargs): - if magic is None and os.path.isdir(path): + if ( + magic is None + and os.path.isdir(path) + and not ( + os.path.exists(os.path.join(path, ".zarray")) + or os.path.exists(os.path.join(path, ".zgroup")) + or os.path.exists(os.path.join(path, ".zmetadata")) + or os.path.exists(os.path.join(path, ".zattrs")) + ) + ): return DirectoryReader(source, path) diff --git a/src/earthkit/data/readers/numpy.py b/src/earthkit/data/readers/numpy.py index 3cccc339b..326dbc1a6 100644 --- a/src/earthkit/data/readers/numpy.py +++ b/src/earthkit/data/readers/numpy.py @@ -34,12 +34,10 @@ def to_numpy(self, numpy_load_kwargs={}): def reader(source, path, *, magic=None, deeper_check=False, **kwargs): - if magic is None: # Bypass check and force - return NumpyReader(source, path) + if magic is not None: + if magic[:6] == b"\x93NUMPY": + return NumpyReader(source, path) - if magic[:6] == b"\x93NUMPY": - return NumpyReader(source, path) - - _, extension = os.path.splitext(path) - if magic[:4] == b"PK\x03\x04" and extension == ".npz": - return NumpyZipReader(source, path) + _, extension = os.path.splitext(path) + if magic[:4] == b"PK\x03\x04" and extension == ".npz": + return NumpyZipReader(source, path) diff --git a/src/earthkit/data/readers/tar.py b/src/earthkit/data/readers/tar.py index ce325e4e9..b4b95480f 100644 --- a/src/earthkit/data/readers/tar.py +++ b/src/earthkit/data/readers/tar.py @@ -34,5 +34,5 @@ def reader(source, path, *, magic=None, deeper_check=False, **kwargs): kind, compression = mimetypes.guess_type(path) - if magic is None or kind == "application/x-tar": + if kind == "application/x-tar": return TarReader(source, path, compression) diff --git a/src/earthkit/data/readers/text.py b/src/earthkit/data/readers/text.py index 7ea687c03..512d8a2ea 100644 --- a/src/earthkit/data/readers/text.py +++ b/src/earthkit/data/readers/text.py @@ -44,9 +44,6 @@ def mutate(self): def reader(source, path, *, magic=None, deeper_check=False, **kwargs): - if magic is None: # Bypass check and force - return TextReader(source, path) - if deeper_check: if is_text(path): return TextReader(source, path) diff --git a/src/earthkit/data/readers/zarr.py b/src/earthkit/data/readers/zarr.py new file mode 100644 index 000000000..97cb56183 --- /dev/null +++ b/src/earthkit/data/readers/zarr.py @@ -0,0 +1,45 @@ +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import os + +from . import Reader +from .netcdf.fieldlist import XArrayFieldList + + +class ZarrReader(XArrayFieldList, Reader): + + def __init__(self, source, path, **kwargs): + Reader.__init__(self, source, path, **kwargs) + XArrayFieldList.__init__(self, self._open_zarr(**kwargs)) + + def mutate_source(self): + return self + + def to_xarray(self, **kwargs): + return self._open_zarr(**kwargs) + + def _open_zarr(self, **kwargs): + import xarray as xr + + options = kwargs.get("xarray_open_zarr_kwargs", kwargs) + return xr.open_zarr(self.path, **options) + + def __repr__(self): + return f"ZarrReader({self.path})" + + +def reader(source, path, *, magic=None, deeper_check=False, **kwargs): + if ( + os.path.exists(os.path.join(path, ".zarray")) + or os.path.exists(os.path.join(path, ".zgroup")) + or os.path.exists(os.path.join(path, ".zmetadata")) + or os.path.exists(os.path.join(path, ".zattrs")) + ): + return ZarrReader(source, path) diff --git a/src/earthkit/data/sources/xarray_zarr.py b/src/earthkit/data/sources/xarray_zarr.py new file mode 100644 index 000000000..4b12c421d --- /dev/null +++ b/src/earthkit/data/sources/xarray_zarr.py @@ -0,0 +1,29 @@ +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +from ..readers.zarr import ZarrReader +from . import Source + + +class ZarrSource(Source): + + def __init__(self, path, **kwargs): + super().__init__(**kwargs) + self._reader = ZarrReader(self, path, **kwargs) + + def mutate(self): + source = self._reader.mutate_source() + if source not in (None, self): + source._parent = self + return source + + return self + + +source = ZarrSource diff --git a/tests/data/test_zarr/.zattrs b/tests/data/test_zarr/.zattrs new file mode 100644 index 000000000..63e460446 --- /dev/null +++ b/tests/data/test_zarr/.zattrs @@ -0,0 +1,4 @@ +{ + "Conventions": "CF-1.6", + "history": "2024-04-24 15:40:12 GMT by grib_to_netcdf-2.36.0: /Users/cgr/install/eccodes/release/bin/grib_to_netcdf -o test4.nc test4.grib" +} diff --git a/tests/data/test_zarr/.zgroup b/tests/data/test_zarr/.zgroup new file mode 100644 index 000000000..ea023896e --- /dev/null +++ b/tests/data/test_zarr/.zgroup @@ -0,0 +1,3 @@ +{ + "zarr_format": 2 +} diff --git a/tests/data/test_zarr/.zmetadata b/tests/data/test_zarr/.zmetadata new file mode 100644 index 000000000..2366f8b4d --- /dev/null +++ b/tests/data/test_zarr/.zmetadata @@ -0,0 +1,213 @@ +{ + "metadata": { + ".zgroup": { + "zarr_format": 2 + }, + ".zattrs": { + "Conventions": "CF-1.6", + "history": "2024-04-24 15:40:12 GMT by grib_to_netcdf-2.36.0: /Users/cgr/install/eccodes/release/bin/grib_to_netcdf -o test4.nc test4.grib" + }, + "z/.zattrs": { + "units": "m**2 s**-2", + "long_name": "Geopotential", + "standard_name": "geopotential", + "add_offset": 33425.29356267787, + "scale_factor": 0.7605308942631956, + "missing_value": -32767, + "_ARRAY_DIMENSIONS": [ + "time", + "level", + "latitude", + "longitude" + ] + }, + "z/.zarray": { + "shape": [ + 1, + 2, + 181, + 360 + ], + "chunks": [ + 1, + 1, + 181, + 360 + ], + "fill_value": -32767, + "order": "C", + "filters": null, + "dimension_separator": ".", + "compressor": { + "id": "blosc", + "cname": "lz4", + "clevel": 5, + "shuffle": 1, + "blocksize": 0, + "typesize": null + }, + "zarr_format": 2, + "dtype": "7Em^c+-ke!8 zrcIeNp|7W_qphW>p{AmwASWXwAtoXuz{kVI!N$VGKu1GGK}JGEfQN&H;m3y;4{lsI zabU-W6$@re=x8WN2yie^5MX$4U_pZeLjw>xG%PsqfI&dPz#$-^prK>Jj0Gz;>^N}Z z!i@(nKKx){;ouPvk&sbP(agk&>Y09)2 zv*yfOuxQD$6|2^)+puYiHqhbnPK*rd@{WuQEP@V<4Dan3_8$O)gCM{Plt0ACpcHTj F1OP|IXQKcB literal 0 HcmV?d00001 diff --git a/tests/data/test_zarr/level/.zarray b/tests/data/test_zarr/level/.zarray new file mode 100644 index 000000000..fef3b0512 --- /dev/null +++ b/tests/data/test_zarr/level/.zarray @@ -0,0 +1,22 @@ +{ + "shape": [ + 2 + ], + "chunks": [ + 2 + ], + "fill_value": null, + "order": "C", + "filters": null, + "dimension_separator": ".", + "compressor": { + "id": "blosc", + "cname": "lz4", + "clevel": 5, + "shuffle": 1, + "blocksize": 0, + "typesize": null + }, + "zarr_format": 2, + "dtype": "L$tcz{@*fsx^VKmd@%z#stSzm;!bV(<%SU;x4lpjsg9 z(6HdZ0|o&F1BZZwf`*O>GZw7au;ajq3pXCT`0#^)g@Z>xL_$VEMMKBH#KOkG#lt5c zBqAmuB_pSxq@t#wsim!>tEX?mq$$&8%$hTA!J;M0R;*gHZo{T6+ji{Qv+uy6Bgamh zI&<#Ar7PEN+`4n`!J{Y7Uc7qq?!%`q-+ui1^N)d%iJ66!jh%y&i<^g+k6%DgNLWNv zOk6@zN?Jx%PF_J#Nm)fzORl&MpvO`AS_ z#*CRWXU&>Dd(ND>bLY*QKYzi3g$oxgTD*A4lBG+REnB{P#fp_HSFKvTdd-@(YuBw? qzkb7pjT<*@+Pqm27}WO}>>a?s5d?sdCGW(@;3MzM$Z$Tu83X`nFt&vN literal 0 HcmV?d00001 diff --git a/tests/data/test_zarr/t/.zarray b/tests/data/test_zarr/t/.zarray new file mode 100644 index 000000000..5db71a9d5 --- /dev/null +++ b/tests/data/test_zarr/t/.zarray @@ -0,0 +1,28 @@ +{ + "shape": [ + 1, + 2, + 181, + 360 + ], + "chunks": [ + 1, + 1, + 181, + 360 + ], + "fill_value": -32767, + "order": "C", + "filters": null, + "dimension_separator": ".", + "compressor": { + "id": "blosc", + "cname": "lz4", + "clevel": 5, + "shuffle": 1, + "blocksize": 0, + "typesize": null + }, + "zarr_format": 2, + "dtype": "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%0A+8PD<=kNO>mn7eS`&LK6sOgi6LGI9V?DY8%rsVS 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 N@0@;&-m3ZY{~xGkZY2N! literal 0 HcmV?d00001 diff --git a/tests/data/test_zarr/t/0.1.0.0 b/tests/data/test_zarr/t/0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..365023eaaee8ae2f394a3bef3c30115faff0d112 GIT binary patch literal 118131 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 z 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 GN&G*03of(( literal 0 HcmV?d00001 diff --git a/tests/data/test_zarr/time/.zarray b/tests/data/test_zarr/time/.zarray new file mode 100644 index 000000000..93c33d49f --- /dev/null +++ b/tests/data/test_zarr/time/.zarray @@ -0,0 +1,22 @@ +{ + "shape": [ + 1 + ], + "chunks": [ + 1 + ], + "fill_value": null, + "order": "C", + "filters": null, + "dimension_separator": ".", + "compressor": { + "id": "blosc", + "cname": "lz4", + "clevel": 5, + "shuffle": 1, + "blocksize": 0, + "typesize": null + }, + "zarr_format": 2, + "dtype": "lz&&L1&3DNYN+r}iB*tRus^7`}ry{h*|yj|7RRbBVoTX&tk z*V=oX1_34pLHYs)`tKuU0t|!(1Oznp2?RtK3=H(6cK-ti%az6X*{Si7!M^T})~5QJ zs`8Se{G816l*IVhsPNFB0AFuUH&eX-RR>UqS+WJX{=X zEKH2_wA7U3WF$lc__)|u80e@dNQm%oFwl??;GiJDK;K`VAMbB3|DK;7A0O`TZf|d{ zudl8yFE20t-?+TIy1Kr;xxKr8czpi%^7j7u4Fn7d4haK?fQ*Wcg@aE-LQX}?z{JYI z%_k@#E-9;^tfr}BU~Fb-WAF6a!`m+~Bs@AUF(o}Kx1hMJvZlVNwX>&haAbV?&%)Bm z`qu8j@!93g{nN|)7Z4~o6f6P~Dh4(#0TC%V6%9Qj3mXR)55J(Wh?s<=w5*(hlCr9r zx~7)4j;@}*zJZ~kk&%({e*yS5G&IoH)78<|(o|Rb4+l9JDG4zV01n&$7#QiODM*P3 zaIw%)5MZIeL4H18pC4|o&QA_@x7NR29FTq$>i$7-95d2{D1&<1s(|n4FeMk8wVEu{;TIB>ke*jk-PAcS{%3jX@ca${IwU*_CN3c5%DJBB=;Xms6|6Rb^ zTAJuG9#-&eq1-%JS0RzYFv8 z^K*0a^9u`q7nhb-R@XPSws-drk510cFRyR!9-aVn`2nCy5HJWR7l(bE3T)aV{19DART3J)y#N5ip#>UFrL|;oqMudl%0v8eV-`U1YZ*6X@ zkBznz7bOPx)6vpsTX|-Tzl)`wij)v1BPAg%2{sbs&-3-+#==B@TTMxJQe>cqqlJOG zycj% zw)T$BZysMhfxw~RkpaNs5fYP-kyB7oQBhOV(9qD*($dkYc^8OtV3OSFWWnfPG(jyA1nn%vS2^_0VaP`s`g`#KSCu zMd!ro(d`>B6aoqwIvO%OB+x71QBys&xiQ|BYQpqbAXh8>6^ULZa-1aa@24xHt>u}q z0l#gGG~~tjSZz#nRAhefuuv0WB0_vUUGA^_8R@Do$&3wjv(i%(<)kG*0e`#NTbk@` zs>n-;3Gw;uXl-Vsr=_lv z5NIY&g9~~#U6N|Y4?mGAc;*iugA#Hnn7T!zm(+I-j!)0bOpf%nROZG9IOs_OP<`2( zs897W_(cJI`KKw}$3%vm7~$(?cX7P8xvDTTF)GN*Wp-+8sIRN7sjjNDFeg1JCd}X6 z-b_b9l#>P@;pc8|zP~mn(#=FsfEFA2?P6zfYN)3Jpyds9fW#=y%Sen0_I9x`)dQ%O zBmi@MK3-ma0bx-|IY5B{iqpeCG%6uAE3c@uvbwgup`pIEy1Y0)D>WfHJS5QHKOiVH zGA=czw60@#ZtMI79F>GwSjo^aAU?mYXKLl(`rkJgKzZR|AwfPK&bI#a)nxwa8 z0$nT()MQ0^TIKKOfF@m&Uv6i_s!IulE)NnTk_FoljOqSxeKvUd`4gx#@}0 zV}U;%ugwnkv^CY&H?(vO|Jl9)#pO_Q&l$PJmkVgQA~VR@z|qOur?72$6OLNMGpll9rkj6YOE5Bh5h!`?xVs7G?+0Md1Ltcc7ju092 z@nF8EBFW2Gk{;#XW`9YPleQQ$5i;28^}*_NUwvNGZvzQNOrXovv9_xGjHLLu_@uPl zvWA|i_45xzQZ9K@@5G9}#j{UDVpdUA6KDUJ^uo%9){d^O4nVI^QJkBRk`Nmm9UYgH zkzd(5IKOuff=}{s}i;ap1 z8x0u=85N6=id9&}G9bHa8-z|HtnD0MC2Ht{-Kp-H(YXUeKKE86>p`~cN3#w@CI5`h zwtNRkz%ssZ;Ti&uQBYRHz|_LR+(=tdgpCyOZEvhH+D4iJWTPoUmjQUO)Kh^HcyG8e zCD6svKuc9gL0(QyUO`D!L&w0BjO~}Au7#6VP-HwH@d^qH^RiN6L%bYJ)Fn8{kw1^7 z8&cdfxG*07RQRjYfvnbr>T{z%&ez9S{=&H%&9LT2J{u|wvC)te;NsvCkkYgX&h6g( zA{Mtz?mmR)Fo#)$lOgp0vV2QaN#-+6vlZ9=hV z`sVel-9zF~v+@5D6%pcLCdY()+#aorv5_MGUas(xLfExqf*5_jG@GwDotqt2!&pL7kTX_+Y5e zPm2k3G0&XlveKMnJ5vp1AwdacDOD9^E=CG+GG3bw>~ONRqOh={ZRQ-F%PM~zLp5~; zOFL&9UngS`R>-+v`W~H4$tF0nZfND=3z3*bSW(}`BQ&X?zIS%}1_*_MSJB)%v9xn~ z`}zwWpPpY%&(0@0y|}imcNid&e`cpAhI?8nb7DMAr08G{J0euD)>CA@>-7=(UFk1s zT?Am)%AEMXrs7qwwu-EnUiw01(Y8vRgitR=OCp^ORb-{*HOxE`YG-cI1+3CW|558D zP5f}!mu{g-2ee(0XvU3vuvizaqsT=~!l@SAi|4(mHzSyL!)fI1;3x(+?LaZAxTQ7E z9=$@L;geBNkm933y`3%f6or@y<33JgTXX)5g#P+!bELa!@>8OLxm_Rcs3|EZC~F*C zy@R8XvJ5Nk-*`f2QM8XP8(2AihX%CCoPy#q@(KX?7UHBQLi#+OX-INWW`aBENid{; z?($;4%oe|lp&HVCNnu;{CI912H0@5Yo~%ZAKiim_7@Lq&KXrqv5HpW!Gl1nXM;E@w z5wVPKki7sxuj-M}x^M+a!l&sPUp>0}f=DT-;SgEexAp)_22k^m?B>bcCrBIyAr*7a znB2PFsg*szEb#vI0}#)z_kZ^n`zw{8KEwzcz2YCAxa@UGber&n@KZCQM{ z0KsMu(XjT40< zo{klI$l#tedGftA7{WH|K(sqBzKmozEAz8*$=K)og%x!#p1go071Hz0Y+JB$@eNPT zt7;pb+dO*)hQ}tQ=M*{&0B?#O|>nKWmGzRutso7EyDEZ`p?7bE>|ebMArGto)SC*v6K2No!qtL}gQT zNvNGV0V5GHi*DJ42hrM5Hgfr_!)USSQb78VBKPEnJ2Gwdsv=YUAlE`K6r&K=~1}>$8VoM_f4zjo!hl|{sE1KgNKC#{(80y=;o}%iC$;& zY*-%aOfkkiXbL>1r_z2tj$bJEo@!=qOK-GbITjsd-?{3#%BI*?4JQfwU_6c!^OUm5M8>bR!8zBFVuXTuw%iD zH_J0`l8t-^Nh*Y|=YK$`^wQQbjhk?yAycS&r5Cgoxl^~WBy8eZ_QC0uUDG=cP((cH z?s>By+zxqT$3GbK;)dQC?HfR}n&B0t8!=ql-YA7sH}3$GOIpFvD?Dcp-Cz#iq;ctZ<8rOuhNKo&P}fjXRnswd4Nfkq z>l#}+en;of32i>YQj8o0=J#m2AXW=&+W5rfkT-Gh4-F0SakbKsWI}uTTNi1_@-r4G z2{|4igS=K`&yRflcVuMY0*Xph+tSh5#ns(AFf2MYCL+ia8|?Y=+U*YlA&07a;XI^B$QZbgYthUd5;dQ+y0L?QVsZ1(!rmP)5-u?b5graM0T~q) z5#saTayLQX9%qsnC&V(2KA~>rBIJNar>`stpusR%)Qut=cA)7DOMa~PqqWt;Zz`kG zNAc_jiJTjOsK1!n+1rH1r57wdQ`lB*KceB|5W?^!0h+D0b&Dx$1pm~bF3=c|L|QMU3dnSR##T3QCyK1uZpFGR8)B@18F z%5JH(eG}7@!(B~v%>zqUVA!;Lat1CDd9AajK$tWfyxfdm1sD zX9&IaCp&A1a&ro)IVLnN+@r84*+&$#O|I>%kJl!8+F07UczAnxdie&&<~A>WF_~wL z9=}7Pkqeq6Oh7B;U32?(-r!5v#g(-UjL&Ud10fUAvJ1)U*?EV>#sTb7!1Ux*8_+9lA-RZo(NkV5k}QWo>HGtOKP=igFWPNa{Udr(^QCY(s%AgFlc z42pK?gJASBzDe0Bxnb4{s&e8SjF?}?J(;3oa8!t^4ll;j>R@9@iob;dF9R(jmxPv6 zeDgL8uT$+EPxuO%W9OHEXU*ydHmj@&pc|@b9~_?;?x;xdHI<|S-)>8AP~jzqe_ZJ) zO^=U>3iWX`QWBs+dD-a64>T6SzU~f{$3E>@Ye)|A4os|B_+qjs+9x$F+(6*BPHJDe zc=`VMe7-t4I=lM-0SEp020a4@&)ws`E)Yh5Mv?9N)7O78R;nru~3lFiP#j~3g_PO*?-BX z+a!)bs}?^9gbck=t9s}6t(-lBAd_-RYnj@)1;l1ocTFyBY%EUpwE#B@E^8wq0;K^`OQBG%3G^txqHjr0@OiSY0}ht*$gDX>zES$2SNxGIlBBfWpyh zOv%6@INf@{V0cWb?w3{DHL?DH!eta$JGFoJ`tf>yv^-Rw6YXPftR}%pitu_k*He+` zt|LT=^tRKRV$Ke>RBXq1mMMCZt$+YDRaZN3gr<y?*Npl@K4{elpXX{;tXZzRMDOK1LDudZfVL zSeBQCfsvI<;FpL12NfpB#h->mR~;!nc7WSNj*kNNd~tMmdh-T}&#mp5m|IxdybP<9 zch2NgdyFY!9aBBA{|ZOMDxqm+=i(6`ImhzK3@@^5Ehl#?1O?B;($7{#{g3SZ1Q zrF$ES->nDP>Bf(63@9c~J9ASDJNKaY+?v6SS3D8h%xPc&{}DvPE*!sofrM3LVav#p z-sLNBLILBbwmnchY8rfy^%66p^?2d?LUoM8u5>pwZgPB5POZ?MPtn93D%z+4en=xpODQu0>x#NXSJFM-JP|n8F)78l>Fg7XNoasDQ zlMDuy%BYRBn68DT;8WByuj9`t5IO2@NYhO>|D&QJI&VvV+>nNIW&IUY>W6m0?QsJxBOO62_ z6JTbOl97^8b4Xjp4*^T2p9^K&N#;GvmHnudKk*0j0*Jybr)Ty08y1s8REUqmi$NtAl@3dQxJ>1pWG+Ebt=`GW^H+KzWR(99YyPYw(s-J82q-UdbW4 z>MxXX4V-D&;yo;-sGhB}qlLCK7bPat%fVz_lB+WH=S+?b$7hcx^IK}yTW#G~Td{w-eys^9d% zZJ*IR(BIY3Gd$dyVn%;csgF3|M7kXL3uH7x7J0qIfe-0sWu&#bqPA^dcKZ>Qh*?O< z^mkBrc#ylf0y_@y`QkuJZF#1TJjQmFp9TjZIw~p}1|g$}s=k?>M|eh6&!4@!Z)h|k z7AfH117rj2huJN9cU6^=LH?S~?bXHbxpM zS}rB`s&kg$Extks>sjX1f9kDhu7BwQ=5dV+Rw0 zfox04H4l*At1JnRTAgUlr=bSEn^q`;R$y^Z&TC?Q+@;S{QpVmHJ(m<_MeA_<)+vJ@ z;<>lXh9RZnXF%`>5HBZ-{WV!pUgq*Fs1LIxZsMpr#pV>pMVjDM5?fY}0qE`KWGE0& zs3aU3(F?>jl^c*8wpG_`z9SF z-KEhUb{1Ovh>HnQ-?i%BWeRU4%D@eJa6`e0)UdBN7v~p`5Ts((DHEUqPFdX>|6nly zqch~gVq?6$G|9`KsyHWE%)cR=56zw;C?|bcLEfPNIUk;0IeAB>7S^x}PAhMpIsze+ zb}QdRk@Btid;R(Su-+Ub4bf=~UMjrd3{%XvV0h*B9RgRPK~G3RLr+bJj*P>mpT0#O zajwu#lJI5mhb;;^-=a(^27ry9(fHdeV4p-9PO+X7!K3IJBh@J zJ2Y-xx6p7uTV-~vkL~{Aa2pvah~tq0PeqEii4=YE(_$^SZWFM=Usr`DEI-hIxx%ez z{s~}dP|61OzVn7|utlw)NjOBMBt}N2S2fj^)=u6N$VV;^yNqBvjlv4qCe<}%IS3Pg zUrlA|ppAQwj+=g^vrHS@2jlj;@?!(v9d2#x-68P%P2<}Q;QH^eI^~U=L*bDUqJf@| zr05bKm6?(3dLJ+Jkgi5H8yi z4LG3>rt3IZQfmW zh12$FgYG;?-#~v=(>1YF;3gu(g1hZWRJ?KMJKE_=2*^5>pD{X>FM`Vyy>J)?luTc| zf8K9)#_7?#j3gK^gKsp&J8SZjLfjOFPew_Cbl4KDrD{?_-O^g5_be<8q?@p! zVeokkLCNQRnlJHZ9uY`ed3ZS4SQ@L#D%&Ra-!X>*+&2X0zxZ<2f$mE``?tBQT-ph-p$9T0w%$wb0A#K_4jP9 zb$j4g7(gpoiXS;_;~J-~uZ}xXt)wY%nUc6rU)QU{eSiM3h8(L*2)2+Vewr_E16+*< zr)#ry*#VY91fNT-UR2$@U3>*wkHEO1#+J%dZw;!uCTFIrazmspYnqSQaN<((a`H@= zXo~fspuqe!@_<8$8uYMFyE$^V8A7v^iqd!o1%46J+Yc-{Ai!sEz#EM%BJyuu&l=!(qC1|9~g83%2!||!Y5(mlNLY- zsR%xYJ&F63H;s->R|WHbjHU!99a&*chf7imMt&5U$csqw&;rjDIw{kDEoM1Oki8BR z$0g@9xAZpEXLz!1c*CZDaLH-8BxD82KsM{V#h?aV$(H?a8&y#bYjrN{-(pDn%}H*? z>uU#((N&8-luAFed$Fw={t1TmY#yw1XSf-e158vDe(z~Ko#4Web4U!-kNI>BxN62q zrWrTl89#o6r+ZKWPDvFtgUK5E5U<})E zB5s62hJksklRB;hhh4aya=BLmM3@yazC9pHB#u3SJT@o{lSV%|T^nxM1IIoI80gTB zixsbaKRI1!v?G{wp$7ox%E8R#`TJxqlLp?}T-)r))WO;;iM9}h9Pq?$(8WS!u*{f) zm5Xm0?;ETLB>Y8HPiPsQUR;@KO0ZxDno5v)%@^5qc#r3L9}6a}Wt+6aTQ3VRRZ+Kz zZa$%M?s(-60(jO3#2Se+pXk^K|CSrGavJAuFqAU4VCm%Dvg%u^6OGCDV^JLjS!P@i za@qFEV^aG+I81}$V!|Sl(&IdhrI~S&(C{g_JtJeBs9NajM6UCc;P=))c;Y^y`GAJq zF^h>JC;t@bW6pXI?~LrR)(UM0F>Lt2N1ox6LCi-mOj$sevv+WHFeL1e=_fBxrsGt} z{*0B?Zj!oA(WJseVs|+RP~+a`Fm&^zEkg2Jho$A`r9=h)RwcWt(*B4?@|;FDtpEDu zmf13Q{soD`=JynW;57!OJQtU;tq;4_mf(TSgV_&#K?KSjH$jpTQHtLO* z1nF}nmvhAEw<`9>26ZgX)u;SXZB(#dunFC65Y1+mr}#@c?MkZTMYcoiu3Q&>C_wlv znk} zC-WQNGP&nO+}!k(^jzG`6nMzsU>I!5mVxfZTt|9+w5=*2quKuMsT-UH*fqKoAHABZ zG9}$%R5Pyd*#dYx(ZH#{eue@$S5-X9`=N~R(7{>Tin_q%AA z=f9h+$jqo71d9-SZvR~!@p!ddAkztN-gx+YzXF`81!%IuErrpJ%61?dmL3wSyZiYB zC8Wmqc}4dkn&*|JWFH6G77*;OoI{e(6Mc=?Kk2qXO1c%Sk+}_ik+Lxpf~=;oH4#Q! z0tx|Ar53-0|FkMGXO=h>rKsWqKB=$Z)(r8Una?XxNm)Tp{FK4-u-mf_!%NSpNN7*NLEGr#KLfqhuED-&7IekWqI|_%Mhp(d=&8m3(q1uk0BoDu=_Z)i}X))zQVXV|Bb@V_E zYp=?&4bb!kjz8>BvN`j8I--h`lk{^Scnn%5ddRs<)dL0zyVVG92eH$*Dubx^ywr{_ z{(uGg?#cu7`)rOmY9@$o={g9tw61|FKkm~|xDqYovSKJ(%so1%%*^npVczqeG_B7#nzaNO#G{Fh+BD@k0{x;p ziGuxaPzacxgW)n?;rC{1tX}n9jd6}5)Lcp6TFJd2y3KeA$d&HS1yK2V;sV+}xk0p%x%MS zTYkwnawJX#N%#hitZ(ZbwLw&>BkkQ0PMWfim^cUK%UZny@k)-TlcW5--IhRT@0XyF zzOsrUJKXtbH1~Oh4Pmcn8?N^nOEOHZz^*4so%o^w#K!UsRTM(G#6=`om*gm04ybl| z|H0SCVyYTamp1gqU{$0x##o4TKsTI8%NwiVZzoNflW>$Iv?8I$zP$MO!mf$o*2*Lc zhJRDdolBRWM7qf(8Lhu1NIpk@!$y2(IG7uGj#F3_J;@}4tX+)p`gA{m&1-fMW)cpS ztqb;>u_LeefSIP{2eOZdj)`_s#vIT(6gAWgBZa}Hp*glwzTjHS>f%03=J_4KzMJF>Be<#K74&%P3Jocel=K!kX168}|fQEd%=)T0JFlpI9IK}gBCeZ}K^Ki}Lv zscD4)Jj;@~S30xw{^juixMY4(wWB|ebSAOIWkq>$`UtIFZV}dCVPs>hX5sZ?{nf>J zVVYDg&4!p~JzjWKB*Dw1)@8f&MLe4>AC9X^U?0ThORlVsWlB%+r0tRutg+jKa&{q6 zj;sfv*m<-oS$dEu=vDrOuO{<6xsQCF#T}g`9^BWNLYGD6Y*0tz4OxD=9FP;C%JeV~ zAY5k7;geX5KOBETwRs33)&mgwDk=)9w(;ZxdvC~V^2#ljP&g%|Xom5e&OLGNF`Z9* z1rY`n`(YUB^AZ7I2gTa*HbWl(wzz?Kfl}4`FrpbDgD`*j3$As=GS!tuYFb zoc<7-8%WBVKkV40F^#dMt^i$)zx3`m`)NdYo! z1`_uvoqxyUQWU>aEk!D}4kP*1%$)tiZRQuBUl_o=(J|F)bR5Au0#C(AO9=mVIF_M@ zvly?(gEZp=8Fk0w)qVkqEu2F&p*DbD;2ggp_b{Aifj8j*Un;O>Jd3NH)ctqoXr)kO zRT6|TimhJ{r=db*X+!ROhF1DbGnlSc$o)U1ow>bLwcm=>&oAD*+_tI8Sc4~UT zrjUv6Oy9MJ{!<+4?IH~_-rC;%=L!s0I(m}kWUos+Z)JbHDwHC0UF9m(*rpsVTLrn1 z!lSCY%n~w%un@_96kXLXJ->9Crjfsor&IKPZXL#*26U@y=uN~kD%j6g=O^IBu<1o0 zpk)h~kcEef0{CyK1I2B8yWYQpvUIVaP$}Y88 zQpdvP+2a6(N*Y{+OSVij#jJ8Wv_eo(_c82MAT^}`S_MNZjdyN(fSooU=Ivml6zoz} zm=4Z>>bgw9KBlyTUqJGT;*tGbl4l7-uB9ZwVZQQnI8rWC7fry(8vzp=J=%uzv2fC{ z+z^fbesJJl)$>5yI+ij-yPj7P-d}7;n;B9smHMcE3%z7fCTxLH*iPH)MlT6%JGXZq z2`PhNqS=7YFP`rfTguC8dsnXD)q0snW3=yO!poD@FgE>fWrs9?h8maYTlHacfmp>w zNO%1)O7T1=v5iyeTN_7_Vo)MqUBJ4FYUhc%;vHmFGM~6j?B(%a$8<-?eEXjmZ8ADn zA0gmjzYcoi6oD#a&-}1j6)uz6I>>?-zM-C1drCu$tt$R8yKYJ+k3c)#C+jPkr0xke zv(@8=oXIw!+tlpB3wpHuD5m$%&h@258;p>e(|XwoX>Rz#mF%(KlkO;6Y}@?$_mj~OinLuokPXkN?JTmW&kTig zCnLQ6Yl+CR(W&K`>8en{1A~UOg&gjZKgFo#b+R1dTqs!$>x{Q3mx^cUy-IYjTUrZF zH9?h=rJcQ%uCk76Lh~t&|E&*NIcF!fyoomDf*0GqK_6|qChYOM%%tI)_u}jGW}PKm z5mnM5wO#4hpYf?5XJG$x&7DwP^OQqV{Lk|{*8C@QBBIw)*{6VgO5^1IOTfQ*e9~*$ zJL)pMWT4u0UqVr9q|XxsPyHY~<`6hUn1BZ2W81E%yl|$Vf6eBbH-jDU;ro}v*h-uHPdcost3K-298dhv+B=VH`gZ!J z8-6KP5`Hb;tnQiPgZTuOMo>nIGt2hxH|K`{^m>`o-%p-oKwl7;{2{C0P%zZO=?b%= z?>FabbE~(w+IeR((;6G9Gn@%KsJI|oc|r%7qJ5aYpxHujm_|#QTXH$Qbpa9}WlW_I zN)he5|DNyQEM8IwGH{xm4L!QqHuC~=A9SkcW_$7sUIV~9r{A%;)a}D-HXx{ll{8cY z3GN#WfD$o__;x~B4imW-Rr_#tU9AN^7S3>Z%qEn#tU*(d{9gj`mwP8y_J0_S^RKvl zyG}qz)RX=|3c5GmQ0uz8Y7+KItXRFrGyV;bVZ=j1!KdccP1$3MI1nv<;V_PBXm-Qd z*~U&5C#RP+`yCN(Ynk<63+m)fnh%qudc~z{DoTQW;LidzpXRAcdfXVON_CMbxS}-&U}L*z9~ zJ77;R?M}Ajhf_+H_9Et2)L7z5*1=f@=e(sbE#AR-GS-+J+3+Rl9FkgI^Esw8?Q5#0 zXjO2=9`o+`6Hk)7hN6-*G+*;u5~fn}U)+m$DS5Xx4w1zaZzGnzhcW$K`LBLTpUz!i za&~Gu+p8p@vj~RM!QpGk?KC~*EDX1JHx;Jm1f=kLG2iUko}r0DOtrY)&C{Fft*%%_ zpj_%Ij%9z^wFvIhXxv-`{lunvFA)I^$10*){+$^5UjBOW9(OrL40v*-;BkEGqS>R< ziw`{6pw<&CG1r>){m~fKVWlH4tO0w9o1Q2W2_{k|!v>7NE3I+XjDM0TTR6gMc6tB& z=$`%#wpOG%3bPnZ+u{U=2#>&N{~?zPtdHh~*@>V#%F(62$yCI9JzlAICLfd-p!A=G z<#X$J;ZKEVY2Ew6)t}TrdAXQPR>VIFde^NwgFMbu1#_ui{M(*qg+m9b*SK&-*XqQN zc)ie?YDTo+jTHZ>5K`P+9V&ESIfJHN@~vKt)8)bbaRMrtx$**q0At0v=0tL{`YPAU zU5B2?&_^9IagQae=V(E_QEU#BMBfN0nTyvf1A(Yq0{(jqiymboPjOqJ{t1s$!`W8e zVAg~6vBgc444>WWUy*~Z?*yiV2zae7SoE82{TUPv1tT-BS@kokiC=!NtF%~>D z4u^_wPuY{g~6IP&`VbqpD)5|7NNHRVjUz@=sZj{Q#j8q8z8v)Deeo;%cP- z0Tqq8N_N*5Q3{{oeut9|j?^Z;&Zmx^1;M+DpqpXSRU|r%II^8O0zc@RMJwQkyYf5~ z=;H9nsGNL5c)L2T5V|bpAC%|@V)Xk6!`iziGwNlCo7(R2|8@6!m&GikplhTeq?!+C zGTlywi&wh{E@^`mhQl#r=+bWY>U6N5|pEd~uE1#(?#z|2H#nj@&H;g$U>&%|o6({SQm6_aag;}yYb7`-x!7Ko$Xv`cPS zqaHM?{v?CStE%0Q(!MzxN|U5{(x+A+?Ur~Kb0ZULPncY4wr;;|l$a4u`?H-SxkL@z zB6HIec0I70B=*Ai4)kXw%5R)4D>3VoZc;?g0`XeK7EHd1b;&O49Vxf+?4{`uUt!g& zFKG;vVF&bER$s6vP&Si)Eh{ejFwHBCvSvV}h+q>)M@-!xS1a#o^?p+;*!wyeXVUEy zrX)JsPIpKM(RM%VML_wk!%$G)_`j;f`5*+2h04-CXDnd{U;9+A0RYj4NPsX5aZ+G< z&n8RRgCoL+J}y&x4|^0e^)49yG=Iu&6r0$bzk|mVa7kX~F)%HEqcke$UIQnfm(Wq? z$GT{8fc3lyM|P|qnt`$1OYrsx?ZgP(M?$)4GJbU2XEuwkZmtT}@W;FR)7`s<;c{XF ze|QIvhjQDhx5-xkrj{~#_z#KGw&9&WU=*CsA}HL;kOzF!4b0=SKofH*l4HY@U^bBT zAVKgflBAZl4Ato(54OcaN9BiyV(q9*pXn&9;sS%>o@LdOL3&BAku27hctsgzz&Czwwwg+ zq|*a6pAY#) za=fTD6_0oY7CFkm>A9$noHno+-i^zOAzS-A#)tn5q!gQ}M5Xt7w#GAc5d31rx+&uB zp>QtkYA+9xf@sse)~kQzup6bOGDkZ`5SiJXOR#d%BPHiSD|p+LoTiG#`D2mkvj|^IXO6Vr^bFMhb*^N9Ypq zmnK`HY1PtQN|Q_$(}z4wK!s3mrV1Yf<9AswGs#NOlOP^8#fG)x1vb|uyP2z^6(Ab~ zM7nW5wVD3&zLM=zKDWLOC#jc3*ln+F8$E!-2kw#X!I2Qe+G?I7N~B$l@oW1Q>^eI` z9;Wh-A#k20mzAbo(e2sW$vOXMT`@L?VnDjh;FrgoFN!0}^=Js9s0G4?zp9g4mMp#| z;pC)2IBs;LUbcG={C0Z?`-OfvT-!2*fC)w6>-Su;aoI6?y)tnL`uT8o(4FQ?+Nm%| z-Og-o#)FC>zgnA;IEdtNOvO#}Q7<*Gu_aTm0|X8GIX{5AwGQvFAwBOTAZ92BI~~hZ z{>0=t0nMjp?Vi|rLhUk)=ey7CSv56X>%(&t2xZ!I`MotZ&=@1V#_3(sUT6K0 ztNLKO${zO}g^@|Ol_rmJ(jDjQ=n-8vQfGRtnv@r=PC(@I?o0o(vW{kPx+p^n7TCEw zY(0l4rl(9fE|}GluNZFicq&3Ua6Vb(Af1*Z(Sl%18i)YYAya>LyO6K2XE=tf7nxs@ z5o|4vxnHOO*J}pdVn*=#cy)05+eBPa1FJZ)A)H$}z+OULPmUV#V>Z*CxKm}CvzJVg z>#HNSQjh?eDgH#XaAKsy45^BH-t0s%d+Z1d7vd1HWdP3Mz>V~5b!RG)=Qb8ZEnyl( z#xbqAy?bgKTr%@nvyVRY0E>%<_@V6Af>icfuB!s&W2Mry4^oB1eSsRrUQ>(-7u0-` zHto;fLVa=qw2jgjFM9(OHAPlWQJsrbR=(Rh+P))k$8 zWbI0v3buY@lKc6#40kP2em+q>CoR~d6L8=qFVQIam0%0s{EQ7c4h$1*~ zGYfg5E^_}2TU14=EOXnhv^StPP|3aX)EC$lmD7esPb~NL={kS*8TA<&@3!rK-|x3u zBc*sMktp!m&-1xv3<6&Zy==W38C#z*~q2_$JU(<=&2+i`NT&>`QsGF&3 z;=S25h|z1f%9?`k!wzs2>aW2+21Pq~YA!zI6i8C}B!>+F`jl%Knzx!IhZu4RlNTT` zKd(=>Fe**heg{e;M22B8?vk(ZSfPu2cYn&g_^(o#`jrmc5Uc(lj?OVGv#t%G*|uHN zWZRQ%+qR9#wmCJ`WY=WdwmI4EJMZ_uuCAWvoV{+ z7V!gmzQoT&hMwA}ez2hIjLA7GIn>|QP>FcLa9cd4d2%#gxRs%aBlnuise!UpcrsXD zMMG0jQk0*C90v#Yt4-i%vOit9=UGj#XVJY@56i)ebYzmBM@qi-DwnmVtu%HGrp-^e zdl|M`cYGu#%EV!q#K8HuopBbazRW@gc)jG3IAg3zj#@(Rz850)A7TQ`_@IXaxn`u7 zOJje3ML20B8gt;?wI?{}8A;*5JPwv5nsXul>kHw&{l#8Rkve_%zTB4X(qFPEQ84oF zb*st>XVi`6Jd351CGCPI_J}VHVQiV8E>s43Nc$*Dq~U|jEO`)K+0)lrQHXEN`UzBAu`Q-DdG6-|d&tZi9?rgxuUWgRtNqf!Uqii$Bf&=dP zP*G||Wk=^O=?^)}#G0P*zLxQ|&Iq0b<7EcNy!y%*sTt-RRQCZ`NzW#l7Vf2*+JW_d zz+fsqHq_0=Sed~IhX7(0^d$M(Dvm#5Pmr20hk%8rG%+Py+M9~2gP8y z_TSE013C7BP?NyS=cMOtpxTO;ZF&FsK?i*Ny!eqc^+x=|Z{YszY-zZ=djZ*^3+jje z2yN`*>+PTyZ15&|zJof$O&afGx!4GIi|j>laLO9Fyrq9`*W5KMBpf~_8vJ2Y*Qr7`Jwe^IEpk}$ilY3{4uv<=?=V0r65 zG!fst&eaDx8CnEH6gULriyV$2$yQXgkd$Z>(m|e>L;3PUOfiR6hCl4@&+z2DT0orG zFgW%%H&_ia>GWklBwx}Vfjof&{~H@82LlN%^jvYmkJ2|$T}?IKmzZlV6IrO-!zls8 z9utL|u?3{G;JD4bKm!Urf=W0+J#8`KHScHZ9g+BXC}w#LO=Sr#N~Ej)U&-EbG`JsY z{nYc$;S~{V!3Q@LDGt9< z?n*su_!v*qF51gjjA}8e22pQBb6YHRnx;;4H{a1vSytUN{47}t?S*x?vv)00F#0a) zmR#B~TkGcNYc5UqSVUWh9lDM$rLW2Tm`s|mNfUpBN-t&ANR}bW#K^B3+I5G)u4LwF zr@{r>AduZtEm_7baf9{!Qciv@q8*1ha$POTUEAWw`y&Ws ztfnc)fWOcU=cXH!pS~-|^!)5toAInY0u7ip)tgYH^elAb*id)#wOM|eyi~}KYaLFO zYNVHAB>{RINXLIe z;_MZvfmJGh);kH2vG^WHS3>ithmPXdHr!B1IVAn*S-gV5WB8%z6jSnd`5sX;X$ggf z=sJ_!BNt(-GtG*8HqMyiGF=?`ZfWEUN5DF%?UGo|CvRYXd;Rk1_INl&mvG9P1j6{|uAVjx9_a5YJt4CLne9TJ9^TOs+&Y|e%Ti_2}0(dYLesi?NL zBrEd$$v~X?XBe2%Dqqa-9SrP4uf?6q4vYUXwY;{Dc`1C90y7r9eFu(F*5U^;E(XX+ zr`3aTUUdTh1>6{Xwr_YuN^W>~q6O)s?zU|1CKv`W)?>R|9jua-I@3+W&S4hSz3<#! z*(OD_qO$T2l$32KR6J75%L>hx2x#XC67!s)o!j@fr_1$`ifBi920X~~<-dvF5q2v) zBnY48s-jFeVU|l>Ip0RI9EBd@vC}aluH;KzwfeccBp2*pvuXb-z1Dfid`Gz&172!j zgrz(KvZMOsl;#KV+}9s6TvE>E*R;l2!^faRYy#@GE@1`rJri43;1m+hC9CM%Jj7ce zFHBK_nCAnjhRov>Ag`D}d#SAKYP zaytfVihZ<^%Y+g5f9<$2?nctBL>Xxr1T`!Zfa2AvCFI)XGo#las$Ru8=*!7uTUmf8 zM5pyMi1q$C77XuDzM zV0$QBmlw;vapa^a%}n{Lhv35rWAnT8Zgy7i~On%jcqwaXntv&<=qw)#N~I_*o`uQ5X8 zHb>=Hw+}}}O9(Ol6E))9yboTr8GM^(YwrUg`G3&qsg-8dfneVYqvqM@E}juHsF&ye`8Jc%s$bX z_&6vt;lRIcxEJ~s($_wUqzjnMlSa6-_b{8B#>8VJQ^{8Bc*3#>XO)T2CO-2YD$ux7U`4jHUCPE zii}9gNH3`B7-+31t{<3(Wmf;aj;m=J>Mz1b{SEhGD3W#2a#yIDB9FG$=*|@+0w&-A0uH+>uyMME$wm4+S>VKV5kp+dn7GgWa~zh> zJR~(cEy9v^PG^QY?UFwc(!`u#|Cv1(KOw_LY|CPW!F%B0W^JOkF;;ZX?p&o;74ibp z8Fny5hnn=t0lJYVdksq?Af==vTn40+D)z%}6jL{I?2*Etfo`#)8NJn!mm2G9qtsE5 z0Bom9=gQ;3W|=*9gH*#iwWO-Pt~52kT8RPnpeN5m0)5p-6yvBXFUV4vpC0FZqdCTi z3u&jyh4Zz5G~rpT?4CE~LU+UT(*4l~DuZJuRfGI#v8S=R?r-nl#Atg8{G~BN8e4Q8wd%&MJT6^c@1^ z?Ru%o3AYf%x{E6I)M(+k*WDuj-SAPE{M--9cZI?z*x%MzOPp{!nm8LeQ4*ig_b%(l zWsSEcLJ~tCOK8g_qnw6?5yw-l#;)3g{2+Dc220uxp2uV0xGWSEN4q{KJ6y`;p z!IRIU&6ZgHy=YX|?jr;O0_5Y)L|v>o5AtzKpvXrX)5}PnpP4d1;o)NXPWUlj>ZeY5 z0c=YTNVj0w{(WSPxG}%h9lvWHMF(?hJPax<5QbgpWs-N zb_q3IJ+(xf(w!Am*w9eH?ruKt=-HU*nD`a6%sl*qqmnW*QvlpsdO|`${m|Ur8#0Z! ziEmg+kY!-g*EhA?-;0>vEOE2fFIvk2O_;YmZhVn49y#Nlf-ws@C#~;;h$_VHYdwHF z`WBuic^TLAuhdzN9UtmmO|+whu@VRFeXASol+B~_saoADk5AX@cNrr`4@UzzHiWf2 z`F+hs)Q}JJ4TC%nDoDPB^IjROPN@-=sM$9J$ouQV*;;$dLhMwE)qSB>nX3?9&^lMb zGREX{LGRWJ5J*JGl^k8rc#JfHLcG{_$Gafh1{LUg+MsRud7V91wBcy}r#8KR4*$$o z#VVlG3J%d(B$U+T1!*xpR{wa5qpzhaLbf_~M4SAeCnx{Tz(7Sx%}5D<+@7dUwx7zs zu0KKJ-TBO)@!|X#i;)QH{~X72kZmO^$SokH?Ugrtj!4BMW?<{==^d0i_J}H&FaarQ z=OIT-CM?U0wp*LwZ)Io^oLgE@(z*8V4vmS5h5`zPfk(+NZ{*++kybUd`2zDF5vOyo zrf!c!%RHcG1x;4cn!M)=VSl|Y(}|*6`Z|^{?SjK?Np?#A?vL_$F7I|{V{TS@WX(FN z<6mT}Szw!_&=_w%$(S4StjUYxCX;PQev+>YK__z@gwHv*ZG35cX{a{INd$I0|GoHxMDUG$ACB;R>V#CjJ@c_biUX5}G!6sh=5EzP5BNNx(^%FeZ{nrS!Cid)+;gQt`SU1}4uui+Tu6^|ohhF91r!m> z(A2C%e@khq=iX2;uqv602$D{#L_2K_J!`*=_8nj&kQfIE>T#vchvPi?TMcFOHic=< zBCgweDoOg6b;_L8zrPtE^km$_I^k`4z3(2tx&Z zyX>;rSBc1q<6AIv@U!)@NWlezW$wr!#CH){Wi4GTML{aK)4nuAV4~avG>y8Me}F&l zlGdmUclU$a&c<>?8h{3f^#bCLDlfgA3a0lQTaJ^gurhZRnaxIv2*(bh4OJwq;Qd_ z2GQs&p&g#;r6_G56p~+&npDycs!?$U1B_u?CbvFwTE|r!f^(>W_htKL-)h8mKa(-w zqGD0=^KnrlT?}McFf6*Fmnl3ZvL9Ck=}1ZmFf-5y+2_o{a$DpqB1**H8%^<75oZt< z5~U-D-0E}?`ERfQkR3=WVNDx1FLy@=YkgTZypOf|P&M-B@p!STxNmJzQv!dm{Pr#A zX~Y~;ySC0Q_ZCLG2FDis)7uU&PSwWs6<>a?#Tb?K#BuI8f;3XkyLWqxT;siiPyPl22SFkTCNL4{_4ugqclI zLuk`IcRtZw*F}M?4s#0`ri{8(DmWyPGB#J^!n)`R7kw$&Dx7fYA&A9_PzkKf zFpfnVl3vLH8n?D%Kx}GaM1ZrYuA(3f`rArRyuO~2067LGYbJRGRn&`4@ivBO{ku|P{pj8+EF2`<^GLci{ircY zzYBJY5yr5$AU-0oxJ~>VXU5TwPKNNIo9~V-Xg{AUHU$EQK}q;j^!|frOeCqI4BCJN zJ{dO<)SGfm{!}%eF@wm$51z-6atu!jv*iBj^Jm-ghD*c(2ps`yzdwPW)bMmiCx&PI$13-WNg z*hPZcpd>9Ta!m9=Y)V%g^rZdoulBpVsp~I*5$(M^0d{fJ1 zN5jd-&odyXpnVyJ%Q|l4oz`~(oXMkn`uSw~;9z8zz_j)N5)A|Y8<&)}k*=HwaD0It z5C0A~hz(4`DkS70GCb{QFIv1lzbUj&<9_DNyMhbVo z&_A{hPA1`$(|>?ywMy+dhQhCsaYADCYwVSTQ{K`yI^5pcQl9>+)0Su{Q4VIvpW~+C zr;)amci{=WN!A`;RNvvhHwwk@WzJl*=o1EGKVS%IYP!2POaWuTn{i&Qh04lyI0#RC zRBUy|SVe<#N39lI+^?j6X>F<@S_P@w6g-Qjj`0t1Ax*s+%l$kzQE5EM!YL)V`f)gAMs{hb#Z_jTaFvu8qg}KQP!N1<`j>anz zy{)uk@u1jYTT{GFHN zDT}#T4=|213u^{v$NrY32H6>?E6Rwnp=}mu!8hx{v^dZl*92)W0|2zvY*!Ut5<)gP zo8$>pIltm9!OR1C(}a>iT$8MYI}`znz}WP{vWlXNqyQt9>p!N%TlvOxS2eDJs3(hS z&&-bblZYCblee!xLh(%__uXtw@%K3in~G0lTzrs)Ebe-)@KFq!-w`&@W`aGe%VMLg4{P!+1!Cqp^@9?q@~ZYiC6P+ax}qmQJL z)_!TVQ@6+@G+grb0SN%3Y4Hr?3l!q%bh;cUvA%ECh04SAgnO!sz|?Qy5>j$Y2;hB! zfn#&%L^YjaG737BY|)t{=H-t+z~FHiCe(GcxAd<9d&h{dZwGU;OY@U`gPZ2xsSOGiKe4Ff0}C2EdxyG*M%(HOe`?~dXX_Gd|8W<9TB;7ym)G+tm<8kX zYS{WjgS|goIwZ836sRY4Z#w?MWD!-<0)`q+D-1t#gjQ5L5k4+#q!~ckq6T%5r~T04 z&zT(ltb7qMFw+x3?EQ^aCEUtZfo!(>el?gEsLo15Oo)z5$fjVE+5ZoPL(?fTxqR#j z5ukKRYTEcG`~pyDE9ckeCu;*05helz0L~&wk8EXRzARLgLrqQ7#@$ns0Sf`0Ro3y> z+zUD}sjOS>A_%>NP5!_kER%}0wXw04SA2eDTT5kLOn6#WY_O}Lin5N$|9R$m1~y(n zVF^X80Bz_K908At1E4lW7IdsXqti>7ctjUA^>nwjH5Z2KGd<_>Jgm;GoV}wksyX;a z#HQ!xB>34IDvAlP5WY=k8j!B0D!>i_6}`~{4|Nv!)5-R(wRa|$PB_J|<_F<`9ykgf zn_p{?w7k5kVYU5pE9-|Vb1Rd>O_{DjFw-e}20YRs6o3o~2i5IoMM8}A5&bmXw7 zU3Dj7HPl7W>M_krC(rO?)Hn|_H9p@vwO>*wvJr*ZeMCzAjcgidy0p(Mr%6o<=UzyJ z)btEAC7CfErpg0Vzg@QkDN@56&orcj2L{;d>za6Hbgn()aVuzAdSwr6T|PiylF@O9 zsp{JYMW+>)7o^9#n@ZE8KJIrGdP(8_gG4@Ri4+P5My`reh>uoSfPyi&ppn8$q^ zTz^QipLpz(>-r{Eu0bw(GMvR<6IthMK4XY84up5~xLG8_uWgQvM+wXe=0QkA&HZB% z<3l~nlzE83_PSE+Bq?DZkJg9Eb8@o+tu4&#LJOOQwqD?{sQAo*$EB>ft7~v-89@BP zV-SD)PW_!6@#Ao+rzXooivjlT1`+<{W^Z$0xGK_381cMki9m8OrZ(6^766@x>m-j63lqm>iyJd zSCp#WsYT^~+LuPsZFyn4b)kLR5P@ENBM zuWql;^tJUZKjYA{^880z1)$oLG?Z8nZ|`4VU!S1RNXVE(l#JZM;*tuwHXZ>#f#@-_ zq`r4@dS-d|V0UeCesQ+FG{I5ixlFDd_m?UwJv08%yJpEeoSc944k|sTh`fT7AQM3T z_}cBuwi7~}i{#l15JWy2@9Y@dLu56Ku78m6`@M?HDit+yhsVMzp`@s+py`s3TiVn+ zHov(#JJwa173!c$54v0tpe;;?g#ZZ(M=q)F9N^>X=o%CeYj5WkbaCOWco89 zF($dJ$WowJj}-OZ)x;lF<} zf@}>`M7eo*7>Ur3ub0LK#s~U(#x}0taA^6YjorL_e*%Psh?tb@(z@o}q0#ZF>ERv# zW}WrZR!f3c1XyEa6qs%Kzg=IkF=(6WUr>XckR^MsCri3Ep8 z#;0QG?qOqTpd>A>Y+zzyY^*8wLr6qWSX@>W$bIdcoZY;9e1a1SDry^A2WOTK{#~5z z09d<|!}a;`;nq}Neb(19)m;WZ$hJgpcN+=E8C;!;V_316p&I~b#mvq@LyQjjyw_Xg zqeykvXouD5rOeC7z^CF~Jq_?$1S7`YS@hjg7eGk`B_tG;#Ds*TO+38AQgZTsHFkA3 z|H+B+v`}V4+G|U6G}e+=R@X3bjmj+kZ<9Nx^jB6|MnP@w0ls4HE()`Pb7-iar?;mn z(@B#0TahwEn+rGGUSBa1Hmy=<-v^&p)i$bt5fI_dAKkvbgF``r0N#ParNQ>b(wwx6 zjMOMkA2n8f0YO$edNxsIV;h&i_`>?O&W_HWp>crJv43!QdU|^K@cQ=l_IkB4+S^iD zk{cZo7#JGhXsW%*>nLV)kMZhk=FCwF6?Bp5rGc?%4 z-Py**%rz<@F)qsA+04Mu#Lhh^Dj_)`F*!LsJw2Eq+==6G*sYi(h? zyXJR{u_VQ1tqHVm!-F5)?L=#T-#_s@FzfOG5XF${xs#_4BzzKp7k;|d>kYtqo|{=%TAb_a zD~kjOAyEM6B{?&%1mNq=ZR{K!9~@uazkEQzfPn&-au^H}8ajGfvj3Ptr%SyJjcuK6 z#71@zK25cA? zwNBrMsiecPnhojDI1Ca`6nkldODB+|U80J+M~^`a{q%njTz18pN%6DN zvq&1d`=$SBU4;@4tXX=*WR=wbLTyn2N>VB+MpkA4VICn>b4SmhKwoDwZAoUdi;;?) z%(T3k?(vn~-Gig8mF53xuU*}vYuB)(+`7NFzDU`m^~_E5MCkAzTg`|Me;X68S0n^` zIyo<%f`3yDt(|)ymN4~6s~p%ke|Q5&M8zg1rywQ3L_&mqd%V4W`TPX=csQ7w86O)P z9vvSXncca#zP^6?fPjR6fI~z=$H7KLMnuCQXBUMMHu3`TPh1jf6_Tq~KGx@r**uDJ*Lh7!~g2;2IVm9vm3-Gb}VYD>XMKG1Ln< zCW2zpbBoK%3bHcO)05+q3+p-u#+P;v4~~vbFE0-_HqaqS1Nq>SP~ZAFZSiw+eP3l|F&86FV{hk;!{M%&2DSXWh)0qt>TX0WHNV|@Dt z1QPV)?f&NG^5STDa(3(D6XUy}S^3T4FVvnP1 zq2MjqszWUOUIq)_wi6shuguDUt?PGikhjb2iI!gpo_a#$a5w9-OX~+0?>OA*4xzt> zfZUsjOHx@*fP;>f7zPR)8tUVEWBc$D9EFroKo+s2NlBkWVSx(ac7E+1PcWQ3K4Bwa&F`NGpUHa zV?cCTY3KCL$<-SOI4B$zF$D<@8ag@>96T~64lWrZI~f85pfPxRzQ4JD`+9$Rc!fsC zC!+u&NK$-UT%fa#ghfopBP_@(AS}SiNCpoM_H?v3)YV)P=kI8)Daa=*z(s+F{P_+= z$S9<&=aSiZ43AAr!@$MLz%HR@>*;K5>+0&_9S{=i<7#WBrLJe`;0@5sQxc-0!~LBd zTs(Y&W3x)itGfrg+iUYPQ)9xTq5(#wr>%)H>*w5`04c1k-Z<{pPD6ZMH zSIW~=Kin!GIn-R^Du(v{-9EiMo^6h`l;?%ps&G@{VB_HuQu7%{<^p{1y?YcIHeLaK zUTQon9F+Ix+ncMS)#0Iu`TZAk3Kn5WX$3`TK{iS(Bv?pr7))|1K=(uh=#Bs$CxGMw z0foV3{GsDtxe1C#{_Q&jF)j)~)6KPDdYP(>_i?s0bMz0L-#B}K`z~wcADfz| z3NX#iFE5_nKEVKI&*SqOC>#)mVG^;h5TnCFK)zjHoSz(@93LH?J%Ykw0)LE>6dfKK z2IA}M0}377wFl$(z%erh{WSM)*23t@}DU zo3d3>^~m((>i*f;{_H?kM@6!asivx`oPvt3Yf4Se=+e&dJrX4|8!b5nHY7OI*X7an z+Unfo@aWvq#>E#h3EO|BA5mTw8VXV}GK%laT!1Ksk%^6yotc?~i3|r734@AH#pY-E zDi|I)86i64`{Vg)XSSm->gi-hQ}f@p-jOMEVj3zYX`Aru(vsYa(&3r0$%VnQOQU^*({r<{hqv$W=xAstuwSo__vZ)un|1@K0p8>Bp{udXfDo-@idSX=|!Dan@l@$#G3k&lL z>(|iO^qk)b0mBxHjFE-57(EI&EE+l%lZ>{Zfv%pJxjNtv5tUPvlvmU+u=4hCG1pd+ z`HynI!9q(-MvD17mS)28)aWPs82FDR?b#9kkueg=*(0zMPc3EPS@!P(mY|ATc4Z|1 zU!LDy8|&=sZ7oak2X+R4ug)jFta*5DWp(=+0u3Jz4HX^b>-p*aXl-U})QM0{W%VIiN_qvoJe5G2GYL zT#+6V;;JpeLP0=^3HAy8{&c#wbOiDK4hasAL#ODIn3Uk-3Vge@)fIIkM^DIT_b2Z# zC`3#GKR5`$E>6y`PA(8=`2+>{1o-(`$cTwaiSaRU@fn1rv~-k&S?S48pdsNApr5X; zZqMec-1y+v@=UlMV==wYl>72Z8V@BC3y0n~xdUge1QYf#StYcCVq-H)04YItLs>;b zQ&k2K;<$TxdW57DSG4u?0mpPi

&i>V0SC}5|2N>VNB4d-XGSbtF>zW$-Cbn+hpH{m| z(~_bBLn7VngJN=u($edDhNhO+XD5fd+W>1=S$SoBXJg02K>eTJg+RRE<7{WGBS-|u z+K`aXrl-fpH-_2pM|wza;0^$Y@oO-w{iPDU>v4H$^j3`})_ zu-euF_-!jI7dKmTGgCt?RV_0o-{^wI!S&luC|Jnn^Ua~^yrhU=Pj{c-KyQCnTPMfN z%yb`z;G}}W+{~new8FFq4@(mRLnC8LdlxUikch;T+~Pm&^XqGUMd21^_O`l)mf9w+ zz?0za86KCIT~u0FSeOC$?BbG>Qj>BslZ$d=e)_oBYD@5Q@$pbXUIM8L$az;~RaJF! zZb4mZ|Jcgz)inqT5gj`R0~as<4{1XSQ&0bds=l`RvEx?&uDiCfy)r#CI6T-rG%>z% zv^O^~IX2Keva~eS+Bmj(^$Lyvb~)bCT=l!8Ah&90xcPTNgtwXMx0}vT1yYcS0IG## z^2JvFzxM-hp?O&$6VRI^Ma%Gv%$(BtE)g+F(V>yyVF{Ue31Qy$+JMr}z{1hhH#jmi z=T}8z@51`-T<^&0=E8J8kbVu$&drW3oxQ_j0k<9%Ej_2Su7$0Yt&5wTdniCpwF4TE zj{X6jUcP~T9!^dkA#oYSwH@PIr#DYeSI4WvEj5MN8HsTTY02^NfUGC2r6NDnEAVGz zRCIJ`SZs{9gC5`#RFseg`o%`37WOXgq49~on;Ys9oHg0`Rdp0~4b(MNe*91}v@p=p zG_-bh_X1eUo^DRAK$X|Y#l+HFM^TuYi5T|n>Eq>iZGHj}{Y+KnWu&LX`FJ~r7c`Cx zPEPHgU4aub(*VYy2W)Ft@*G%_0oDT3q%ni0U_Zr zu~A{cA+hPXg{Adf6D!*XhkL6N-Hm|2vNSIxH8VLSEv2xmv8ypN$luS?&BNQq4MQ;z!5EnBUPIlbekr*B7=Yi62Q)xY3S*b{akgoSv2smUV z=i(=FYHAKiDP?PCFK0dAw33&W7Z8#b=jG!QmQ+zzl>;Y{?;!kkRt7K%^JEv~9*t*=k^^9DjZD_d(vcQ;2hetuEFqbb72 z%?vP{#e_K}4eafJ!jzYV7#}GdF)KG4qZl&*$UhWvDnbNsIDC3uZeAV%0Y+jH3L-)R zOgNCI{pE?VvF^(3jS#ZC`B&d3 zJQO$%i;Os*n1N$xw3m&Eu2XQZgRZ*1y^E`lue}rS!2EP~b$2q9=cVS+QFo3Bj}CRV zQWIgJ<&~zmWmh-#&=6q3Mnm}6DRP(p{IoYor6WChtX9>9bnT0Ik8(wIrN!j_? zeyA8)>;7P2rl+Byh6JT!r=n)!;^AXvV-@D-;&<_KkM{8P4+#tlPt45yT~pUG3>Zj< zS5DrZj`t6q5wOW<*?uVKTl+*MX5=RNyIIw)reqgZ z|LyMYs7#BD1kmQ8KspeeQdm`ASy51u6yOXPtyDC%P0jSwxTskK#e{emX(@s6q;G8W zluTk;I>rWChRR&uU|+y6abO^kAYRYccb=d?A1{D1G%*42gCuC6PaqJ0KW}5Qy{fV( zFCUQ7d3$*{+qwn@1g9j2yPC*on##f*?0iV+nMbB&)(;=NU}1tbSJe-{J|8|HP%sk^ z&#Xm00&(^~qkf6F-YpJN}SXeuH1%`zNc{n?J`UFO%7h4^}V zxd3OlcW^{per0W0VO&y_x1EM4JCBfzrmO%HE&(;|HxfJ)SSSb>G)zofbW}<{F?D6`g8S2ZxD+^e4EmXiEgl~dGpc>IbCw>j86`+PWa1%*P00)<8=C@o9| zio(K3!OX@X$S24rAZKi2Zmh4Vsvs^Yg7<}hj7)$>EGEav&&e&wiuQ?sPl}95#fG^w zw>~;{xZPgnWvMSpPlJb&h=_s#^>~9w!lS8SoBVehj*sd=UO=<(m7(U1VC=)>v1yLUJkP+0H}6jCZ?3TjqDZ1jH{ z$G7L3Yg3aW)vYb{bBhCmBmGl5yT`XTfNF1IY+|@N6W~n)f6~Lo5D-`aS_?NjOGkTW zS8pp5GfhQZafpS}@8XI<<>?vqiyOxuuV)*BtGD;tN3WldFpuwq9DGcuM^7XuUl54I z%v^xCkzQ0yN=-}l2Vg;@LjyxZfI-4S1Lnpl3Gm2pKaZc@pAWWf?w9K7TPhnzbDcAS zU95G)*l})ZjoAq>(ecQb`NTE73)>gJP*K1^kca?nBn1}8<fmHyprXo4PtPF%)XU9my@EZ>^>wwh3~in5?TvM`)qaRd0;hzTosBhM`1O!8 z(gs{sD6gO>#LSE|_!lQPH)lutn~R&LS9cE&uTPg>&|fI{#MIcJ(098RfRlM+q^-NM zttq3kudJx5@^@?B^ycR3((=S`PkmOnr;EL%k(Hg99FL;5!GD8m78drNzELjr)*2dm zM!ckyoQhh`KLg`RJ7+defgE6a=lJH}@aF#h>Fx=Y1853@!eIW}xp{&G0fz+!=}c@ z0X(b7YSL1{Ng2t%s#-h$q~>-H{v8;e-g-hI1}X_ezoh>kI zWMbzUkW(|Ve|i1_b-A^^wy?IjeR%x>htEz!OF@8-d$4r*?+q0N2^R|#go1&Ig-?V9 zAE@6ze}X|lLVbK8kf4J+>}(ya%+62twvH?eR%d6VWn|?622d|+eMJ`7lhFbn4PH80 zc7A>(+mP(0(f!ApqobP-Xy}id&8a@1ggrJjGdn&uJJVlVQvSOF@QhvEfuN$mBaul8 z$hsv}Wo0$=04Lz@k>Q4cv6Wi{0FsUa4~K~Vje?pj3RVH3`5y&BM&j&Mu)WC-sAyj)4pd9qe@b>R0 zXl5L+LjS35pV;1*SU$UWIA59D1|y?q{zila4+nO0yg1y~KRjGh);F_K86M^95L(pO6g50__Y8^i`Gh^v%4y-BYsD!#rJ_qho^-vXXpkE%YsXgR&|| zc5WY@A@8@hcjt$vhv)9@5J;HV1iq2rlY(w9FYbK8zayf)UZc@a64P-pW1*m-!hu0T zet|xIAR>aCuMf8k479g5R9Cc4PPFBxrKBfCMTG*URb53+%(wmCELRmVQE4STD+llR zs@}Q%y|vAg=l74>t?{<1vfA#6+3}&l@u}{z^u*M(;wE4@Vd@_`F)j%;mpstCYN{ye zn&_ym8JHb!>=-+OBLM2d2(S0gkg$;d)df_I>>Ta*cr~R2@Ng&qfesou9SfJ3jEuI9 zCSdthkdaeXkq4A_qP$GZoLuZ|3@joN0$dFASlDRDuy4nky9Za-7Z*?006%}E@poZG z$K3Az!ph;<%iY%W;yV!yD=j(DzZDyNh|v46q{RF*FCkh`|s-L zV6wfTH1}6&UVNaBi@k}Vk)fquq;G0zqOYfiZ+J{xZcd!9wS}IUZ%}Ia{LUE!64>+I z<<;)sNL%m483;C`fG7hs*>|+V`QwFMco2y9gMC;s929IK0wh4^4vT^e|MB_wh6Zx6 zGu!p2tg@!6q@=XGzN5V&KRYcxA}qki&B{nmj27cm)`Q2L=W9e7?WCJU=rtzq_%wGt*g?Sy0!vd3Jww za(40Zaj~@dh)PU>4d~Ia5ni^JR~MJA_NNDW+FPqDhKrL*%bFtG+n=saE)S2_M+f_A ztC}h@q9S~p%uFpU9o)k_-C}(8+3@jO)Fpl|5uMMBrfu!GMJTfByjcynly40)`ZT)&bc4=F)guWnNZ6 zA)xGSsi|#hs`*op8Xq0%?`#1an!woD)6Q^BHejF+_4JI$spuG=ofw(h`ge74v^M^? zrlPiMW^S~jG4NTo45R#JM zV8eslU7hcrT!51^0ZMsYId(Eqz_Lz=fq{olEFdq+%p$IB)C|M}2-@S4(euw~tQK)8*dA`u65*KTz54Y{>o39%l`F z&hCB@0gm1-QU;zbZk{n|(f%PW_BK{-)`1z#Q}f$rXP4Ixdt-ymO^w5^U@+gl0gmqP zR3r#@Pv=i?galuA7avGquh$2Mj}R9?GZi?UKkoPTcK{)MTg|VWw3O7sy6&#_j`p_J zhKhpB#Gft}Cb~*LSij*R+^-F`lx8G^MP--Q{vDZ{n_fQrcXqtBIN96QH9WI0H#ySV zT2Yi&P*hy{r?GQ*=?;mEjZZ>eO4-;k`cGqPYfD%2@a636>Jc*(z1Vxg<4p>Jxet0v09#UmswBfu>n zA|uWVlvJtlkP)F@?ylArCp#OOS{lmp($X_hQ_Fy%p^xY5$A?!C@Q+V0SO^qM7{Fcr z@d(soA)ry;0HM$QN=ruL@X%P#-?Gf%#l^jev8DCtj`se}_U7Wmus|PI8*?KUf1u=I z@1mh-VPjz%5*r>KYVG80U})tRRoF0oy19R`akAChUEeo8{rZJSLl4BbRFu>tC~(j) zU?`Z#uwRhaa9_6@Q~Q9d_~sD`3Jmgbe{E%XXM3Ut_@whvGE17eho+{+hkF_;N^%l{ z+#Rj-HGx7UGv@p8d`nS&7N7-gY95%GU*0;sJU`f2o*(NUUj>BiD>H-LO?7oO_4O@X zqqCb=fD;2~yDO^bSomdBH#aqPPt5Eejn3|Wpc0^C5h6T4d_late1UzwV1H*95n$vb zg#jf3>gv=On3PoCIV4r}Eu5U4-Tb^Aj1Be8oSe-yWyOFNjGDZV&<`0kWf=(}E^aDf zN&>Lgo3+*X!TQ>Ux`M=n%$&TG(!bOD4;ROm*PoYHUx3^h4jB*q{r>WJefj9|{R1d= z;u9ggexosLD?Y1d>5ZLmgXZCqpH183jdk6Mdb) zq@O`v#x@=b;yMn2X>GH+%j>6`2U`QZm4BBvKVXmvzhMLIUs_^(U>XY<4FR153l*Ib z7v^<$Z2s)u;l(=?=*!d7`OePqi*>iG6k!E zps?KkC_3k;%=!QbXWO=I+qUi6+S+W}#uG4n3o|mZQy{*dtuD_E)-_dE z<;12{7UkqL_D*lz-JR~Qp3LvPJbfVJ0SiMYkoUVYBg?=E27!o?ff5h)aRvA{e&?tB z&dRPDUKp8OTAUj09UW|`&W`nWu`x2#Gq5&R6JVwwAm~2qy23)g(%=}7uriRe>XP( z5BNJ5S9j07N@2}1-@Be{-zg?dl9iIX}z+azm_;mbI5<+rDj^1Gjg-spJZGH1A zTZcq@ed;vyqMqf?8R82W{pYKZViE3&buI662cm)DFgukGwU z9F13Z-oT-vpo4~^W3JbG~Sy)+lhDOIm$EN2O=49lT1pim#d$jQ*)7&(~u0egTRI90$QBV*$b z(KFMK5))%1g5K;bj*m{StS*eU7X(_#&|pKJEc7x!y+K2 zq^6;!p{JpyrKX~$XXfA$Q3SRxU45f6aEHtSVK!*h@X>9P(@x=Lf+cmDI_U7H>;ouxVts>jf?@SQo9>-B))lF~RQU4yF>!_~6%TEBgnVBfX+_?v(ce`$iC`~gyW9FECa2f;FW-T~B^ov+1}-553$Kvq|0b%8oV=VO zFcoiL^3BCRDmAaBbD+1iy>Ds}Ku_CRT6_AY_OGBZ2pE}p`81WajPy+mjIC@;mBi&# zAOzY6=m39j|U5?5s{tPmd0dwpJEYmUp%1jqMGt?v4Xg(bnqSBP=NaJ`px13KA;h zCpaF1u#}jrv=r6-`Ne2sd;558YGF=pNq+0<)&@Xtu567p&gmp6TrcP9?oBF zuY#h&A<^(rLY%HFtX{x?W6J_N0_CHVm6NdWh!pG`yljH1ng#~q%%m)A6iBe}mBy@r-gn0A}Lh1^FT--EDrfQCk_AZ8!f|A0F+)T7wB1)RdvLYOuEX2r=pMUQ! zkM}lb+cG^gxN$*Fr+eB5rWaOrF8+aIk&}^-5s^~Su!_h7PLZ~jp1Ga9v!`ERXk^0A zyx-0J)7yvpyPJC_m&aQR1HJ9-Kz4oe>Jts0oRWNFbG83?#`@0Q_L{t!2y1x? z$m5mGosG5irRmxA<)N;^tg8CVu*$KH>9LWm!O_9qrL})3=%}a|I7GlX6B&z9P)bGJ zOkWPT>%Om7{QNWCU7nrvGcBch7NE`!4)%{XCK`((o$X9Dq*yQzU>^^6TLx|ppP-O2 zzV>?yQcEi*ckl67%zcu&)jQ7+!j*7~{RCv%JAMe13`CwU~ z8a=|}{%n6o@94t%E3Ie zu0D}@je{Fkw{IYjDCpP(48r0PVq#+AqCC{7ppRF_2W!(aD{CubosF?BDs+g~JG%!v z$5(rMr#GiFy_G5H>47fMr8Rxc-R;FKEv-YqFgGX&EEXa_CezcgNvNutxcJy>a&xdC zUDc}=@U&7Aq{P93dpw^VT3&mGgZXzl{>#-BB-QidKZdHN<~;+N8jAWE6UqgNkmdko`Vn_3FHe88;zKTie8kLUq!*j z_1lkcs&+o12_EU`ei5-Dof z{`S_M;koV8XLv#yMix#^4sHQaDK#@^|FE#|=;Z9m=E1qGqsxcCU@)jSr1Y%Jj7)3{ zWLU7z+v5Z6m1ULv7ZB*!sOan}3g3XNSX5L@Y`DKiSeUEv4_{YzUvF2>(3JG7%&elu z$-Vn8XnZnq21X_x865+F=>c?TeHm6_L`Wddbaj1yf3Y=Nl@XxEg8X;}q?{oTz&=0T zANI%U;(hHk)Ez^!8-G_MMCO!KG8l9H`TL=j16+JFq9|09jVIA zC@6^ycK_y;KXLH^j{t{^g9dT8(O3F2&fij*pN$?54j%mI`uQCR1?-~FPua|ffcblP z2LzpnQ(Q*t)FY*^gt#W~;rQVe?EPI|R$fC#hLfBChmZ^~^NER>#Y7a03|u^1gMDp6 zbFvGQQe(o>a#CZ%zkTA$^WYp!jpgH7L-;u4lSQNfg@uP0>UN_zo>$ymX^M*f(RQa^zmq0 zZGKL1^VTO44knP6C4#8;_q&uC_qh!gN1^KfCB&Zu-sZ3r^kc)_4oA?o`Qyg5C!!4Y^EW= zOoNlh@LSN&s8A;_9~Zy$KTTu%A21*HZ?ME1qT&k1`rjimN;1=ew3#0!tHV9q{e68r z9X*oUwn0efDe2fb>9O86`f9S`gB-LJrC6yca3FCBQ3>!+?v@J;O*JT43`43G&u~;- zBYw}kk+Lz-N@`gH2Mh-%ukUJ7YNqYFvt}HEYoIHWSq7zZl za|lbx$SP@?{_u{;Dz9(r9Gslld-y*`$1NnGsAXViYU}D16y{@RtRhJAZ@i{BE4!$9 z_8JTckBXj8PD9to#1^3coLn4z!h*kj4+x8mj4NqsA3xq-UR^zUfWW1uFaQ++EP`4jYCG= z*3(K)-^p50%PsL|$qF3e$NT*Q5(yo%u$qp2aCTQ)V}^wg)Mig=fVVe5(YkwvlrDXe z@d}8`DFQj7zxxwaX>s9>#=5d%g6!1XJk-n-=%0T#6TKakbc51MfBifrmiMmQ0Yf9g zhocaX)-cjj)iTtS6B1F@Gt<}7(G;YkX5e7w}U@=gXxx1WYGUE&u(5op^$JX7}y1+6jW3V z?A(Imv#PttW|#NRZ$S_U7e>hBeLY zO@Dsp1DkCY2I>I9r>(7~$N5U~)EQ}`B*eqXFC+r6Vw5;whb!fJ^4xSH0+L!Lrt*5O z4w{+{-XR4S7zluwy?gfoiwuX%tmjuWx;Rk~td93@wI&1LPyBsdoxF3#ZZY`e)D4aG z<+&-5?&cfIvO<2?=&7q~D@eYJEwHcS2S7(0x}XF1q-*ZtfHEhfsI>GLUvX6 z^xFRU3j`7Y8MBD0rB_69W?tT}l(^LLp6-UKiu@QmVerMC>hhYtxt)`nucs4mTwo5B zg`Jv;T|ib`RLR6pQBm8$FSej#cJJ{G5kN!XNSOE}UCYi7*6CTzky4zZt8|&+;i*vIRf83mv>r0a1O9+Um>I2GJT%?Dwm6>bp z1K8*L>Dv7G&f0KO#RP$4&F;l+RfeM=>1}^eW@1b*5GV-w)qRP(Gp?fk@2}_2Bs8{`&L{5d#AQ7Y+L34Uqt! zj8Q<4Ura*A&?mKK0@(b8!Xv`MVdCclE=$_ZF?ls@eZ4(H!~MP8z)M~CJ2yHcCa1c# zwIj~jMH2m>yCfq%Cd}K(N<%@89s&BP!NZ6ZQ$j`6(mo-lxu)c&<9FAzq4U?*=gpbX z_RbJh*gK_VqgHU(xAB?`J5lhp#=P{TI6zrQFCO_zA_Ooe-eDmgX7Ws!A4fCIS-}n_ zN|O8xbUeVx6Q6*d6zS&p`EY4?WcQ8MAbaxY6Xfv=hY*jDl!6==?&jcZyt*)_q_J;u zcJ1^D92^A$o0(hA>bqZJVQoX-Zl2*3vStbM*`g2jWV3Wi?IRfcNwKi9p0GqG0F{`m?-c zU~+zKZKAn;^x)#^>EUd#Iqkb3&`xxIcy@hu3Vc_tAy84E{s9uo=+5Kwzb_a#cr+qH z9%HYcO>+yYHxTG(7&kM{qXK^z>q|2dfQ%P}tB_0EIQWGY4{Xkkv?L`~&2C-;Hu%x(bbpp`N4isE zquDG86%N?xXu1Uz*j9ht@87kxHFcd^5aa^tj((v(bCLtTt8rmHu5=d$S?S7fQsQA? z(^3)QQFF4=V?billOh7cyXIc?)9bIG009m2_75Hx`f6vms@UJ!%GlJ-KO!nIzox5i zYVYa;ftXj($j&bT2;X#f)KnDy`dQvSzI65mjZ1=y3jcn8{|thTkBb3FOvkfbwN(Xa zzFO??>y1DCZFMzuOr5;^f?|L`SWP`3=nsyLPOl!`eLx`-Fo~*~xrY6$>>OX&y?DCW z**yD1K*z&E0y&wf4bx=AK>&Gsxxab7-#-FF0ROi+P?wk0FxWRdH#0vob9ewjFJTw^ zr+sMY0!ZZ$0>F*1h>XFH;MAhJmX@ZPlHA?-5C{X`S2mk8j5VqlYgqATV#w$J=L1`Qo|axEY-RZ+tw= zta#7UxmKbiU?*!!s~g+9XCN39!iE5|Q(9kFlH{(#fp9ob9AqlbO^yNi@&tp9NeNte z1?ce!c_l?v^nZk4jw#t<`}S(PnQM>lWC z0CB=EBBg0$>mQ#91eSg!h6lw}Pwku@&yDT8K%-&6zHIIEX8w>M1wUOL>u74N&Pw%D zp(n$*UF zkgPudFgUe)e6%*)QCrhc7OW>n2$W5=)KoNd)>b$6F6>`_q0JTQqh}6~Xz|H8X|cbj?}+3agW^hhR*&CsXla3uw5%@h zLPteMgoH;2y9X4{Uc7-mo!voT5#nLMy*#eeM;LJ7ex0mM4Gwl!XNOseF>?_-?+tbJ z4vel|+#zvt5@8_Ia#7;oV57jlA)_JR=ehcw248cT0T#kHYjb|M+m%1jE+*PaGO{Wv zI^UdplZ$GbJNrBTq_}7RWq9pbe!5(kAcxZ}zZ*B8fR~1bhM7a%+{)1>qX>XA=l5kW%A84 zscP^D8i$mWhMkd+fL=gBOGinBo&XgZ{OkU3ZKyicU4sSvb$fiMtG=m!;qVd&Hv!SG zf9Jc0`};QlzeY|>Ov9(3Z{r%8oKrclb`MX=!YiSyZD4F^p)JQx{xP5Ls>#pB$wo_q z4~v3$H~S~VL`6o3lS5F$*wW1{B-SOqYwZq6NKxN2zJBrqhM16$ik4kY-^k9>+so4{ zEIKr%YWWTv4F>}qpMsni4+Z*mp()u`j2!X#U>Ud)|0#-fHIR|x$9&&eoLfA7egVT{ z2cBU`n1uznsiyph;X~yRasO!c>;=xMZhC&;q8~4S>4zHP`me^pSQ<5bG4DS zimY$rW%}4i4BW(+@Ia2~^7I)A4WE()7^_cjnEgwy<`7rayYhie%go3ouH%@}zIpQj z0)<6JPDmx7s&Aqr%}q;2fCF4%?+)i%^1_UTD4_xRXlZg5h@Bx~6Jn#l{@q(zTVLJ3 zd4?xvqvjM*P}9;gHqlp?=D~Ry&ve#NR8$bNyv`p_@-hSb*>joCIj?Qjgydz=| zvjK>qk%~A!2NNANIX?Wy`AmCpxP=Te8tB{KOMn4FBcr7u!-c-xn4TUVp58clL!;#d za_aW(zWzaBF=+*VdNw`@82H4b<(1WxWyN^Npbp#Oe%M%B87OeEun>do^yK&&OHx9C zqLA_^=sASM{c0Xr*}nyc!69Q7HIDx?zjyQuhDy$@VC>*z_roJ7HodrY3dp|!mwa9> zR%%?xyUuJMV{zc@bhS4()SQzV;pbuh!v^SZqC|vze0Y5YZVt+3S^^@nx^`xkz}eK* zFFL>f7*5pt_xuAi1{J3)9~m*&dUIBoqmhb~AO{l*6DKztJsBP<%-iWqRhT}*$7G(1 zA}#pA!0+_9*wkMoEi>mgLgkZ>IQS^Y1XN@=$SCkn+pVdQe%6w3U7^B%&z3ShgZ-^F zML9{qZ!iBsVUm(ju}SE8WcFMVYlW7MoWODFxI|~yj-4PdDcOgmmp1{IS8yB_Q6+6n zC3#tKK^_)B9`u z!`UMwE-kC1_YiRBNNHJsa1kj6EKo3=6X5IV_)S-umzfy+VyP+lhZ-Xy90s+px^-Y` zVMG7a%Esx<)#K&;7mkEU^zWA0(^n)iJ`G!E0DSWZh{`DMnm>I*z@=hhqQ-^3Tg&#= z;l%?vTb~9j)Dj?G34v>3oJYlkC?EayoO~!L|9}>LfY@zo+Efh zgSde=LN-N9-xzm$dqq0v>(&0oqO@p$l=Sm)vp3h0;$tC%I_pof;elQ*_fe&JpRbH{ zGB>nvi~Zfbz5$zj07i_54E=nvGTQv7Jjv6-%0`FttKSG>F4jPs6%rm9^m?%)Kf>8W z$H*}>Atf!hZu}IU!~EyuGl8&Ga{DeAiKu0G+3+zuorHm7NP6AuIS4ux3m+g9iHiuZ zQecAJ?vDRS53$n{W2L|c!jBAW9BkA?*bp~cJ^68w>2+hfFNj3+>_SpdNk8t9Tyel-lyd z+?y2QZEL8fsw62St1ivY#Y&6uadP%}`-Ol(Oe3gh9hRCHm-(}JVsrb2O6Ggk3bweh zPj*{pWqso>UqewkOt8Dt-PPs!$>HvX;$$x?HEx2J=`0(z_wh_y0kqSO1Y2oVQUZEu zpWOcCIga+jgT3vgzVdiqz?qehR94}`I80>UbY%F~)Y=s3>*gP5r^Si&x-&UAzHxcA zfA|1RCGTFg49;N@)pUf&XBpqR56h(Dn$xrOhR3617hTdmxpVOsaHP?&Fp$BXPuIp< z^21y$)rDzEiSTeq8Ce*q@Ck?jnyM|y&pjl!b@2iemyR3gLjXSgB;+(~A~MRVhAt5q z1!c8e{q04eHj0e&Qfk_U#s+G_jI<>1U{~Ws-jb zHIHxH!_!GQS4>?%k%}8TdAI;k-q7ISgq(`@snzYBtmQkAqE^s4_lM-2iVF%eFLis zKQnU*vtk?+IVdTBpe_gEVwk|LK1h`@?F9%C((}Uc`_0i@YnrQplB9?zH=xi98U|Nx z!?IfC3_nq;1~r@!NW0`uz0xSy#x!pK!(x-O^o#vjSXNn4)zscuUy+v@72s;5D9lQM z4h;bf0fmZ-hlYepg!F#Ym7NfmSvPU~g~KeSq^>5-Nrm?DbbtE@Gy$OC(eTQs>6zPD zXz&94f@5bWR6s&jh=ZP<00!)Cts^tY_D4uuR#n&Z&I1@U_}l%_{PavO&{8qAe)fdO zVwT!<2!hQdscmj+=jh?%;~fx{lwH}-()0(A62GfV{SMY+gSkQkzLTrUikwDUr3~!{M68w6P0o9 z_EuKb*5*300`!>g+g)ikyik*o694{Kk{kf7v3U1WRf$0W@SZveu9J~HMWJqDYGEoz zi-v?vNriq}rL$%5X!}%V#t4HW7vigA>*o~|;HW?acCj&7mFWA!)-ASj>X}&9w{90h z*0Xw_+#sgsk;&rc*fqMSLt@RuAs8tKAUjBj@^bMCaxs$OVWOeK15bFHn`=Pcbm#0J z#0PNNgZ+0hUR%>P0wl|6fwgpfJvAXpq`y03t>xLjGK*_ldPf#_k1rn{E{}jlw$S9> zV=#1Vd{Uqn3iRU{h^o~jXS9xO0D3kyB_|&f5$wzP&gvv^nclm6{KDck2`QU8|3V?5 z=aNuRQU;z}3@x2J!(t->T&>jEkZv>?AyACWhN8ippv_M@QQ7LoB6u z81UepPX6NX>NtcKkDh~*O4_H^uE0?9e`|qP@ULEcBjZ)GcQIFFM}J&s`(0REUR{}= z;AtX8@-|;+$9z{~4J=RLkNU7ZH2Eu1Kwh03TmoH;<|Uv~PE|KpWHg+dBAc9tk-$EnOo^2WJm2cYAGh=bxPb$NmKi zj{pPq@$quGKR0!WM9D57uApmfp(R9#_I|RxvH%2!_b%U2=;bZ^vzmZ63fM&l0Ag?m zcr;8RDke^5Dgubp;a~1^Wqt|>>pi_|ZxkY`7PhwL26}*g=P6teoyEn4R~P_3d)hD|T2?+{hoIkkU#wzW7mvU>K3 z%O)x%FDpV1e>GMVY$i(hyxL!#lm0Ug@C&&%A^x*Lg) zpT;b<<$^>nbM+HOpbSbWX%a!wKCNMJX0#^3Sw|FrJ?ZF3ke}wt9GK7Zm7cQs7wx}N zn5XPeI?Z7k90gHc9>E9%Tx+iAEnD#8E_fU?tt&ew#v1co_>bInSnH(5SsaT9r2ECc zqWX8Pk;zF>j%&UHUkW>-#RN@in7hNXo4?>BN&%f9iUm)K#o#85P+FxAd~T(SAXH*{ z_THgDFb2qeYf7*ZKwfRn5BK)ZU3_7(DE>(6J%Oc`{GL)fJOi{=4FPfn5(N`86D7*S zRC&0KB+1)MfwQUrH9FYq>+3rhaBM`O5jPF^S=+z%4`2R!czN64>>?mFf5D$-dyPpisO_EG0oFh7-$0-dkl-OefO01=Fi05Cht2M^ zZ;k0z6c?j^N;~$jfOgU7#I&r^s)qX7y7Dx4CGy*@WLqg7z$LZu2rt>CvTu6fawy)X zb!!~IQtQI8`~4*1kTtY%e7MkEl@a9n-NI0n3;TW`LH?~!`aA-^kSrG?gQ{6-Cx~Lw zg#mP9puTYxUNPx_H*S;sC%pGuqYGFj3_(fySI@t)m3@mQnf@1UOIY;G|1KDh6 z6FCOh{r0pUa;((k1n9`<*w~m@#I&6Jd_c}#MgidZr9}ZwL|j4&D8Uk8fLcL&UCOXg z77)>JE|~g@2OJ8R1f&$?C54!gwn`j$zGiDvqtkv5?L0!^@qR145)A1Bl}_5{wv9eE zAER;`#L|iHSUbOexZIj(D@h5m*I)vj{bc}K!Z&aC;&rO{2j20>V&7=r1$!_+S58qy z#Uk%nwGP33lQ0}Q3-uPWxV>w2VY1MkYeKMzr&V-JtXV@RpL$F6=ROPu7Q}k4K4zcR zf?zqtANkwLKpQh76KnT`h7$^>1<79FLD3oY=+Q20QGP}mN$h^9#OGFk`Yxjt#07qRBU^k*G z-jMcXsjsEFAR{Tl)6Ueu0LZBr8k$&InwuHwX{qZNTRD38N5n+>7;{^x2_RjpPj6m> zP^)?sG`0fqjit4vk;-^m_NNv%KGdhJg{AFBR9aETK&@ z{G!gr*2+RvFbK%6EB_Tl+pK@-L9mXwFdd>0YI}ezi_M#W77U#vbT7Je9o~Gx6tztqzd;bNj;fhGxPH9Z9V_|c! zB-o0g>{L2)TvvK1vy=Q^C~^5kA!8a;drfOz`Pb+%4kh|gWk6$Hv6C`<1x>&zzP!G! zAV`z+s89>G+a5IsKjGQ12aa1(UjMsaZg)@L0d#76gr&$jE|ADe=y%OL2;V0Uw^C9X1>!JQ<{{=Q{z?#O-7TG%`7af*;V_OD*xehXOg z&dK$2t3_&9BPPfri9g3*aHNyBNHuKC6liV>rM8TEK$$ICXiDkYHQ%bO$ajC~kzV&@ z#rJE8i813$i`KI@QT(Xfha%=wG&0hXkIDEm{UH!}DD#K7 zRP@aM&1sP;U=CF|xTs^Gz0gw(b~a51Z>z|H`n1@RVzqXX2?cLw4cO;r|zIVCN-47RrXpvoZCh1Y!cSLE@ zAiv*SoZr2GpfW0&yTz0YZr*~T5>YV%ry>JWOIz=>+R?2$2xLq&Sdgpv<~S|%+1&4J z5X;%-j1Wh|74cqHA}BWpr*G8mo0{|5P^&qLK(qX)!MVegK^v5WcR_PkbD^)uze2Io z6pn7TJaEm_&bisPSb2!P?6*wB$ySnV49p_>eszE4t1y$O=MyCN zovt*y5aqM?pvhQRNM46yN;;H-Patj&7gx2J{o!m2H3%af)(~flg)ujb8a?97soHRtyi_ArjC70m5Lb?Ipib z)AQ>`4qMoG2P7SF**@!>8TychAnuVi2*) zSO%5Mf70uO)vkOH^I>qPm|D4n#U*Anubkc8Z1k1K{7`3woX@cpLRrqWVK~j#MjG^C zTyh5g$+MZCoL2EK#lcRKnvj(W{CqY??!X;9?8U6@m5ha%8hZ2xe2w-?DyGj2EFA|u z&P@^DsCqYgAjr>e-b;2^v%f3X?HaVo4%L6Zs(3`kdt0!s2Ll6vWVV0yu9kXobl`h` z)~q9Grf)IW#AT!;drRDTKe=5SbRe6x;dw9dHA}4;Oi+bloSl3UQwsE_d{ZE(WvZo4 zJSNjhsPAko2(eXWhCk?tH=sGrR(h|qV+LIx9hp7=rLnAm`Mw~#_d8PhU$syprO!Bh zV0GU_Z*ia!>bM{GLybN0uq|Y%?4B=fyBTzMtf4p^)Wb|=)(;hqht)hw=xNL%-3{Za zuUu^NpDjoc|E%k8m<@wc(&DU{uge|zK&F0KkgsJZfBlx(bb>07dFf2?ur|`sF>wsb z>b@d3{6qqp|r%JsPXh0?tm64|c>F3??do|Z| zR+OjpAvY&_&fO&hl~q-|>L!L;%fn^v6UmElf>tm%M0hY(BN%2KpweIr@@CFYr)%7i zQ*lB@;H>sbTfoD>3>RYJ`|zkYHRAqRZx}aR;xllNpA;&u%NG5WimWKeN-`xoXdZo~ zHZ3@!H;(|A*D@XH+Ts=R+=LB1uNp6Pj}VSbZp=|EsNEX6{}I5)rOHKq51TaL=5ac zK5X@7=wUDaREKUc0?A9lwFA&PU30%lFCM!2g9l0+=1?Q;7mHVbp<;v&N_cNNf+Zu1H@n;a=oVtW(wIAYW zrfxC?E)W@?vH>;l7Z4b9(#DPeRiMNOdpQ0l)>atxqB~IVsoLNpopC_>_}h-!C}S*g ztEaA(L#He%I;U3f46OmsASX+Fl+N5lpRkTdMG86{#kubE?z2v)6Ph0HF84R!!>RL0 zY1}j6QYtz(eb6KY*;eAM=!iWPCh=J&ZSeHtxWS#g3O`_l(&(GLubn8y)eV-Ab>q^PHp(9sP@r3Hx`s zz>f2wO!GaPcS?Uxe^r0a9i#a>-R(e}4)IF1@^hQ~LG`iFNnX1aRVseJ1O0A8z=u^2 zLO6nB*E^TFUyQ#hQn#-x`c8!{!%+&)0LxFaZA#rcoKANp`BpudXlFnH};pAn)S#$j7wnXHQwKRMPS;7a7q$4J4={^jN%mEi>m~$0KG7Ugm1!?Kh zI9AEu$L7_*RmrdT=}9)Kb8~00&iKmnMzP=BNY}mIQf`7H=c3t~K-T6Pxr()|xxRs=ckT?Vn0w{szvrb4 z>6=8#D)QtH%PH2BHK-VfA ze805aRUMU_wS}qGeaRKw3MFmQ+;>C>M@h`wnoSXk#p&;FE*@RGq~gR{)fv^<I$x-AGr?-mmvwtcko-nGoO`k=K7kuAg(xmj)Fwr@dpj z${o>n!x}cccYe7$-k22t?Cmv&C_H#SJM2qj{xun5Ex}Ly5c3)ndw<8_G78Q3bFBLH z%2Oe*qf-)|Z__drRE8b4WhvB|0` zNH~)IL<42*j$zp|D8`)#;x;bdl@J=lTgm)d&q&pP(%j_uq{=l^g~*{t7_bB2GTUbQ z83P~kU^GM%b1ahcSE&rdWaS4FDuGJcDrGoW23h#zCn-Rfyv$U_IjZv#y^V(Q|Lw`} zw~<1g3S*zrSrqAwtVBwon{#?_0Q&71UKNZGD2|o+lFxJ7M0I|W*2!Hq*dFeKmz*KIVyvsg->+pz7jaH#YMWx zKmThHQ)6}R=lD+<&%^|8i4C=GLbt&$Mk)FYHxNJgtrzxhS>w=Fo9Z3l>JMKoJTNs% zAOmH&p)TRSuXVe>s6WyXTP`{%C-Sm&#o*EmAOk|pHN?rZDTIMjDbxe zETOCCA5cB?fXZZ@2@*Us3iWEDlvdl?f-o~cJ~Zy+>17g!SxNm;Ed9%)rzMIpR}vub zG89BNW%CvXYqy}aq1;9od}p<13sPz=LQhT0EbUZwNa-|A8T_PilCFk4Wj{@nilkx} zSy-MQ@1{e0_)B_SqYXhdsvj%l$zqy0?vUQ04cVXc9R!1%K~TfeyYN66jwVwaVL67b z;)8~b@G_S=cKPMysFJUZuN>N{<@BP%7Cf5loyvRYQz&GRtX3bn&48NMYnHbZIg}1^ zEm}#*E)T?c=o*?zfD>)SA3pl8TJ9mGVN&1T6NZTWuaRpAnD^}}N5;dSQWp{UHC$x;t#IKB(Cfs5p}`Msm9$=XoG{DT73xFJv0i!wv9vkKi@ zEdrBfciwQ-Hf$dac;vLU`GuXVulH|mPo!w6$Sq1Jhzf9bb~9F>ZG@_g-ji>c157KP;DnnGdoZlp(w?+|MW#z0$Li z-IXZsE7cE_OKzAfvv)=63H>)1oSW|1B0ti#*@-YvkT9r)bOW1SttfRaYoZ+Z*Nw*R z)*Ji@(jLjk5$D}7s#vOszQ+cufn~5^X|xf#birz6-8vE9&ei5rja|`ucBAGR15ROUt72;An;wTqXJ*K4iuh7`vIYOlZ8hg8>1&mHd_ z&rLVz*gZATnQevjhr15fZI&$;*6qZ2%Vb@mp1D(X z-|laM1pf1o+rlNeZ%Oq$f+sJ!X#5uSQ#_VSoUg6qhCup06!gPFb%Y7yZJi_8s5wYJ zR})<&R~;hI9Lt!Cor?QM7x#FubzmRIxCUIK;ML}W>@ZOX8+=gb)ODReGdL|JNRRek zr`4@q`4zKm;T~hy4twATjCf!#Y7lB?peh~0%k|Y8w&1s-Ytb6_T48K@PLMK2JD(^! z5YID4QC|3gK(5tqtJoY27qKF2UjH1Icz#~;ht9Hh`zbo~bQmnaAejWZX0gt*7_R7dkbe6%{jK&Jx zq%cYDJR>$KyDYyIMJ({pycHalRO6o9z zVIhd?wh1U+6yRc^BSrl;pDue7OW9}m(bcjAgY`KPtB8Fw(^8h55@IbyhY1A&L#`6u z^{L&@oC)p)Q^MUq7PiCUn%~&oQXa%Rr!&tJw=a-!B^a~Ll1pC2SjAAoH)e#mJ<`>b zAy)ZnG=QR!I)&u{uJs_Pz6J#eLx6VNXgEQnXJ@L+iuO7kDRiA7`CO_GSH#jORF7?% zHcc3VQAppW@zENr%0MAsfD&}AQ3;i#E`Q?30e9LP%hyZiUEWliXl9}eZ5DGe;1xMciAb82vRk*d(-vj z(LjR0szE`vqng{M`I*T8q4x?~!lisYMFHRE`8_h9QzMkcjQm!l=tpl>X2U*_hDTy$ z&tOk|ocu#9?k_}_X##_k!Ap1|QUX-ai^a~uaATU?c#e6C8(*Xnv4c>GAxo$#EwFlX z?3ol3ASQOUI#QqMDT_Q6Kt1VQWGJ5naKCh|GTuddkhEyHvhlVxO9!KSP1X2 zL>U72TcDXTaur|AH8d{Pg=^j6`THzm%prH!@H63GtH!ix#!cTd=32g)Gry?{m?>?Bd)|Q=WgtN-rgb_)9VDUSNV@9+F^KkjkP zHD2TSdR^B!_v;zyM!e82=y~O`_u=g#apzN~i{k$+`8@f2e4gxKTwO9s9)L!#V$qb7-@L?*fPXQCgY93L@~e?Q-z4WexX*R-rQm z3kEID-~9&Gq&hcB zXU6sIHZuqLc!9&8>$}E*{#RX;|~m$FO$N)s>d~LaVs6yKdvUjiOqk` zaTh?_CQWwp|B>HQ=nZBmH(89~c&a;-V!}9Dap#h+lck=a>Vt*E+FH4Z&bjmc;#U52d+y?KAUN#k|JuJc^hUQ zOebwRLu9(Lek^I^j1M&kc)UV{bTN}Yq!c9{V#fTJZ=xCMSPy^=%#!--srE% zkIf8D6XgSHSuU=Hv%R~hLATJ~H%BS0=NX&-C^Px8vp`n>2wybHUzK)@OwGwoiw$c07(Y4W9oe zH+hAtNap3SkxP<`A6|W!Ac<)^gk{xEQXLLymAKrmn6+z?(FZt%1!(8Xy=V&M%lMKh zZ&F5l_Z&9zqK#P-hm*28nA~XsxlvP*$OM3fL{FtdzOrPbqGr!BPXpT_T zUs9(Y$xKRV()UKVO#ce6*?%`hni@ zqnEChy21e^lmChr6cRRkmaiOGd>&>ZFQI4|me;XEMf$rlR~LP#&9qVW);DRdNMYYwK`}YV^^r}U@{C)CBn$n=qT8RfF<13}ZVtLB#{tD#v zZbjY)w`wmRYSPFY{j<@WpgSMFvDcHD2dZ%rxcW0)@=!PpU7(a+q)Kd!W73 zT3s{19ljPy9Zyjf@50NNnDAk)CvsEURcpqhQzj_4_{!n=LRV5E*_m2+jvF@ z`m(YlBg{(tXLX=R$Cm_FG;g&}bI_%UO3v<9+b)lizOCo^kg%BB=mBX){hKWm zTEUNoDTO427|_0+&SJvB&kjFQWj~mYd(Jp?=(Xs+QvBpe*4QRgX4tQj`Bms#uQRDJ z@5+fyUHYjfUR(_(?-+E=RVnJde}@hI?oADfs~n-We#L&~#WLM~X}iiJ-5ygqw+5xt zIcgKH-l-#4SF3cnEaW7`n5h-sMSm$|4k~RZ51@!%)Ci0?CG)o{UA^^08HH)WM>3m| zU6acFF`EhcVr+Djlw9gQ+0&}ozf7MnWggwFi8v`hO~t1djGid2e|^_gu>U{uIOz4} zpS;6=u{Wdr%p|tnh8-LT0C5br>62-~$CW~p1JyZ&-P&CClQSuvdBqjKWtKh0OOVf7h;Gbd>-5(rG)Muj2U7nZLILN+cf%qfI2!#cU7h<@aqY3HwVWGyd9@ z5HAXlsK^ErW~<`NX}*wKBfPikm9lg;=6<90oc5TRh9JeaGPi$M)(tZ! z1kZio4INa?XUWp|_36o-E8#avhM8=i()zXVoa?duRqQFcQdju+-S7_@fde8~<0YBC z3s}^$UTWJm=K(oky6>M-OvuWOhMXsz`pvubN`9+*ILR?^TjW#RRN9B_6VkgnV({r` zDYa`QLGOCaG+^fDq}yt_Wya7Nw*Q67Yqbx$bxLJocj-%F3pO0O44YI6slBT)?eNE+ zC+4PFa=rJr1adWa64OOqs5hD|r0P?A2&7I|BoT!ia=)>wxhB5sU&R=BWsc|?Sv$F+ zD%gnO(?7Ygdwv`2&eX;0t;exH5PG8cUcb|J!ez{P=Ioyw8yRk9(%J%4y7@Z-i`oqu z*LdOK7MmU*dB^3zR=Mr0>g{im&%II&9u84u2fsb155+q)Cu23Wkp!v*Vhpu<)SyI z4n+^DWc_vhD3v4nDe8nE^{J9Sjk`2GfBA*6V97UZI)PWTxUq7zqT8+YG3h3!Stk}n%^773#)P3oYeGu%B}^Z zu-%AluU7F(A29_RRD1uNrPQx?mt;pSVv#%NM-In#Z0yhm>c^2~DzrMxc#Is&+ti47 z*1dq}K*}!bV%0u$Pma{9SIRs+GMah8$w-NQv@G-Cn>jv*l==mFMca`0jQpon)vqcZ zr$@T$iqVs3L<}$7dXRS~M33f`VYhjQUZZYWu_~OB9_mT=J~_7F=WZj8*~%g>24wC@;pc-C3rgCUi1#DM@?6 z&xsw469snyTuS$5(5RLgBoV~%_= zd@B$!qEo?t+}B=xKf;vhox@rbbpl1y_PJq=yWe<}wQUo}T>rDXHuUPc9;w`*$K<{21FoVIyREN346kjk&(H6$YXuh1@|lL_)KBhEvG54+ zb5brhWuMpP!hVwr=z5{YH=GYHr;0z5D&$W1|Ky zeb>ofAFzbK4${y3e{bijz~>bjKHCJv7W&)GC8M_Es3l6aHHLJo;~ks%d@mlM^(nr zEW@Rs6}uW*lWWggdfKY*`bv#o6?moBYd7jNq*qGr_+*A&Euvrf&YEeha=kO_*X9oc zf22H$fm?@e-Qg0}phij!@0i56PzTxHmCjq2|9Mrd(CwQldBVK5KboO7fVEJnL@;sn zcyjYPlh{EiZrGP;v=oFgI*UGErnb8w{?U1jUw_f?$QoV9AK|b~YuORdarO%d3lH_P zQJ|TuN<8m*`r^%d`45wP4$UP9mPqC@TpU!mwPNyCslk!F`n=3q^)&|#xx=Pm&zF>Q zr9K55AbqV#%NRJ|<)hnnOLOGxhWo2^7GCnUOZ=6RAqD-Cx%|m1Mz5u_h-dm#Gq%pr zb>Gu&5DIIi)VluT&bh#QeVlGDsL##D+Ujd7$*P&3veV*ODs^LOI7@M5lu=UX&)W!7 zS@H(A^^omQLJD7@XsKF{A7zmdsjIrCb5p=JYn)ZhA^J5<81der59VJlvc0=>f_uFw z(?|Mil2o-~0nzyCllPsS4f!GJze;U?r|=hw6bNP0Ug#9`eKfCld&}-6OUR_fg%ebKmz4 z3=9r@d7Tw%rz*tG%E8G(x$z~_j=Jnbx5kfl(s3?ry4GH+l~0?zZTBbm6w!|{ir@#Xjz zcfvEjlj{1Xvj*5~vL*!GpKP1I63eDLS3dTemJtjW2Oj9P|G;i{v--EcFcmqiu-#M6 zB%*(p#HChdv#Za$7k{pF=eaP~x&2AvtML-s>8q=J^`U$ECym%~-`h2_tcEw5DGY9P zk%Sf0b&hGui?UPy-CSE7Ys?GL0xvx%?~CU1++{_7LQwyx+o<2;FlyDRKOCv_t3KY< z;&lGHNrV3EO?7&525uSm&sujY9&(M|+pguw19<8=i~9W^1ntveNIC zj!Rtqs+B$?bD?rg$oPEh?YIDa?r#NFzwW9oKJwS(__HuEJUp@Zn_fcCKdYUn5nl86 z_?@pJHWBxpzHVr0?;RZMYpF=I;~7rTUA-k*air1yXPRQKKX!vEo<8x9eHr5oo~AI# zj$j25lSe$c@*RE*&3O->JpVwru%Yi^&>SOPArw)+1PEJ$g)X#3!f|uf)3JxTJXQj? z!icv7UmAXk=Bqy|iLFjHXSoV4)MUiQrk4-U7(WE3#~mhpH=}5?W!~sC>ek6+{?v&n zuDItewD|FP&Sgv9nIaeFrbw0Vx#s&i@3~6+{QUSfBGXB_SNgHHx9RN{nxMZaqLrGh z*288m8N6GyI|F#Tq6LdN0$#6k%BpH8^KZRA$6BuY#j?+=S?K|=DF(I`4Cp3m^wOy@ zx1}(K3OCN41bw!h<&_=ABmSkUhp)F&XvA*a5PHVx{jhm-dgMcjR<~83{X+b40TxC^ zR#E+!W+tb?1#yp@Zi1Blsnb68y28IdL>`*HqmNYw9VQ!jeLv|&LPl}ZB8TDCcYigm zbcjTcD_m~)$*pYYdNDEUL3)G*_fV?d?j!yAXr5YcwoeyDyDm_taoy*-I>6)pPPr|R zr_N@xs$%OL$Wrb)%hX+-e>X8_+Om~}A6%Df(tNlga?DsynPV>BY&l@Yq9;In?!MmA zHTFEI*TxIi#XBxZ^zYP)`SfEx^Mv#%=TL@xvuV|;5Qtwh%%0{k z@bz~(OgrCR^Z0gv0ss2jD2=}ju?Do=4}$dhw))W1_8Cbxle3>ZeR$2=G3b@h6P3o} zHR9=H=X(!c9Y6lwa=XCzn`<3G!4|P$M4Z;8s`aMIS9*Wqqw+)OP(kJLn)&Q|j==uo z4JU>}IBMM}E8N-JK;f%(^83r<`#f>-gFgE^YVlo3|1#`{=R^r5pabuiA$mg%49_*!(s9>;B2-$q^T?JX$e%!y7VSi@ggZ_%)Dexci?{`t{t6bL8=5 zH#5^yk&pT)RQw-J@3%>;{~>A{`@DT>d2N31O{%N#$W`8wgXPLCXN6W+b1 ziKvZhihp`uJ}&E`b7_!z$oE8b;IamH!fxcACHIi>P4K^5W`W7HKst4K35a zg0JF%?`R#0z8{Y15|8`gIco7yCVrUP$S)=~`n;vo`eU=TtMs=hLT61XnEje~|LajK z;LVq=*6DEB$vg?_qBM3&?8%;4Y^EMweVd#^x=JB2$Dql z>5~U}*|`sxUwH?0Vq0nTuf6?w@a*FWI(ZX!zrb_O$3=D~o3keZs` z_|LRPzD(rSrbDaN3s(2sekxU;g#6N?)DSb#?GFi-`{%2!nC@S!j8d+1lRKKQ_08{?#ucEFdT%bx2V~+bS@vqGfRA zCzHzQhl7lUH$MDR32k6H*DiB&@<4FqBAddo6RLuLTjN#70{>p#^&da^j2?mV`K{GSojf`R!NkobO-FaOXc=xf<{<`?px(m z8yWJMmYP>}U90SR*FG`W7p};Myd^VxFfZkKxBe?&7Lb&YXto~9UFEj;7Z*s-J zZ*Db*i?@qjHFge9PLKCBzs!pdwAGN{Mn!|w%zUEnzndcPp;9f@E@wkyQ}3754(#w~ zI)|o}_w0&V#*|Mo91SX6l)3Qwk81GazTfN;vXbmupOTJmB=5h^e0kouocThJVh-c2 z{gtL`NiuDIEcJon(>V_OYfZVK2CO3w-T3#4=y!p4|g+JLTjQiLw{kU zhqa|s(4G1pk{-E}Lf$3I;;wgFw%DcQ^}T+tpy*-p1p~Ie zM6Jz|)AE$xK9wf=YBG%Fd+_zeY5aVCM(TUr-3Wib3lY~7Q?m*y8;4ejT=GVzujjuT z*6U}$Kt_shrEsA~MROQ8{{z}vu2Ru#A8xhpvD6>%k2&Iyx$BckBC1*PBQ8k}mt`aICbwe4L#S>S=!T z5dXf-!PjZNhe=th1=V9Gw9ABZ*i-gh|LZ?=IKGFzZEE$$_nD4o z(fYJsZrKQKG-d^x%hJv_-0_n8{WeVLN130@%JY!pB24t`=x^qD!T<0ZcXFS-Z5>0E zf5Z--wDY|fn_f`t^UQ0*Z#nWsJ{Ud-!H>i&fDw zATH;5!@v)gBkqYWCnyfuMC3Ow)5}=|BI2RB^bV9|4HS!bm^g* zlVf4~Kc1$4ZnSV+xH;&+&z2{NL5|1pt%Psg_0MzfB;L4@oKyb!E8vtnY3u4@eN-3> zlwNf^c|=L=gvBY};OluUP%E~J+pBz6``SlL>&B?&Q-O4f3nNF1C|ruaQVYn)Dagr) z^0KocwkJ7teOsT<_?+kOd%tb$Vg&FDJs{cnsjmD1dJHGvg8wN^mf3f?iIIp}!c1RB zRZg6jnH-V(RAokanQ1A?%Bku(hh{bH$OYBXy45hB{lMl?x+3lv_w2CxkprCnhRdSO z1h+d%|k;~+=Y_t0BA9<^)e-8Wwuv;rTEv6|d${HN%h`ex!UyVO}XGc`f7xzDA^7i`sd z!Px6N=M6>S;M2<}qE<%fF^`vo8HrJ@PY?BVcX#&=j7&@+dIv=I@VdOXFu$-E6?vNd z$E9Kyn(^Y(2&x+{pkf`I@n(V`q92g+g>cxvXqZOD?M6xKGVLMjh`gq6kmG=kLvU)< z5D?b)Pp#}(qeO*WP8ge+=&8%F{T_H0d6MRHwA}br;dfSZ!GB{&*QnDaKDrYh9_jP~ zIoc}^?D#hNn%=)}sCkx?5E0N-M{o8Dg?u@X`p2hF_od_mrb|O2uD5Hx(yF-MEPmfTIypJg z-CXm$Feg1ZHY)hEvzd;(Aj98;4V~8Vb@P`%#Gp z0>ntPoZ?4}J;KwTHc$WKMmCn;vd*FD8DG}3$tb05<$l)xlE0gUuA&Glac!_J@2cl9 z5%O;zb1zv*6Z)TAGUEOI;jX`-3@dSab>SPz*?K!Ze|TH*1TSw9e9p_=-6tSAt@P73 zRKnRU;!bgG*F3gg-8s6jWsObqY)&_oc3Al=`|*H#@1}{UG|H`)BTz`k<jqW;EhoRQumBewSnhkBaY29oT5X&u!{<=JkM1DjV1wzX=UNzLhQwQw*(;*m z*R{zy(1S(}mL~c~6-7B{AmZ1VnVgvXI=8&O_4^;#UFj_iQCcfHwSTrIC*@vQFLuZ+ zwdtSgd|jzQhOV1&i!>T7flH>{v-gYB?gSa#hMnS{~#+vOcgO^gk7f37LG<{-XW{+|vv{PE)-qqxqg zsKOo^!?dXb;Y|zH>I(*e`x&F(%Rbc`P1jNuA%9*KeT-E zCtcA0;#y{j{2IVwNp?8<`SDf9gMS)t9Q#+{$&9n3RGV0fVz?M`73uGziu5~_z5ZL7?~L9sJQp-;sA6f(#uGm66S*<1Gt%HA}#_w@Jo_w@~o&aB|kkc(eL@{r0=18X<`$fVq= z*6H8O(#KAP->VrVNLofc9idS>_n?a)=aTSpf?UQT>|xU)-9c0Tq|(pdAm>3HOLzaU zEAhAQ+`W4zCGmP>kgt>3u_FhCIjDcnetD7Ncbs#f>e5jHDp#X87pl$AvP*hB69ZI{K}85?akG>i6Jz=p7swfA7vybrWL-MBuSbE zXVgwn$~weWPBUr;J)YpSzB9n>`gm3Tzv?ZyvjroJ$AWX4X8{AZpoplbh>!pxz265n zCflmAubehh;ip*bt$BF;oTZvD&3fPK>?k)~QHr_7+;FRdGz)DN50bB53O;|@1M&Ts zpmJ7bwl1fGqmyz-ttG1T{}xF8KVNQ~O*-<2tW?@crghW)-LCg60J(F&e(SC+N;q$JkZPj*vLV}efsgcB`31%8H_>{`6VD<| zg?HNXLd~Tq=UblNjXCF_f8>BLFVB7f5eX@2RJe|hmsdbUN>))t9kEKe1;ypQ9o%A( zHxEp$?BC(m^1fBODrlG5NoyF_gsGx@lS+Z!M!Cx7H$DyQBh%j z9u7o7%*4#X!bC?2Hs?p$>z?J@40hBK_%~D$?{a8&3D!0#|PSJitbwP>K^tSZjeP+=#*ljm-|=>ASCreTRnHKaXw^t8O9;>EKk1vz&Tu7vu#+Zt-h3NXUe zQIu&UUi2_l5kdv4nxCgfc<9Tr{u--Cso8N+^4X7hQT7V7-`=L4KFYb-Rh|~^fohdY z3G;HXu`n?p##!Rut<|rCZFOb2^MjwNpJXOP1$f#T>8Qwx3+~^uFD4c?RQg&(TK)*m z(GU=skX7=geewqdkDQ*%f5}A%Bgl9Z3_T;#pMRSA%`Bzs7?fC8H}IWWNZlqNp{Rc7 z$377aYrj}j=XUlli_}q6&p*Am7NHCUhxp;+magY6#w4UacvMzh{|Uv?Ma1Gr&A`gV z!^_9Vw;x4rdYp23X=#9l~q+AU~&xp$L)BRcK|4^0_7vy4~ zB(QILsug7+epYG%@K~KvVlv` zjqLJ9v{+=5R5x`GzL8m4*EzL8Vii?7f!6{`%P*^I9sIWbmy&tEnCxLqL{?y7ZRgW_0rC;R8Z!6x*}C^`&=X&RS`R(QhNROcb_FkBBmXm9e&3wDOLN3OVQP z>S$|aVPYw1N*AG*dD zHvf^+v2ybZiy@4cmXZ(?=I3UkrvN{fCVHA)7iT9%oO3c#ljf#IRLPB%dABe7IqDx4 zVfwq!Q}-zOzf;GhnYKq83ggb1DRTauYAU&X@ze=fPU7ltb7gLF#2LF2ib9NF1p!=6 zVx+%|nGOODs^7~KT_0beA~cCtFNK^x>*M9&;pydjF5tfiMD=$6amBmVLBu%1C?I>( z%=LR*rvgyXaAeBc zG<*tr&KKhTrw>78Lzke~OhigBw!BM)@J#i%nS;mKpm4#@z6@hSC9HE_%}T(s&ojU+%?6_`6yhQxfN*`MWyNSzrDzIr5x?o{|v#?p$~6 zqub#gMoPTotNnFF$^W_N$?vCFA4T=GZicv-s7bI9*C#vbp1yjVnGkv2&FZA;0mOxa zFnXl>(>qkcKmY!{bX0ObDJl6@O4{Ab`=~bE3&c^>-ZwUjYH;pj6OdNcF~J!e5;GrH zz3&)Z+#zQckkv49JQH^F-jmm;T;(5H-h)RiJTKnNDQ_G?rB-<0ek@>)iAo&n+4Z)M~@DX&I}O()7pzs@bKZJ=!_RE2A2>&N%yxv7!9 zFHML}tq5W0)$j{v-0aQuwN(&7FuovzzkjZ;EX>YKPfm=FkByH{Oiq3MHjfyi5%`m+ z=vlc1#AM_TYv|}1BQ_K_uhae){tJ&p6|obNZ>Oba+{?<&&dJWs%0#u5ZzbHg5^*u$ zG-Bn@(^8TaV5fmU7Z53L&9l7pn~{N@wgwuqf~@4*^Fu8)#rNX=^Rm>I<)!)ct@~Z+ zy=#G|3{^y#;p)hzmj$<@P)2lEjE(%~xBjND{VjDbiZYX~Ts-GygCn#UA3HrIw)^w@ z(%jd{F;r)LV4xp8!Z0*EIzEZ2hApFX{4XLbM;Wx(L1oR8ruOcLx+39j{ z;j*2X934dKmCsEd>TBO1gMLx={Ap=PN%4~>PaZ!;FU7?rrKQL<$`Mg!)yr3}Yu>!A zdspA^;X`9n^QX^f$<>A*_`<8X@k2x1+t*d)PoES%%({~tcjZ#>IZr1mLtT7dj+Gj2 zEl>5g)>jojNR16Y?{1^7aY&e*`p?=_Pa`td_z1sKX4>+?tQ0#7BW-ohb5pN|_}Ck2 z9uj7w{_`Ddt<0b_Iqhj7KE{BLToPC2X$KTC={-(JH zNuR{HUs6fO#L)*O@|nnWTl%I}c8GMG!Ut7QoOL@J5*3${nO|D@_G4?$@YMY3<}M_s zVPr$FiJpsBP(GrrrGqos86swE{4g>yG|)qYaXLpe)m2rLQAMHy(h_25oq?h#Cp#-z z0n^j%qoJmvBqt-0hy)Bm0RH{``{(~6CGH_R+uK`+nsf8V#`^cwm8He`*_o+{(cyvK zt}kebTL1P{#q;7qfwk}d{+*2q4Ls}VY-6f-^ayeuE+%TiukZ5{{p}y$ymy!PnVR|ERKzFc%}`pKZkDxwnFCMZ|1xUR9Po zeNvR4i;DTDr={FVPE5EN7k48z_BwvV#@>j#8K0PhkR<(H_Wj&~$0g4zUcGtu@l$*E zz$l_!-q`s^qGmvh*OIcz8pjOG?OeQ3VX>biTU zoWoH$DJ}D1QR$0UweOqp#wNclu5N7aBDzd!S_T$&E*?IVZHr4vBg;M{C$FHWh!nqf zC@La?Fu6l$BlCcaw3MWzggDx65Ec>?;OFP#-Ot0##mRvx-m|hWGchvI)6woppMo3- zAA-Mse(!Ak{IR~ava~QaGd(djJlNOW*^b2AP+Rk=q6|fb-20j7DM>f4MMqo=@;{Ae zt8L7UPU@&1mX{G1x$B%2N}2H5r5kG)9y~zrg}Q+O0tr| zyc|rl6j*!9r>2j4yehcMm7};*^5pTOqQZjwyxfNm9^B7Cu!W8+1Yiila_--MfOxa> z@(T)!9zQ8Aefqq-qVi>RO>JERzU|T8fwoSEMki2WyoA>K+rR(9JwY+EbKw9WbwCc4 zS=H1&jx#LU*gLs;c%SwQ2tq&^eKq!G;;q#5ds#?(C;~n!ufSV#qWt;Ok|&P}^K&2MWZgqtf47q2<6^JI zL`8&O47m{CcgEMt!`0cr&f3D%$N>4frkaY9ysV6*n6LoaN@8WA-$zAGgnxe{Hrmx? z6fUO5M~3=)yE{-;UsXi{{|rp_-g$Wfz^f0zziM7R2>9s!L0w)02(0Pe15LVkFQjsVtj6ea4nXM#F)h3^62TMDV1RH1|8Rgf0O<6< z0rUmY4iGRvZUZ@wL%?_p0tyy@Ndu??pfv&*00kh-!hjZ}bb{zg00aOUACv=V8t^FI z7!x=T$kB(Z_=gkZrvVxt@ev@>;|)_^1yB&55hf4Gogroo0S35r5q7Zv^bTHtKmh=4 z0PF$bA|QG|%og&RKy)b&AUgx0JE~<0i0=S2#9Jo=e0ZaTNxXM(6Ymz}fPHBEe1Jhu z0Y8O-a0WkxHXRT+05K7e+yOBH04YdvgWxFMJB<@WGD9sQvOfQJFl0l)~5dqAQ)B%XpCW=NB) z0C+>p7h;tF%iZg3Z}QZb27prlOaS6I02u(}0$2=S<(?+!0AR=GM5qSX8(ag_F@VYN z6fO$7ctA~jKp>ofL|+Kb0Q!?b07L+I9YAA9aSE{n1IP*(bol|00N@Z{IH3xNj{(RB zFa;2rFlY!#&XC0%jhqQUen95~DF}Ul&&WvV|4adw6gwb1h4jWU0Gj}C3}8cmFaWUn zJw-JF>;u5w0f1&+hpU|o>B4aDVl~`0;2C_D5G_L!gfL z4Dc2E4?0C)5ohAV8{zi5QXv$r$f~-c$_tTx4muW1*7_0hqxZLd4MUG$bC! za9^MS02y&I0m~+lq_BPN5Jc?_(hh$mp2j6hjK*IH$hyP<*1MGzYL+0;B+#90oUul&-h~5Y`}!2V`ij=719UMgx8cMA$-# z0&K>rL=Zv&mLTlGu(Jpu0hA)*lG=hGmPBxe6wv^=06^MK0x$+(HV{b)O&Vjx{RNE+ zrURO{7*ZeP1Vnp4Y=X$%kp};VLvRIvB>>_nkf^{*5_G~*3ZQY>;*Aj8@BxBpK!)Jz zB0j)l(SYm{J{P(;sL+>f6a*arEs)B+0i*_y8(_m2h2fs{ zDU+k2kpnq=I%s4BH^5+Fi;s&8hyWM}uo@yXCSX&PxI`WHkdGEXX9B^VKro~rj6x6u z;4(l>5KzcL7L33Hoyh?xeD((P8*d8($kx!e`6voN5*GxMkc0*$Ng&(6X9gH?abP3} zJ%O$@I-CL7{}ARmu5UsJBxeHn5S;*a1`u2Di4#2bS`-AtcYtt`fb~<8pT*-5mOz5< zh~$kJ!H5E~5b@Td0X@16@&Qo)Ra@Wcpz#t001hm51oL-&jhK`(M%zc1aUDHz)Mh}rJ(9g((oPa;t?YFhHv7rHkVGK9`jD-x>)Bi>Ol|73`YE;0*2u%P%3uCaN z0)(4;u&hjk2%Oc3xVg{)I)oaS55QzG{9IFjDG~uAg!dqmF$+Wx3#9;v+%sEW3~b~0 zPk|g8g?ThcTL7vcL70G@Bmh$Y_yHD;+c7>;W&lqC;DQ?tgB=-Qvh9WtMLZNXkK>j> z09AV~eFl=#0~&-k2){$|ID#61NHcQ;^a_=Ts5`<b}BLFDSq1FhN`0VhUC=Otz z0KS6Y5II!Evl|rDF-1Hu0W!#7?FO-bcpN~ZA<#g2YXr2x7`7LLa141$q((U)YA+vG z1@d1`I}|p(0Ko~5?67t({-LWG@Np730N6_h$p9}d3xWzDpvfN~(0L*E6oI4y0_FnI z)WCh*wg4NhD&RsUU}h#k1uG!XLLLht0%t)$SVg#qGENA;5U_nXM&g6?h4eOp0QllU z#hwAuJOI6bh~k&%o?1j7$KWLl*)f?1M9_~cn0Oh$=DjnM0O3CjN)tdlK*0`y zC~U_t6j3$zewD{hChXh8n*T)(9ss=|1qw$9^^v4dBPjqofV&FH*but2IzT|?10fpj zGk^tR1SK+Z1At3J3}92pf&eB7aEr9YC$R4vBpD#Yl0|3;QNz%Xg#NomAR|Yy57?6k z-wE_)>VR++2S;#uPs|7bQHV+cKm`IOi0l&U#u!bg37`-HO_b7LP#7ONQ4f~_J}e`E znc!FA)@TL53K6~sG^JDkqTT>d-g6pHSmxm1;P`)sKO~|Y%YM&A`w6Hq_yC~~-YF&p zh{%jkv_(T21t1%bUBC~*FiHehm?a7M0JsM*R{%1Rr2^#A2u=xq$>0td$rHjO04M@- zl-+P(xLGFvFa@A6hEhOmFX$6AaDgLq!<$7=cm$9J2`Ib63dykMy}ruu92i(5{6^o> zBQJymI{=LUKzdprW3aUb2uT=Gr1VFVL346IA>s*OwnnQfr2pf1YE5sSikk`vDOUkG z5}r-Mc{F+elQDoYJd_NZ0Z<}x3*ik_2z{dse?pe|cJWL5#TB59ZA0Qy$kN`|J zXbQ;qPU9K{1t8bS$r(S;=6wn35D-v?tO-bL@ST;FwT+D}at$;$lt|}5Oc}uYz{L(B zq_s7&JT?+i3*jLpyh_Gv;sZ&*15lu4Lpz2 zh^)gAY!Z+~;nO&E>eTk#xK_ku)zh^N3GFz^eIZW9n-pxh8>V5DNSf&f?%j#cOl9^>=@5CGT_B5{p?WdZOE zf>H=RLNpPyJRza35s>o&X1jC@W$jGn$1!jWj61jf=te8fY07e6^9Yz$!3NI z`g$kbnBL(VdB*sC6twmbC6JKut`RAbr?+FsA&^#xN<^?tptnIdSp!g_7kKKF3x1FQ zxP>oe>79_P%mM5u+S3MiI%5lTZW&ry#~e07V!?-tB@; zMeYOuT47m1GbDRwXXNK0XzUOoLq$0ff(mhnjg$B+EKZUD1tp#h9r*uiW#l1afT)G5 z;T^gqhqi=KutSqks~f*o}b|F0ji^`+!73T0*m;p@QU8sB;`lrHO=_6yVf;{Cgy3e(W7S z8Jv3xKN10XGD;>OP{K%PjnPQ}8-Vy@FWq^Cz=nWlczmcY0z?6`8|9@C3q&ylcOn=8 z!G1D~5)dvD5g4Oo8&m^86A+F7`cq7R!Vf^?J`@BHNQ6`rgbWhB{Sdx(C%8d0a6vDx zQ5>^jIiNybk5cYoN^FM~g=XBV(@})NvjvD0V+=`XCMZcmI^qB>=&q`zwM{$5=j`GM z={>FkW~2gSV=x&BCmEpR9SIM15+I3yZ2+)?5ri;*H#b)oms6)iiMXfDBgBCSu$bJp zNB{-w7&0gVaQ#>Qzn;zm%&O|@`)lpJPq}60-eEvQRE921K|oPZ5yW1wfJkSC4l^(? zbSzjvjRkCAS8Ui#Q=>@~V=uAA#Ap(uMpIssSa_qce!sQx{l@1R=AL`bKD(^)U;nlC z)@gf_r|NV-=`0gWSEso7w1eSYIn-+}RyA|4=Hcq3e#&gkA(-Blx;aO<_o+}y(^XW?0gPTC6}P!sGqT0L!`R;SI@AF(iV=FXjG zKZ$577EAFetdGUTzj3-HNwIv-3eP8ffR*b1#|b>$tS*wgMN64ZFps0Fv^wJjbqES? zc{l|OKtB2dM1JdZaFErue(rbbE^*SDYq7@tsVpx!g-sYO@+>2I%^;wAQ|f1Zr-rE4 zVk8KKt7b_}!*(S4MnQ=>UQy)6%!|Ap1))G<+tgO%$`a^XA(R!&#`+c~^=1 z8iG%yF33N;?4Rhp&OakIq(`OwtXV;U&6hZ=Hms5!>++%6uJPm`pSrpxutwLQRSCmH z_EbF^)2G-5x4W2;s1qUVo#*xO9FEu37TrtkB(!$+oWpId-EYN>amf!vx2xQzAWqX^ z2(Vu2MyfwVnb_+17u8`94h+nxjlZ|Jk6m?qfe)EX@;iz#Ys`QaELa$S7Vr~?2S;$Z zNukW}@-Ht(svMa4*AqOit@FjcZYGm+yKCRrRrFoa$5ghfLkPZ#gxvm7pMGpMLeV+Q zO1&+kKZl6Qp?}#2PPPS8hFddzsMd{0fi0+#Tx-qiF|o5Ji)-@!Z2*0cdsOdtIwmg6 zO$yh0HB^Jr6+Pm3RPl{1uDK^^T&zM84>%b7KI8Uo+Vr>B3%KKU1{FRO+OH_9)85 zWt-<|suLqjWaJk|JMe{*%VH3UQ|_O;!6l6Rr^Tk3v*yg5zhEJwZ2A7j2E1?~^rVR> z>{5py_u34~51ow2%>H5tzjLitGSD<*`o~3&NY2_68m*O*oJWbc?NM!<;!s z$JwscaHUq8r}?^t)4YO*Fi(RF1qxkIInk#RJSbMuU1Wj5<8$ZHsg{;S{47qSx~Qm9iXjrt z;$GKv?wot08+OulMpJ9hUofq{em-XO6DL!HT#(8$+I`;qMyXqjU^YcyyHGvK=c1sV z>UEQe7MPq+9e67r17t*c;rKh78rgW;#OwUytC7O`y2&BSpxn6JVF$#hR> z_~i5%O)M=^i~wG(i|5UqJ+pE8w9iEbC0!>^?P1~LCythAW(hyBwMC1N!zWZ5rMlpU zp{{wN>OXPj5XQ2U7nxwtJ{>HEFfNnJ#9@7ADOn3>+VE;fW(bd)4g8J69x1_WFE3vu zuA0d|Get%A^bk^VWpFm1;Nu#3Czsn2IlLrSSNum6EUNw@NGX@ei9_BOHEDGTBrHyB z3`%3Xt)7x#!sOOO_76*`Ua>W97a%I|l5$&Fr`2qoaN4A)Gx%oyf|;^-$&#h^17KOe zGLBMx%>BnLT-f~mB*uwsZH-pBuH{O_7)Rq^_E4IbC1^F!3KZ`r2#)kK-Cim9jvlKk?Y+~~LtAg8H@3z#VJ$_)x&2uS%bF&lT zQY_x?iSe;p1=e@*+&QymHZ?WEZyuMon?xw#rLp|FNF-~A!Y;Sg+Mo62>N1J!OLcVB$I+% zo#+l{U%ccD8ZeB{5;w{+O$l(Fk%WRQF3Njw0Ty<a~eHQj!_D z>sK+de`e1A{>ORIAJHeeVKTOOKTAE)9e;++i|jztiuJR%X>}11#Px3J!Ugl@Fi;}? z<e!V41o!6 zC0f$OEv(+`rs+S*AO?1Hn8g3fZRVW$3tJW=plvMPiWMvQS@r#o{k?LP=4Gi{0r0*I zHz+fZ6Gpg?ci!Ay(p%l~yxKI&``I}i116Y_WYOL?LjXE)cM)CZCP}Y4cu)C<_13j2 zaiD>k_sq-ahmbdkn^d1Ow8mWK*y?3XPDm8J&RBwgl~*N>X!kYOFmLYZAYM%mlkmq> z3VsRG%}UK!FY@p94O6G@9~zn$4Gj&;mLG?u)eZnij5fV#x3ZH|S4CUy)*4o=E(a#k zvw=Q56Wl2!(;?C*n{jv^)7Eb8#NehI9Iji4rDyqD9xP^zXK8LdV}+7b&QJ$N=Wo~z z1hPBFD^Em7A-WPluFiHS{Kv)^?s*QYF&9`A(D?JDTD{vp|A z>7V=)$dKvTbj-=*LyC*73IlUEG=PLY(bcP0ulfH!d|0M+t9BJqTGo$Eg1bV68Ae0v z;!hPD0@iVtJFf9tAP8P`x~C*pnndcbAZt<`>ZF?(X1i0BT;vqhkx`5Qs=M0?B~2xy z(0x^qLn6ced3n>1QB~V<@dQ*aT*hw=7tUm?dHN2_Vnny+e7p%CvP}+Q^h0@ zIqaB`@o}dVu@lmu`gK}u{W_HwoWIX2)wn`p z#DsA+%$ap%M;Ym>bCABpt&8BZl_1^x4HzPo@Sc-zED3MX8hVf5qx5v4U8Vz6K_myO znfGbYKcfl+$c$o?XdlsoBkZWRtSOS`8bsf8?hPk@$o*Y4@}F8$`j4fqv1>)nE69~K z`lRvOYg64{}^x|1KCOHKbLJ;#|6A@mewtQI0eoq@!;}y{?^bH%n_CN zCAWU<+O?!`kW> z&bXnaC0NQ6v?b5k8_Y`WD%mwDc_dTmXCrM%ItDX5x~ymAnu)yKL7y(r>AIHBYlJj7 z{r*ifeZLj|s?;sL!MAJaR5DXd%dfkxixzDzr(?3Z6@QGO`ikIJ2GT0k=EKO<1d?H( z0eV0(*(}FSzW~4aMVvoGq?-Bu-lNBtIGnwS@h}(-gVq74!!s$#!=0JO0g0xf`y`40 zLf@i1hXlDhK{Mo-vg=f1+WM)GjFN!?)zMd_{TfnHEaa;^U}?1ECk4H#M*nJ4=tuCF z`1cZ5Q6Yu$E@|Z0scq_(NzEKR>`Z<@-1K>q;afUyjL;J*-y}NDYq_9fO?9*ob7Kdo z?qigBVXLF_N_x-++&z(diqxi<2K!u|YbQgz*)RlrMM2&x%KN$Qwu>yPmE+3puU@9- z@3+`^4P95cNpO8?xv8tcf$uEm$6<&8KeP;!lv`}Xu5k(`_CHKXEW|L+tH|q~H z=9M*qb@wwYU0^wf)dvQWokkb(c~ZQ3q=q3tODhV3#4PiBY<%)z%lYX9=V#QJtdKr# zE9qT{-jJIT99A?Y4D-}I(SJ+3RnHN#m_1;E9EfL0S=uCYE}0fEFCRfLSzV00S);W! z$AK^o$4}ToE0>_QnI-y%2FPEH91{}ih6YLwJmENfLSXxP)J^vyYV~@PD^Z7VjOu9B z!I9RXCaa^d;Mfn^w|M6fujBbF`Z2_1nVFE!C9EVc@KQ)wPIr=*v-qk~Ky03O4jXu% zckfI1P!x#=*{sAtNJCrO(D;TWmM3&)7=Sg(qrD{pNXxnp3T{G4T51XY%pHNIc|QdE z8LY!-lN&vmJX0p)Bz&uScNtmNLT32jFXVGn_S0mTy@1#)qYn-Ch*?1#C(pi9K`9-z zPux%|?j|okQ5hAOUT*ViBI^&MM9%HXL~#@!n|C7%YR2Xb@qa8-+sKfif(NQQU!Yg? zHcLaLA9%MWn#)UbnG*bJ>2f;#lCzEBm2(359RoG>(N|p|QYh$)xk7FJQwWdYMs{01W1DF>piSjNhF2f|f=J31XWZa;-m$qRO|x zwpU&I)3Jo}b2Dx|YP80DOh#_&Wbs#EshsX%(8)pN&x`uC%-Zd_dQ~?fcnL2^7$O~3 z>|NzUGD2jVee0H+AIhj^V~o69C)+@%lH;nS&3zxGQY73M+arv42Ey7XZNEJhDs>cX z5CNYt$XaQ)dY4!lyPjOGQY<){rC*t;Tvd`U6G&n|1NP8>O!A#Qt+QjR-J&L768r(N z8fyPcqP4@&kx;JuI;uiXNrzcS>hv-DC)Lxf zQq2w4;whr7W*$j4C0j%rq&PU%nly;%DSfV(8Urn9HEa}Z1kQ@zB8BnRUn&O>Oh!wi zzJ@8h*1DbT28Sf>dV5kHmf9#BS@@mEC?~)5Ixo&-ofWd-QXP~$y2i_{*2>@z^SK9G zgPBkPb^~Tvuwl|@z45cfA8+3!S))|COTwQaJ1|Up`Z-N=6cybUU8K#*c2iddZ0xo%v^#ypzg{6Nd_6c z!FLrr0h3D;eoqMl?8||W>D4vSo?BMsPa@6sJqh>pz?7F_^@L)gov+*Z*5LP-^!b=G z=`RV*)kyK`kYchHl^Is z8~^TL*>LVF+c1&r<0U;rlV6A)EtDg&N@1oHze|zJm}ZUa@j)z7z*p z6MYm(-Z{3rik8@`j}d9NC^p|%>xR%rn_!b1%-cz5FZq*<)-&maJOjSD(J>U+#dM+a zsia9aTk8rndMqFnKa^qR6!Z4B*X`qGiu@sSv)aq1UDDwJi_X=e8)JPeuqgF*SA9y= zz(6S+X$Aj@?=R+-Y3q>hWrr6PkdQ^8GB0>^y7TGW>8Q6ECxk=ptoHJ5EpEa8)s}}Z zDjM4_G1DFFb36Y_NobYEpNp%l3mhvjUBV9S}%{IiT`bW*s}Baz25=$97}Lw|lGO?+rAy&popZBq}ZZz;lrB z;HiG5DmP0{zU_`hQ)+ZGN%2sF_u^7s=$B-p3+dtZbPrkT2o0*MH+8lkuY0@vPi6ut zYjyX^X6a#fj4oH3~z|6FOb>V8C%A3HUDVQfI#M`pU1}y7Q3f#&2OA)ikA6@KL zS6LrdYR_;2Bc-rscQ)WS2wyd&XRK<rr-Y%iH(*oxQdRw{y}vPz1{Ay(jB*-f(ID*?GHuD7=OAuKw| zmclGnXX|Kf9B?1j!J$raxX)MFW%*Yc*l1ynZJ3Gto|O~$6WjsHi3{1cGE1x`D48lD zBCJ8~G1ImLiG{AJNHWVRRxmS+kOm4A#O{ftuth0#_{uKCEq1+^k1Lxc6V3aW^fc71 z^=2AqiF#fvKB1No)^SFQ61%ep%_Kr7${YALs$TD%i)=aZ1P_h{|46FVbsBP%H&cv3 zY&kF_aoX!DvvrTkOqeqUducSP7116x)89Ab!9#AMga!(Cw6ahSrV;ZFi0E>`5rQmC zODTY0Kg#ZvlNe{4eI;j$oaOOC))cFr@&-RaG?IxXNYMrE=T5O3Bx#f^UZ~K@ZZ^yQ zQX$pCcLOT(_{780#be2S%O)&&F?M6@%Wjpc(4t@N&XiUTLPXDy3Xf}LtF}u&k@WnI`O!gR z8MS0Do6vWZMJ9-VWLt%u&IM;&=pi9`o;o;2`dYic6sFoceJ#;4jSmoyGA925&D-bD zU2rRWZG@7a^BaP%yq`j%*|@84NH4P>Muhqd=*~_c9?U^%J}r z)j#6?8;98*!)px>0ifx!UGi5fP-1&vutvLLwD>fcVj224Sn}Bh2wZ2wOz1{UFKePi zI*D1M4wB#P#qVRy4P%83kwjH>l_ip8T<^(02;a%EkK*>Fv23=r6iyA5eWaA_DTRZh zS%LeZwGNWSQ4iG#mK1 zR9Cl2a)1=N3;Y3(7k0DX&?W{kcC@byk|c@Z?31%0*rZ5n&Guv{?rr8c-`_Zyv}*Jb zb0TZ(yAIj@*6Vbf4TZ<$Ctci2BmChm>t)dd;~+BNzEoS<1B9`lAw3}i!*|T7IW{L; zVvAA1<=PVWFbv|3kc6EW5CxNcC|NI)o)T~^U@4dtUY;Cp?@K#Ii2b&NK9WVuW%c?` z^M&2o(;aUws(o9`T+9bPk}qW~DLaYDWZB!^s+iC6jD3vJ{^vmnE_R^3FJtxST`IaL z(L~y>ewkj0M2C}EL3!^`nGXu-ZQTfmia$3li%d z4zX9rnInJ*AW|3Kn4T8*4l=aEe2x>0vu4K1jZh4qP2tY4Gaz-dBzeVN$cEMs=iegA z0<#$m{f?6K4pNUoNifS6z~R~OodRV>5#UVF!5uCL*uk0EgYn~~S#o-S+v*oz@DJLx?YV0z_v=K#X= z3R=k%%-7ia5KwLk&)D@M&S!|tL!l=L0qY@!tlSei3L43PWrD?g+;3Y*4z<^SarsCs zvy8C~u+V3K0j?`Eg#J+k+JsNBl6;4ij(_m6v__KCO?rv_xLA%TODNOhO4UZSb4rq= zxA+lJf%Y_u5nv|zS+}xc+)lfn$Vjs%pls{YkhpcTs}U{%7u`8ZPJ{T9jNax37`QT- zvqtM#Wlf}7lZBEGj}!2)wM(pv@MOHCv|jvYhb23Ff~TbIiBiI;-swYAq{AlIs%U=! zrBE@=!eDaHAL{N3Q%GJO$Utmc<9ve}S#qg?EgIHriJ5Yu^H5E=Dg%^ ztbZ*1;R?j^u5p!s{L|I>Z;nQID7@5WtsxxSVk?^h1HLHbyKC0ZOd+qc)hyt17y(lW z-S~N&0ni2{q>cTr8O&jLknMg((AMG+78qOSe5M`ME#41rN16{T#<%VSa?K-bG)ViV zV@;?jgBxGiSKP+N$hgG)F=9H@-v7C-r{O!{ow0wqmbcL0I2_vd}zTW zv161iZWs(+jTzu3i#WhiW|C&30Z#9BEDawEhXe)L&CwDaV?*4H(x>g!o|)X+Uc^RmGdz2nFr>g^W{P&*VrS~`$RGyc>G6w_pX1=Y z{LvYrXhKK<{PhX$&FVpJblOE-Hzf;f_v5Rco+v*$(I2hJ+_7{;JDlW?ouIFt=#CQY zJIswSVX|uK*tmNwuJ@B7H^nPGOwF;!)&U?PcSi0Dzrp%q?mV;<%6^LIW#YG{=nnyc z-3Ed{7Xe?E&E+#eFDo`mlYOcFIZ3{>-JB+~pE%h-Hgvi?Wuo3;&?rDyh+8b<5L3X zcuatdjB5(719kF-mGPRD0o84b*P$`EpJ(@!Y^%zDMHb=>9>eRb!LmG)EI(o?OYUwb(kBL_7fsNj+d68{CoCl=_tbrTB@ zO_FCO>t)jfO+;#G@FtNC=1a)c*t}FFc|@|oW>8&&`g3AmcZ%RuL8dq?6P1X9WgR8% z3d2T43OrfYw69s(u*Ckx4w0bL#!PQZCH_hx?4|Oab3dCJ&Yb9`&G5T4x*ta)zcqR=T=inh!^qu1`?`SSbTLbi4~!!*xB{k(h3CA(SysjyY$4s}O0$@`P^fT{AxRCnnz zPl6Q{xi-EGIDS1<>&Ced!%|?7f}{538N_hc;S4v{g+DCU zpJJaTX?^Cp*BZ_n656#L(NZ`6h?RQPeEEEV>$NyzD^gq*+fR-E|2nQ1)BXd2&~j&q zF>>n`q1^H&BaO0fzsPktCD*QN6OXVpOWw4!t4z1$O?#l(STj$@Wt6q%Mu;?uJnQ|9 zk{m&mH)jf%WpsNsNqgr;#cAIn;aDh&xL#xmo^tv_p&CH$SeTEU?{A)DZ<&?Luufzd zJl3FNYs1{xWUuO$O25Ta&{P+k*dpxfo$4vQ;idjL@wa(JrVAPQJw4Ad+|s{U=b!0E znY#>KG*CD`Gy^F3tvSpOMwtHl6?k;pXe2P%Z|82{?CQdS3-uMMgM-{J#hXQL5n&Y` z$c*bGC&q$e)8@<-%FjpJt%OZVsfxNA zt1OlqWJj5p3ScE43j}aRcxm_2K2tLt#CJikoFo-Y4;miuCrzF+XSC*}n45Zt~--PT0F7nBixWw5}1E6L*` z_-MX;OY{T5b1<{Eq4kb=Ngqn(!Q`qK&z>sJI){b~k0rMsomHJT(Ya1e&h@rulg*^a zTa%_N5gZD~;IW`IigQ@RnM;{21$wZ_%k<2X@7!1PrZ#uvYJD-)ms1KE$;w+X;*KD_ zocxcM_RX?tN&Vbo&a6&<_yXy3fec!qU$089X$3`uFXJRNMKd~>LMKN7n{+GcL=UuJ z4M|xWM8pCf=j0vHzXfeuDE}6BS{iVBOC-6-DzEh*Ns+iGmdlj$q+5#~vqT=x{AdUT zhZt!Z*kUP*eR31(*v169d!=FF!j9Akoy+bh<`vnKJK0KimMwGb3Ynegj8<)GCG~IS z=%tjElkRX|D%X{5FYooO`q&29vD$xg7W`t3z2&Fs_o$PXNeHsbku)wRJds#c*QM5d znrd&AhoeZ++q0J&Ktc>(cX_GkAB8!H`#Oe+?%5)foIJT+Sj}@Uu;QPeE|rrPrSHw< zRD0JLa2_j}eqARG^X9KwZXUAP6i44shlTufk=(dU9#|_Imzf9*Fth6s*`vL5 z$>oo#+g9m~cHD!0vkraG)(fpK7p`T4mis|VzA87Ixw%LYijEh(d@&wPfwmNCf&m^8 z4}{%^9dHH%0ezvDvN6ZH^Y4sNCKo%um-@eL(EZoDznwwk7CL6`cZBCbj`K>&9`1*I z<;*$T(+Cs}E@PYMJQSKDRj+C22OOeCp2^E8{{HfGPP&jDhAhDu+Wky3ME^Ql_E;J2 zSV>QlUT4XFg6-a*o77;P7j!tg;e{$wSI#`-TGbq~-(cEDY_GINQxACbLH4X+f0(R-BS4@KG|MrY~baI^lo!|=W@|S;AP#u+C6%%)|TjmyEDjPE4 zpR7BzBz#03%#c!K`jxnS)-IkhtrS#t69Zsw+4r_J3Um)5%Fx3Y|sR+B!%ZF#mH|IA_ zo;tm8=B&C*e;V=|LpmZF!tGvnAa!Wfrts25Y&$m*L9o7!8;K^e^B&DTONX7UuU%-q z`H-_un#}lT&QCaO#=7uOQHE^rx2%+dvSj1=difUlAkk%O^qqpQ_2K=5dcN?Y11P%s z3NR3&){V>QW_FU32bp0>-)}SHu{*Jb;yn!#^6>$Y(-QZ}1+bi!2mEENKAy??^N`i< zlcx49xVedSbRi+Cy1|*;%yf7eLd7%QRziO=q7}Vy72<4--DKNMwW~_E3x#U`-*1rC z_4au6JSS7vk+6DcauVjd75#uOkmzH);2C zdrX_&>Aa-B>I~Z-<32J`f_0Lai9A=7^VhknQvH;WDb}YWlh?2z8dZ$4(xlnZ8})%r zdi$p8B-Z3T&Pqx{mn1wS{=Go(#7J>r$?}}C5%(Zln~WlgNTev=CgGZ3`ooL4xSR;F z>Ke{WzN6NF{|876=qU~Ut=T4QreN|OEy@lovvSe|H~3r56`c0Q)L-fRcUM>=jjB(l z4*m>BJ@XvB=Nd}ab zdM^iv6$?LR8A%4>J{BboxlQ#Z?kpqPCR%T?Y4bW+z1e1|h5EYdwx2R?{DetUrp;i7 zW4w7U%EHl;l4>6(h&n0d~FnR2gGYr`t zSza9Szd^_)y(NK~lJlGi1V^4-#PL>mW3xiKL_A@~ zP=WK5|K2XA-jHnE>iXVXJ@LUCF`n(F->vjq2#^u=(}Vt1<^G&`&hV`4sOn!ZJbQ3J z+cACN*s_9Kch7ZZCP$PV8_3^MPrWPeIau|fAfT)PAWb}%?MipIN9u<`sE}U2FcvJ(9?!iGSlV!*kbck>*X3Zwyx!*es<-WP0y4Zn>n_CORjVn#+x>xalE( z-9rd0IAY}~uV<7U3-Fw{M>F5e$;Iqg+@!$6`48y6uFNuQ+~PhiyD{aYZ%OMmg)<9oY9~X_F!?;uF`atKe;~vnl zuzk=!8u0J7_!tznBlPK@V?FzQ61Zg?8I#mTHrTVR7tKfw$MKaM%*A^;XoF+zNagCD`z7n?&!q*!LuvL9b07?P>#iuCQY1h>iF^3 zUZvmOCAT{H%)7;jzIqkq;^dMY9lZ&LKT_b4b!V;GjqjDqqOImW&$yk*(65Vn`WE;5 zn@q#sydHv3&UG9aLw&3aFHyK<Ly^U~yCKo8H)p40e9O^dy401Pg<-46`pmr` zBp9WVnP6U3i$!G+`E3QcDq%Yg#gWviQEF=sb_&^jPH#))<)REO_yJWz_|P!yDr0fB zI%!D!bvECYb%Hc~*UB6qKMm+6>;pr)j=R$YsVf?PzoYK?5if`xH+9kMq{Pj=TkhRr zn<2K9llv8D#QYm5{fI3v99P3u8;7UT=Oudq3n%VbQa1M*bxqgtQtM0Bk7rzAOj&Q? zj7cx8#++wk0lTWJQGP0&4XFF8%z)*Pu>=(?Zr9l9lSmYqN@xV_q#Th$+awGRiY{Cd z9SLvw*jLAsZP=Qa-gb|~*z?9BnA~6wSKEe8`6v3Hygq9v zNMZr5RoEe~Q=EgqeYPY6a~YmP3&|U)EJ}QHk{*yEH*%QeWqdc)BQ@!ydKZC;PkTy! zVie!a0t(N2iCzJ7E7H@L^+4~Z+38^wQ^I6-kCexU@paTJ<762O;HiW957foO{bbMk z<^8+d#wUoV>Q1>i2_M8Yy3qWU8}oDgNsd2x%?KU6*eD@bM>jY%24`ecf7qW}1h0~D zXcQ9M7}!`{H=9MPdvrVE$PdJRqEE)>Fx1 z)(T+2t@Gi$m~pz?W~0b4%k;dFA=L5~u!KAwq)#wEKQV_t%Aa%Y`Jd_Mxqka&w+p-K zy#FARy?-vFg9omR3-aqCvn_|>mJ<$)(ShoYwBOQ9Iu;r-vbo#E|41C0aOxw1O%;|x z4`c>J(OEh`aryWk%ONFMxtl}Dc#vJd1Zvpbe(E-e8yjbsOx%Vj{cA0+QxHnyczAkj zf?Ll4MP-f?5=m!ErzQSV@j3pf?;Y!b~xtL>Km(vZi(5NLNF) zhC}eU@{S}_$~-E``?k6!H@zv0GN-CH*+)IuV+gf(9zL`g49`1?0_D_LR&hpzL(`(H zEVhoOq#V*a#r2CPy8uNwBmw1Rz8NSc19xyXB`yNSOa_~Y(c9z7W6UvueP1%9Ob@!m z(Q--?o%oSMQVNe)axz8Y9+AMly`l|F*1r-ZzP&hMVwqHEKkF>k zj<7Bk3Yl8dPik3Xr>_+&;^*EO_R+OQU!PiJ?I%R`g&6vAiW!2tB1wVgaf?gZ37XTk zeUSZyc{4T*-IUhAR^)Ae5{fUr~1OnU=8bR67s0D5!j#a*w344IMt+xTQX#s88UAO z$H}6$NPtEAZ?rvQZGqB8f^Ul<0$aZy47P^^#l74?@$$U0lfzKe&YAi0zA?1_qF`~y zvp+8v4Qx7n%6cTRN1zJvWQeER@nlFCTON`l#1so>&HT-TD&*d7aXdq9zbT&Aw=Ys#;88Nw|fcs}{(}h;iVvGYJJ*Sy6o&PGB6&O_2D~bX+aDxSUCSo+l zlJc9tqCvxnJ7+K!yybLZcw8(@6kVUj0rTA0+is`G$a-?JXp^{Y;-3&16(_Wjh?%b& zgnXUcQ_(9e1kgVf1!9E<7R9cIHOfK&wq;qt__A#jhA3qkoS)3dlnQg}5k>0&&kyX9 zBk5~WwjaDMshP+}@Rlp=yVP}$l}ATVx?x!;-AeAyD!Bwd%cEoMmBC;Fmj;RL@MXD1 zIq9{) z+T>kd+F3+ylU*u`v3tdA()%oD#-Dz{3R=lvjogL1qX z!!ZJvB*cJR$N$>Q1A+M)=U}m(D3(XVBRdq-iF&g(Loy~-$$J)LbNGH4h z2W*FswK^%k#^QjI#sl3_57g;eOKHIvW zBja2GQ4J)E?`cKYm@JPxeP^|>NsSvS( zsi9&D9I%JRrV5;Q{)uM2O@8zj?f6|;q-EdBp2K#yA~^6OTesv6d!qI=9VoC3|NIl;!mA|vS#X7!ktdly$Mv;~Fj_=|4b5p2T23SIG-ii{o!`k*7ea%+` zAQj@Q#2W7kHK07;8cohUPL?;^Bn9?%cd{Kj`xIj}Ze_BJ&R-?nX$)+ZDo%s`S|#Kt zvq-hE>@>``PV0L6olCSDR-y8V?Mo;IjV9dd0-~?x0KBDzjWZzu53-Q|4mC? fe9P{bUD+x*|2|i{SS$aM=lHX2ET>;jocR9$B;csL literal 0 HcmV?d00001 diff --git a/tests/data/test_zarr/z/0.1.0.0 b/tests/data/test_zarr/z/0.1.0.0 new file mode 100644 index 0000000000000000000000000000000000000000..ecb301d59bda1d52cc6c43197476c5cd85006a3c GIT binary patch literal 88518 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

m3VXNj9y?Z>GtxAon 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
  • standard_name :
    air_temperature
    long_name :
    2 metre temperature
    units :
    K
  • " ], "text/plain": [ " Size: 2MB\n", @@ -667,7 +667,7 @@ "tags": [] }, "source": [ - "We compute the mean along the temporal dimension. Xarray will load data in chunks for this computation keeping the memory usage low." + "Finally, we compute the mean along the temporal dimension. Xarray will load data in chunks for this computation keeping the memory usage low." ] }, { @@ -1099,7 +1099,7 @@ " 227.70048102, 227.70048102, 227.70048102, 227.70048102]])\n", "Coordinates:\n", " * latitude (latitude) float64 104B 90.0 75.0 60.0 45.0 ... -60.0 -75.0 -90.0\n", - " * longitude (longitude) float64 192B 0.0 15.0 30.0 45.0 ... 315.0 330.0 345.0
  • " ], "text/plain": [ " Size: 2kB\n", diff --git a/docs/examples/xarray_engine_ensemble.ipynb b/docs/examples/xarray_engine_ensemble.ipynb new file mode 100644 index 000000000..c2c296da9 --- /dev/null +++ b/docs/examples/xarray_engine_ensemble.ipynb @@ -0,0 +1,1181 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9ea9a922-c03f-43c1-aa83-b4ce321b75f5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Xarray engine: ensemble data" + ] + }, + { + "cell_type": "markdown", + "id": "ef0e9584-da7b-4461-804e-5785e494485e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Get input GRIB ensemble forecast data." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ecc9eac2-21fa-47a1-b1a4-37a26c980800", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fe83d176cacd410da62e44b2ac16e0b2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "ens_cf_pf.grib: 0%| | 0.00/7.03k [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    centreshortNametypeOfLevelleveldataDatedataTimestepRangedataTypenumbergridType
    0ecmftisobaricInhPa5002024060300cf0regular_ll
    1ecmftisobaricInhPa5002024060306cf0regular_ll
    2ecmftisobaricInhPa5002024060300pf1regular_ll
    3ecmftisobaricInhPa5002024060300pf2regular_ll
    4ecmftisobaricInhPa5002024060306pf1regular_ll
    5ecmftisobaricInhPa5002024060306pf2regular_ll
    \n", + "" + ], + "text/plain": [ + " centre shortName typeOfLevel level dataDate dataTime stepRange \\\n", + "0 ecmf t isobaricInhPa 500 20240603 0 0 \n", + "1 ecmf t isobaricInhPa 500 20240603 0 6 \n", + "2 ecmf t isobaricInhPa 500 20240603 0 0 \n", + "3 ecmf t isobaricInhPa 500 20240603 0 0 \n", + "4 ecmf t isobaricInhPa 500 20240603 0 6 \n", + "5 ecmf t isobaricInhPa 500 20240603 0 6 \n", + "\n", + " dataType number gridType \n", + "0 cf 0 regular_ll \n", + "1 cf 0 regular_ll \n", + "2 pf 1 regular_ll \n", + "3 pf 2 regular_ll \n", + "4 pf 1 regular_ll \n", + "5 pf 2 regular_ll " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_fl.ls()" + ] + }, + { + "cell_type": "raw", + "id": "48c853ab-abec-4475-818f-f64d8d3d01d8", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "When we convert GRIB data to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` the ensemble dimension is defined by the \"number\" :ref:`dimension role `. By default, this role is using the \"number\" metadata key." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7ae667a2-8e6b-4c6d-9cfe-b6bde40b2971", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 33kB\n",
    +       "Dimensions:    (number: 3, step: 2, latitude: 19, longitude: 36)\n",
    +       "Coordinates:\n",
    +       "  * number     (number) int64 24B 0 1 2\n",
    +       "  * step       (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * latitude   (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n",
    +       "  * longitude  (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0\n",
    +       "Data variables:\n",
    +       "    t          (number, step, latitude, longitude) float64 33kB ...\n",
    +       "Attributes: (12/13)\n",
    +       "    param:        t\n",
    +       "    paramId:      130\n",
    +       "    class:        od\n",
    +       "    stream:       enfo\n",
    +       "    levtype:      pl\n",
    +       "    type:         cf\n",
    +       "    ...           ...\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    levelist:     500\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 33kB\n", + "Dimensions: (number: 3, step: 2, latitude: 19, longitude: 36)\n", + "Coordinates:\n", + " * number (number) int64 24B 0 1 2\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * latitude (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0\n", + "Data variables:\n", + " t (number, step, latitude, longitude) float64 33kB ...\n", + "Attributes: (12/13)\n", + " param: t\n", + " paramId: 130\n", + " class: od\n", + " stream: enfo\n", + " levtype: pl\n", + " type: cf\n", + " ... ...\n", + " date: 20240603\n", + " time: 0\n", + " domain: g\n", + " levelist: 500\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_fl.to_xarray()" + ] + }, + { + "cell_type": "markdown", + "id": "6f2a0d29-10bf-45ad-abf5-e457fd6f820d", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This default behaviour can be overridden by specifying custom ``dim_roles``. E.g. to get the ensemble member number from the \"perturbatioNumber\" key we can use:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ba4a3746-3549-494b-8272-df0b03d6936c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 33kB\n",
    +       "Dimensions:    (number: 3, step: 2, latitude: 19, longitude: 36)\n",
    +       "Coordinates:\n",
    +       "  * number     (number) int64 24B 0 1 2\n",
    +       "  * step       (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * latitude   (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n",
    +       "  * longitude  (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0\n",
    +       "Data variables:\n",
    +       "    t          (number, step, latitude, longitude) float64 33kB ...\n",
    +       "Attributes: (12/14)\n",
    +       "    param:        t\n",
    +       "    paramId:      130\n",
    +       "    class:        od\n",
    +       "    stream:       enfo\n",
    +       "    levtype:      pl\n",
    +       "    type:         cf\n",
    +       "    ...           ...\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    number:       0\n",
    +       "    levelist:     500\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 33kB\n", + "Dimensions: (number: 3, step: 2, latitude: 19, longitude: 36)\n", + "Coordinates:\n", + " * number (number) int64 24B 0 1 2\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * latitude (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0\n", + "Data variables:\n", + " t (number, step, latitude, longitude) float64 33kB ...\n", + "Attributes: (12/14)\n", + " param: t\n", + " paramId: 130\n", + " class: od\n", + " stream: enfo\n", + " levtype: pl\n", + " type: cf\n", + " ... ...\n", + " time: 0\n", + " domain: g\n", + " number: 0\n", + " levelist: 500\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_fl.to_xarray(dim_roles={\"number\": \"perturbationNumber\"})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "596c4426-32d7-4766-a640-4436da604918", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/xarray_engine_field_dims.ipynb b/docs/examples/xarray_engine_field_dims.ipynb index 082335403..36bb29093 100644 --- a/docs/examples/xarray_engine_field_dims.ipynb +++ b/docs/examples/xarray_engine_field_dims.ipynb @@ -75,12 +75,26 @@ "outputs": [ { "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9d15f57ec3eb42f19a24d4fd6774fb79", + "version_major": 2, + "version_minor": 0 + }, "text/plain": [ - "(Frozen({'forecast_reference_time': 4, 'step': 2, 'levelist': 2, 'latitude': 19, 'longitude': 36}),\n", + "pl.grib: 0%| | 0.00/48.8k [00:00` or :ref:`\"grib\" ` we have the following mappings:\n", + "We can convert a GRIB fieldlist to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. This notebook discusses the **level** options used by this method.\n", "\n", - "- level role: \"level\"\n", - "- level_type role: \"typeOfLevel\"\n", + "The level dimension is based on the ``dim_roles`` and ``level_dim_mode`` options. The ``dim_roles`` are a mapping between predefined dimension roles and metadata keys used to build the given dimensions. With regards to the levels the \"level\" and \"level_type\" roles are the ones we need to consider. When ``profile`` is :ref:`None ` or :ref:`\"grib\" ` we have the following mappings:\n", + " \n", + " - level role: \"level\"\n", + " - level_type role: \"typeOfLevel\"\n", "\n", "When ``profile`` is :ref:`\"mars\" ` the roles are defined as follows:\n", - "\n", - "- level role: \"levelist\"\n", - "- level_type role: \"levtype\"\n" + " \n", + " - level role: \"levelist\"\n", + " - level_type role: \"levtype\"" ] }, { @@ -56,14 +58,16 @@ "id": "a477dd3d-e98f-43f2-99f6-628c2bd8cd6f", "metadata": { "editable": true, - "raw_mimetype": "text/restructuredtext", + "raw_mimetype": "", "slideshow": { "slide_type": "" }, "tags": [] }, "source": [ - "When ``level_dim_mode=\"level\"`` the level role defines the level dimension. Since the default :ref:`profile ` is :ref:`\"mars\" ` in the example below the level dimension will be derived from the \"levelist\" key." + "When ``level_dim_mode=\"level\"`` the level role defines the level dimension. Since the default :ref:`profile ` is :ref:`\"mars\" ` in the example below the level dimension will be derived from the \"levelist\" key. \n", + "\n", + "By default, the dimensions related to dimension roles are named after the roles. So, although the level dimension was generated from the \"levelist\" GRIB key the dimension name is still \"level\". " ] }, { @@ -78,6 +82,20 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "59d6167a6800496ab715b55fd46c41e0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "pl.grib: 0%| | 0.00/48.8k [00:00 span {\n", @@ -286,15 +311,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -444,18 +469,18 @@ " stroke: currentColor;\n", " fill: currentColor;\n", "}\n", - "
    <xarray.Dataset>\n",
    -       "Dimensions:                  (forecast_reference_time: 4, step: 2, levelist: 2,\n",
    +       "
    <xarray.Dataset> Size: 176kB\n",
    +       "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
            "                              latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
    -       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 2024-06...\n",
    -       "  * step                     (step) timedelta64[ns] 00:00:00 06:00:00\n",
    -       "  * levelist                 (levelist) int64 500 700\n",
    -       "  * latitude                 (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n",
    -       "  * longitude                (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * level                    (level) int64 16B 500 700\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    r                        (forecast_reference_time, step, levelist, latitude, longitude) float64 ...\n",
    -       "    t                        (forecast_reference_time, step, levelist, latitude, longitude) float64 ...\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
            "    class:        od\n",
            "    stream:       oper\n",
    @@ -467,34 +492,34 @@
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ - "\n", - "Dimensions: (forecast_reference_time: 4, step: 2, levelist: 2,\n", + " Size: 176kB\n", + "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", " latitude: 19, longitude: 36)\n", "Coordinates:\n", - " * forecast_reference_time (forecast_reference_time) datetime64[ns] 2024-06...\n", - " * step (step) timedelta64[ns] 00:00:00 06:00:00\n", - " * levelist (levelist) int64 500 700\n", - " * latitude (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n", - " * longitude (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * level (level) int64 16B 500 700\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " r (forecast_reference_time, step, levelist, latitude, longitude) float64 ...\n", - " t (forecast_reference_time, step, levelist, latitude, longitude) float64 ...\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", " class: od\n", " stream: oper\n", @@ -581,13 +606,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -632,7 +658,7 @@ ".xr-sections {\n", " padding-left: 0 !important;\n", " display: grid;\n", - " grid-template-columns: 150px auto auto 1fr 20px 20px;\n", + " grid-template-columns: 150px auto auto 1fr 0 20px 0 20px;\n", "}\n", "\n", ".xr-section-item {\n", @@ -640,7 +666,9 @@ "}\n", "\n", ".xr-section-item input {\n", - " display: none;\n", + " display: inline-block;\n", + " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -652,6 +680,10 @@ " color: var(--xr-font-color2);\n", "}\n", "\n", + ".xr-section-item input:focus + label {\n", + " border: 2px solid var(--xr-font-color0);\n", + "}\n", + "\n", ".xr-section-item input:enabled + label:hover {\n", " color: var(--xr-font-color0);\n", "}\n", @@ -673,7 +705,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -684,7 +716,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -756,15 +788,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -914,48 +946,48 @@ " stroke: currentColor;\n", " fill: currentColor;\n", "}\n", - "
    <xarray.Dataset>\n",
    +       "
    <xarray.Dataset> Size: 176kB\n",
            "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
            "                              latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
    -       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 2024-06...\n",
    -       "  * step                     (step) timedelta64[ns] 00:00:00 06:00:00\n",
    -       "  * level                    (level) int64 500 700\n",
    -       "  * latitude                 (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n",
    -       "  * longitude                (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * level                    (level) int64 16B 500 700\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 ...\n",
    -       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 ...\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ - "\n", + " Size: 176kB\n", "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", " latitude: 19, longitude: 36)\n", "Coordinates:\n", - " * forecast_reference_time (forecast_reference_time) datetime64[ns] 2024-06...\n", - " * step (step) timedelta64[ns] 00:00:00 06:00:00\n", - " * level (level) int64 500 700\n", - " * latitude (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n", - " * longitude (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * level (level) int64 16B 500 700\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " r (forecast_reference_time, step, level, latitude, longitude) float64 ...\n", - " t (forecast_reference_time, step, level, latitude, longitude) float64 ...\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", " Conventions: CF-1.8\n", " institution: ECMWF" @@ -1005,6 +1037,20 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "56b17d5515a14844859d8269f559d026", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "mixed_pl_ml.grib: 0%| | 0.00/176k [00:00 span {\n", @@ -1213,15 +1266,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -1371,18 +1424,18 @@ " stroke: currentColor;\n", " fill: currentColor;\n", "}\n", - "
    <xarray.Dataset>\n",
    +       "
    <xarray.Dataset> Size: 351kB\n",
            "Dimensions:                  (forecast_reference_time: 4, step: 2,\n",
            "                              level_and_type: 4, latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
    -       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 2024-06...\n",
    -       "  * step                     (step) timedelta64[ns] 00:00:00 06:00:00\n",
    -       "  * level_and_type           (level_and_type) <U5 '137ml' '500pl' '700pl' '90ml'\n",
    -       "  * latitude                 (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n",
    -       "  * longitude                (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * level_and_type           (level_and_type) <U5 80B '137ml' '500pl' ... '90ml'\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    t                        (forecast_reference_time, step, level_and_type, latitude, longitude) float64 ...\n",
    -       "    u                        (forecast_reference_time, step, level_and_type, latitude, longitude) float64 ...\n",
    +       "    t                        (forecast_reference_time, step, level_and_type, latitude, longitude) float64 175kB ...\n",
    +       "    u                        (forecast_reference_time, step, level_and_type, latitude, longitude) float64 175kB ...\n",
            "Attributes:\n",
            "    class:        od\n",
            "    stream:       oper\n",
    @@ -1394,34 +1447,34 @@
            "    domain:       g\n",
            "    levelist:     137\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    ml
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    levelist :
    137
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ - "\n", + " Size: 351kB\n", "Dimensions: (forecast_reference_time: 4, step: 2,\n", " level_and_type: 4, latitude: 19, longitude: 36)\n", "Coordinates:\n", - " * forecast_reference_time (forecast_reference_time) datetime64[ns] 2024-06...\n", - " * step (step) timedelta64[ns] 00:00:00 06:00:00\n", - " * level_and_type (level_and_type) span {\n", @@ -1697,15 +1771,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -1855,64 +1929,64 @@ " stroke: currentColor;\n", " fill: currentColor;\n", "}\n", - "
    <xarray.Dataset>\n",
    +       "
    <xarray.Dataset> Size: 1MB\n",
            "Dimensions:                  (number: 1, forecast_reference_time: 4, step: 2,\n",
            "                              surface: 1, latitude: 19, longitude: 36,\n",
            "                              isobaricInhPa: 6)\n",
            "Coordinates:\n",
    -       "  * number                   (number) int64 0\n",
    -       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 2024-06...\n",
    -       "  * step                     (step) timedelta64[ns] 00:00:00 06:00:00\n",
    -       "  * surface                  (surface) int64 0\n",
    -       "  * isobaricInhPa            (isobaricInhPa) int64 300 400 500 700 850 1000\n",
    -       "  * latitude                 (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n",
    -       "  * longitude                (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n",
    +       "  * number                   (number) int64 8B 0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * surface                  (surface) int64 8B 0\n",
    +       "  * isobaricInhPa            (isobaricInhPa) int64 48B 300 400 500 700 850 1000\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    2t                       (number, forecast_reference_time, step, surface, latitude, longitude) float64 ...\n",
    -       "    msl                      (number, forecast_reference_time, step, surface, latitude, longitude) float64 ...\n",
    -       "    r                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n",
    -       "    t                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n",
    -       "    u                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n",
    -       "    v                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n",
    -       "    z                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n",
    +       "    2t                       (number, forecast_reference_time, step, surface, latitude, longitude) float64 44kB ...\n",
    +       "    msl                      (number, forecast_reference_time, step, surface, latitude, longitude) float64 44kB ...\n",
    +       "    r                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n",
    +       "    t                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n",
    +       "    u                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n",
    +       "    v                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n",
    +       "    z                        (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n",
            "Attributes:\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ - "\n", + " Size: 1MB\n", "Dimensions: (number: 1, forecast_reference_time: 4, step: 2,\n", " surface: 1, latitude: 19, longitude: 36,\n", " isobaricInhPa: 6)\n", "Coordinates:\n", - " * number (number) int64 0\n", - " * forecast_reference_time (forecast_reference_time) datetime64[ns] 2024-06...\n", - " * step (step) timedelta64[ns] 00:00:00 06:00:00\n", - " * surface (surface) int64 0\n", - " * isobaricInhPa (isobaricInhPa) int64 300 400 500 700 850 1000\n", - " * latitude (latitude) float64 90.0 80.0 70.0 ... -80.0 -90.0\n", - " * longitude (longitude) float64 0.0 10.0 20.0 ... 340.0 350.0\n", + " * number (number) int64 8B 0\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * surface (surface) int64 8B 0\n", + " * isobaricInhPa (isobaricInhPa) int64 48B 300 400 500 700 850 1000\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " 2t (number, forecast_reference_time, step, surface, latitude, longitude) float64 ...\n", - " msl (number, forecast_reference_time, step, surface, latitude, longitude) float64 ...\n", - " r (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n", - " t (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n", - " u (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n", - " v (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n", - " z (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 ...\n", + " 2t (number, forecast_reference_time, step, surface, latitude, longitude) float64 44kB ...\n", + " msl (number, forecast_reference_time, step, surface, latitude, longitude) float64 44kB ...\n", + " r (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n", + " t (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n", + " u (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n", + " v (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n", + " z (number, forecast_reference_time, step, isobaricInhPa, latitude, longitude) float64 263kB ...\n", "Attributes:\n", " Conventions: CF-1.8\n", " institution: ECMWF" @@ -1946,9 +2020,9 @@ ], "metadata": { "kernelspec": { - "display_name": "dev_ecc", + "display_name": "dev", "language": "python", - "name": "dev_ecc" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -1960,7 +2034,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/docs/examples/xarray_engine_overview.ipynb b/docs/examples/xarray_engine_overview.ipynb index 0d1fe3615..0d256cdb5 100644 --- a/docs/examples/xarray_engine_overview.ipynb +++ b/docs/examples/xarray_engine_overview.ipynb @@ -57,7 +57,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "", + "model_id": "8b2a598b3f264e2aa75a0cddab1650d2", "version_major": 2, "version_minor": 0 }, @@ -115,13 +115,6 @@ "tags": [] }, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "From version 0.11.0 the default engine for to_xarray is 'earthkit'. Use engine=`cfgrib` to invoke the cfgrib engine.\n" - ] - }, { "data": { "text/html": [ @@ -155,14 +148,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -217,6 +210,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -253,7 +247,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -264,7 +258,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -336,15 +330,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -495,17 +489,17 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 176kB\n",
    -       "Dimensions:                  (forecast_reference_time: 4, step: 2, levelist: 2,\n",
    +       "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
            "                              latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
            "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
            "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    -       "  * levelist                 (levelist) int64 16B 500 700\n",
    +       "  * level                    (level) int64 16B 500 700\n",
            "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
            "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    r                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    -       "    t                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
            "    class:        od\n",
            "    stream:       oper\n",
    @@ -517,34 +511,34 @@
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", - "Dimensions: (forecast_reference_time: 4, step: 2, levelist: 2,\n", + "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", " latitude: 19, longitude: 36)\n", "Coordinates:\n", " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " r (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", - " t (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", " class: od\n", " stream: oper\n", @@ -820,7 +814,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "5ad32a3e-2f48-49f5-b207-15e89c397fba", "metadata": { "editable": true, @@ -833,17 +827,18 @@ { "data": { "text/plain": [ - "255.25649845948692" + "(254.25649845948692, 255.25649845948692)" ] }, - "execution_count": 5, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "ds_fl.sel(param=\"t\", step=6, level=500)[0].values.mean(), \n", - "ds_fl1.sel(param=\"t\", step=6, level=500)[0].values.mean()" + "m_0 = ds_fl.sel(param=\"t\", step=6, level=500)[0].values.mean() \n", + "m_1 = ds_fl1.sel(param=\"t\", step=6, level=500)[0].values.mean()\n", + "m_0, m_1" ] }, { @@ -1189,9 +1184,9 @@ ], "metadata": { "kernelspec": { - "display_name": "dev_ecc", + "display_name": "dev", "language": "python", - "name": "dev_ecc" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -1203,7 +1198,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/docs/examples/xarray_engine_seasonal.ipynb b/docs/examples/xarray_engine_seasonal.ipynb index 2c8c55528..52a0a0b1d 100644 --- a/docs/examples/xarray_engine_seasonal.ipynb +++ b/docs/examples/xarray_engine_seasonal.ipynb @@ -1,700 +1,1204 @@ { - "cells": [ - { - "cell_type": "markdown", - "id": "55f1f7bf-9589-4a43-b246-7c4c7880fa2d", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" + "cells": [ + { + "cell_type": "markdown", + "id": "55f1f7bf-9589-4a43-b246-7c4c7880fa2d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Xarray engine: seasonal forecast" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "7e0be52c-bedb-4ae7-984c-4807bf253d7f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "653a95e071ca4633aadbe42f597676a9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "seasonal_monthly.grib: 0%| | 0.00/160k [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    centreshortNametypeOfLevelleveldataDatedataTimestepRangedataTypenumbergridTypeforecastMonth
    0lfpw2tsurface0199310010744fcmean0regular_ll1
    1lfpw2tsurface0199310010744fcmean1regular_ll1
    2lfpw2tsurface0199310010744fcmean2regular_ll1
    3lfpw2tsurface01993100101464fcmean0regular_ll2
    \n", + "" + ], + "text/plain": [ + " centre shortName typeOfLevel level dataDate dataTime stepRange dataType \\\n", + "0 lfpw 2t surface 0 19931001 0 744 fcmean \n", + "1 lfpw 2t surface 0 19931001 0 744 fcmean \n", + "2 lfpw 2t surface 0 19931001 0 744 fcmean \n", + "3 lfpw 2t surface 0 19931001 0 1464 fcmean \n", + "\n", + " number gridType forecastMonth \n", + "0 0 regular_ll 1 \n", + "1 1 regular_ll 1 \n", + "2 2 regular_ll 1 \n", + "3 0 regular_ll 2 " + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_fl[0:4].ls(extra_keys=\"forecastMonth\")" + ] + }, + { + "cell_type": "raw", + "id": "665fba14-79d5-4344-84fb-2e16da77936d", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "In order to use ``forecastMonth`` instead of ``step`` we need to use the ``dim_roles`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c500c39a-8cdf-4e25-950e-581924879e6c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 395kB\n",
    +                            "Dimensions:                  (number: 3, forecast_reference_time: 4, step: 6,\n",
    +                            "                              latitude: 19, longitude: 36)\n",
    +                            "Coordinates:\n",
    +                            "  * number                   (number) int64 24B 0 1 2\n",
    +                            "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 199...\n",
    +                            "  * step                     (step) int64 48B 1 2 3 4 5 6\n",
    +                            "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +                            "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    +                            "Data variables:\n",
    +                            "    2t                       (number, forecast_reference_time, step, latitude, longitude) float64 394kB ...\n",
    +                            "Attributes: (12/15)\n",
    +                            "    param:        2t\n",
    +                            "    paramId:      167\n",
    +                            "    class:        c3\n",
    +                            "    stream:       msmm\n",
    +                            "    levtype:      sfc\n",
    +                            "    type:         fcmean\n",
    +                            "    ...           ...\n",
    +                            "    fcmonth:      1\n",
    +                            "    origin:       lfpw\n",
    +                            "    domain:       g\n",
    +                            "    method:       1\n",
    +                            "    Conventions:  CF-1.8\n",
    +                            "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 395kB\n", + "Dimensions: (number: 3, forecast_reference_time: 4, step: 6,\n", + " latitude: 19, longitude: 36)\n", + "Coordinates:\n", + " * number (number) int64 24B 0 1 2\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 199...\n", + " * step (step) int64 48B 1 2 3 4 5 6\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", + "Data variables:\n", + " 2t (number, forecast_reference_time, step, latitude, longitude) float64 394kB ...\n", + "Attributes: (12/15)\n", + " param: 2t\n", + " paramId: 167\n", + " class: c3\n", + " stream: msmm\n", + " levtype: sfc\n", + " type: fcmean\n", + " ... ...\n", + " fcmonth: 1\n", + " origin: lfpw\n", + " domain: g\n", + " method: 1\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = ds_fl.to_xarray(time_dim_mode=\"forecast\", \n", + " dim_roles={\"step\": \"forecastMonth\"})\n", + "ds" + ] + }, + { + "cell_type": "markdown", + "id": "e917dbc1-ba05-4180-b1d8-62e04bf98d50", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "When we check the \"step\" dimension we can see its units are \"months\"." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "850836de-db60-48ac-b42b-253ab335ceef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Size: 48B\n", + "array([1, 2, 3, 4, 5, 6])\n", + "Coordinates:\n", + " * step (step) int64 48B 1 2 3 4 5 6\n", + "Attributes:\n", + " units: months\n" + ] + } + ], + "source": [ + "print(ds[\"step\"])" + ] + }, + { + "cell_type": "raw", + "id": "13b0fdb1-ed1f-4a0a-b77f-71115adf40ad", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "By default, the dimensions related to dimension roles are named after the roles. So, although the step dimension was generated from the \"forecastMonth\" GRIB key the dimension name is still \"step\". To override this use the ``keep_dim_role_name=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cbf6d822-546e-42ab-a8c7-ad8d7d0c61fc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 395kB\n",
    +                            "Dimensions:                  (number: 3, forecast_reference_time: 4,\n",
    +                            "                              forecastMonth: 6, latitude: 19, longitude: 36)\n",
    +                            "Coordinates:\n",
    +                            "  * number                   (number) int64 24B 0 1 2\n",
    +                            "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 199...\n",
    +                            "  * forecastMonth            (forecastMonth) int64 48B 1 2 3 4 5 6\n",
    +                            "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +                            "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    +                            "Data variables:\n",
    +                            "    2t                       (number, forecast_reference_time, forecastMonth, latitude, longitude) float64 394kB ...\n",
    +                            "Attributes: (12/15)\n",
    +                            "    param:        2t\n",
    +                            "    paramId:      167\n",
    +                            "    class:        c3\n",
    +                            "    stream:       msmm\n",
    +                            "    levtype:      sfc\n",
    +                            "    type:         fcmean\n",
    +                            "    ...           ...\n",
    +                            "    fcmonth:      1\n",
    +                            "    origin:       lfpw\n",
    +                            "    domain:       g\n",
    +                            "    method:       1\n",
    +                            "    Conventions:  CF-1.8\n",
    +                            "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 395kB\n", + "Dimensions: (number: 3, forecast_reference_time: 4,\n", + " forecastMonth: 6, latitude: 19, longitude: 36)\n", + "Coordinates:\n", + " * number (number) int64 24B 0 1 2\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 199...\n", + " * forecastMonth (forecastMonth) int64 48B 1 2 3 4 5 6\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", + "Data variables:\n", + " 2t (number, forecast_reference_time, forecastMonth, latitude, longitude) float64 394kB ...\n", + "Attributes: (12/15)\n", + " param: 2t\n", + " paramId: 167\n", + " class: c3\n", + " stream: msmm\n", + " levtype: sfc\n", + " type: fcmean\n", + " ... ...\n", + " fcmonth: 1\n", + " origin: lfpw\n", + " domain: g\n", + " method: 1\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = ds_fl.to_xarray(time_dim_mode=\"forecast\", \n", + " dim_roles={\"step\": \"forecastMonth\"}, \n", + " dim_name_from_role_name=False)\n", + "ds" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } }, - "tags": [] - }, - "source": [ - "## Xarray engine: seasonal forecast" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "7e0be52c-bedb-4ae7-984c-4807bf253d7f", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [ - "import earthkit.data as ekd\n", - "\n", - "ds_fl = ekd.from_source(\"sample\", \"seasonal_monthly.grib\")" - ] - }, - { - "cell_type": "markdown", - "id": "918dec31-6135-458d-a243-7ccd3f5ca3ff", - "metadata": { - "editable": true, - "raw_mimetype": "text/restructuredtext", - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "The input data contains seasonal monthly forecast. Because the length of a month varies, for this data the ``forecastMonth`` key is better suited for describing the temporal structure than using the ``step*`` keys. \n", - "\n", - "This is how the first few GRIB messages look like:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "78ebb588-85a8-4a67-8f6b-046a536a508a", - "metadata": { - "editable": true, - "scrolled": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/html": [ - "
    \n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
    centreshortNametypeOfLevelleveldataDatedataTimestepRangedataTypenumbergridTypeforecastMonth
    0lfpw2tsurface0199310010744fcmean0regular_ll1
    1lfpw2tsurface0199310010744fcmean1regular_ll1
    2lfpw2tsurface0199310010744fcmean2regular_ll1
    3lfpw2tsurface01993100101464fcmean0regular_ll2
    \n", - "
    " - ], - "text/plain": [ - " centre shortName typeOfLevel level dataDate dataTime stepRange dataType \\\n", - "0 lfpw 2t surface 0 19931001 0 744 fcmean \n", - "1 lfpw 2t surface 0 19931001 0 744 fcmean \n", - "2 lfpw 2t surface 0 19931001 0 744 fcmean \n", - "3 lfpw 2t surface 0 19931001 0 1464 fcmean \n", - "\n", - " number gridType forecastMonth \n", - "0 0 regular_ll 1 \n", - "1 1 regular_ll 1 \n", - "2 2 regular_ll 1 \n", - "3 0 regular_ll 2 " - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ds_fl[0:4].ls(extra_keys=\"forecastMonth\")" - ] - }, - { - "cell_type": "raw", - "id": "665fba14-79d5-4344-84fb-2e16da77936d", - "metadata": { - "editable": true, - "raw_mimetype": "text/restructuredtext", - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "In order to use ``forecastMonth`` instead of ``step`` we need to use the ``dim_roles`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c500c39a-8cdf-4e25-950e-581924879e6c", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "From version 0.11.0 the default engine for to_xarray is 'earthkit'. Use engine=`cfgrib` to invoke the cfgrib engine.\n" - ] - }, - { - "data": { - "text/html": [ - "
    \n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
    <xarray.Dataset> Size: 395kB\n",
    -       "Dimensions:                  (number: 3, forecast_reference_time: 4,\n",
    -       "                              forecastMonth: 6, latitude: 19, longitude: 36)\n",
    -       "Coordinates:\n",
    -       "  * number                   (number) int64 24B 0 1 2\n",
    -       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 199...\n",
    -       "  * forecastMonth            (forecastMonth) int64 48B 1 2 3 4 5 6\n",
    -       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    -       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    -       "Data variables:\n",
    -       "    2t                       (number, forecast_reference_time, forecastMonth, latitude, longitude) float64 394kB ...\n",
    -       "Attributes: (12/17)\n",
    -       "    param:          2t\n",
    -       "    standard_name:  unknown\n",
    -       "    long_name:      2 metre temperature\n",
    -       "    paramId:        167\n",
    -       "    class:          c3\n",
    -       "    stream:         msmm\n",
    -       "    ...             ...\n",
    -       "    fcmonth:        1\n",
    -       "    origin:         lfpw\n",
    -       "    domain:         g\n",
    -       "    method:         1\n",
    -       "    Conventions:    CF-1.8\n",
    -       "    institution:    ECMWF
    " - ], - "text/plain": [ - " Size: 395kB\n", - "Dimensions: (number: 3, forecast_reference_time: 4,\n", - " forecastMonth: 6, latitude: 19, longitude: 36)\n", - "Coordinates:\n", - " * number (number) int64 24B 0 1 2\n", - " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 199...\n", - " * forecastMonth (forecastMonth) int64 48B 1 2 3 4 5 6\n", - " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", - " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", - "Data variables:\n", - " 2t (number, forecast_reference_time, forecastMonth, latitude, longitude) float64 394kB ...\n", - "Attributes: (12/17)\n", - " param: 2t\n", - " standard_name: unknown\n", - " long_name: 2 metre temperature\n", - " paramId: 167\n", - " class: c3\n", - " stream: msmm\n", - " ... ...\n", - " fcmonth: 1\n", - " origin: lfpw\n", - " domain: g\n", - " method: 1\n", - " Conventions: CF-1.8\n", - " institution: ECMWF" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ds = ds_fl.to_xarray(time_dim_mode=\"forecast\", dim_roles={\"step\": \"forecastMonth\"})\n", - "ds" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6eba63a3-452b-4317-aa7d-7793e4a1dd2a", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dev_ecc", - "language": "python", - "name": "dev_ecc" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 + "nbformat": 4, + "nbformat_minor": 5 } diff --git a/docs/examples/xarray_engine_split.ipynb b/docs/examples/xarray_engine_split.ipynb index 6c77736b0..787e734e3 100644 --- a/docs/examples/xarray_engine_split.ipynb +++ b/docs/examples/xarray_engine_split.ipynb @@ -44,7 +44,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8e0c9b139cf64468a0126a7780bf54e0", + "model_id": "6c861eb4640740fab0436a6893174a95", "version_major": 2, "version_minor": 0 }, @@ -59,7 +59,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Dimension 'typeOfLevel' of variable 't' cannot have multiple values=['hybrid', 'isobaricInhPa']\n" + "Dimension 'level_type' of variable 't' cannot have multiple values=['hybrid', 'isobaricInhPa']\n" ] } ], @@ -84,7 +84,7 @@ "tags": [] }, "source": [ - "In this case we can use the ``split_dims`` option to split the hypercube along the problematic dimensions. The results a tuple of two lists: \n", + "In this case we can use the ``split_dims`` option to split the hypercube along the problematic dimensions. `split_dims`` does not use dimension names but takes a single or multiple GRIB keys to perform the splitting on. The results a tuple of two lists: \n", "\n", "- the first list contains the Xarray datasets\n", "- the second list contains the corresponding dictionaries with the spitting keys/values (one dictionary per dataset)\n", @@ -527,20 +527,20 @@ " u (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", diff --git a/docs/examples/xarray_engine_squeeze.ipynb b/docs/examples/xarray_engine_squeeze.ipynb new file mode 100644 index 000000000..c32360896 --- /dev/null +++ b/docs/examples/xarray_engine_squeeze.ipynb @@ -0,0 +1,1497 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c2feafcc-430b-4718-983f-554e55dcd54a", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Xarray engine: sqeezing dimensions" + ] + }, + { + "cell_type": "markdown", + "id": "f1b37637-7cce-4af5-8bad-1ddb6492d732", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "First, we get some GRIB forecast data on pressure levels and read it into a GRIB fieldlist." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1a6e355d-3fbf-4d92-b32f-a9d7e770f9db", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fbbb4422431d4d75aad6e3a4bd7d20d4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "pl.grib: 0%| | 0.00/48.8k [00:00\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 176kB\n",
    +       "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
    +       "                              latitude: 19, longitude: 36)\n",
    +       "Coordinates:\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * level                    (level) int64 16B 500 700\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    +       "Data variables:\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "Attributes:\n",
    +       "    class:        od\n",
    +       "    stream:       oper\n",
    +       "    levtype:      pl\n",
    +       "    type:         fc\n",
    +       "    expver:       0001\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    number:       0\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 176kB\n", + "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", + " latitude: 19, longitude: 36)\n", + "Coordinates:\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * level (level) int64 16B 500 700\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", + "Data variables:\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + "Attributes:\n", + " class: od\n", + " stream: oper\n", + " levtype: pl\n", + " type: fc\n", + " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", + " domain: g\n", + " number: 0\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_fl.to_xarray()" + ] + }, + { + "cell_type": "markdown", + "id": "5e22a7d6-8f86-454a-b7e7-fca4273cb493", + "metadata": {}, + "source": [ + "When using ``squeeze=True`` these dimension are added to the dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e19e154e-89ac-4d5c-a82c-bd6227bc94f6", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 176kB\n",
    +       "Dimensions:                  (number: 1, forecast_reference_time: 4, step: 2,\n",
    +       "                              level: 2, level_type: 1, latitude: 19,\n",
    +       "                              longitude: 36)\n",
    +       "Coordinates:\n",
    +       "  * number                   (number) int64 8B 0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * level                    (level) int64 16B 500 700\n",
    +       "  * level_type               (level_type) <U2 8B 'pl'\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    +       "Data variables:\n",
    +       "    r                        (number, forecast_reference_time, step, level, level_type, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (number, forecast_reference_time, step, level, level_type, latitude, longitude) float64 88kB ...\n",
    +       "Attributes:\n",
    +       "    class:        od\n",
    +       "    stream:       oper\n",
    +       "    type:         fc\n",
    +       "    expver:       0001\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 176kB\n", + "Dimensions: (number: 1, forecast_reference_time: 4, step: 2,\n", + " level: 2, level_type: 1, latitude: 19,\n", + " longitude: 36)\n", + "Coordinates:\n", + " * number (number) int64 8B 0\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * level (level) int64 16B 500 700\n", + " * level_type (level_type) \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 176kB\n",
    +       "Dimensions:                  (number: 1, forecast_reference_time: 4, step: 2,\n",
    +       "                              level: 2, latitude: 19, longitude: 36)\n",
    +       "Coordinates:\n",
    +       "  * number                   (number) int64 8B 0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * level                    (level) int64 16B 500 700\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    +       "Data variables:\n",
    +       "    r                        (number, forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (number, forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "Attributes:\n",
    +       "    class:        od\n",
    +       "    stream:       oper\n",
    +       "    levtype:      pl\n",
    +       "    type:         fc\n",
    +       "    expver:       0001\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 176kB\n", + "Dimensions: (number: 1, forecast_reference_time: 4, step: 2,\n", + " level: 2, latitude: 19, longitude: 36)\n", + "Coordinates:\n", + " * number (number) int64 8B 0\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * level (level) int64 16B 500 700\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", + "Data variables:\n", + " r (number, forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (number, forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + "Attributes:\n", + " class: od\n", + " stream: oper\n", + " levtype: pl\n", + " type: fc\n", + " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", + " domain: g\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds_fl.to_xarray(ensure_dims=[\"number\", \"level_type\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6937b30-029a-4b2a-8391-ed4f5ac8eeae", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/xarray_engine_step_ranges.ipynb b/docs/examples/xarray_engine_step_ranges.ipynb new file mode 100644 index 000000000..cef1aa2ba --- /dev/null +++ b/docs/examples/xarray_engine_step_ranges.ipynb @@ -0,0 +1,671 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "415a2c2a-8d00-48fc-9a02-6fc79aac663f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Xarray engine: step range" + ] + }, + { + "cell_type": "markdown", + "id": "b8e40382-ef81-46d7-8c94-2a01bd3a5214", + "metadata": {}, + "source": [ + "Get input GRIB2 data containing precipitation forecast for step ranges." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "85d04283-3488-477f-90dd-ee27f0a91935", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "936ac87a643d414781370a22f10a9904", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "lsp_step_range.grib2: 0%| | 0.00/1.17k [00:00\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
    paramstepstepRangestartStependStep
    0lsp71-7271-727172
    1lsp72-7372-737273
    \n", + "" + ], + "text/plain": [ + " param step stepRange startStep endStep\n", + "0 lsp 71-72 71-72 71 72\n", + "1 lsp 72-73 72-73 72 73" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import earthkit.data as ekd\n", + "ds_fl = ekd.from_source(\"sample\", \"lsp_step_range.grib2\")\n", + "ds_fl.ls(keys=[\"param\", \"step\", \"stepRange\", \"startStep\", \"endStep\"])" + ] + }, + { + "cell_type": "raw", + "id": "b2fab96a-8435-4ed3-b43d-e7f5dcc27141", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "When we convert GRIB data to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` the step dimension is defined by the \"step\" :ref:`dimension role `. By default, this role is using the \"step_timedelta\" generated metadata key that is the timedelta representation of the \"endStep\" GRIB key." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "5b6872c9-97b6-4336-89ba-4e6491605f90", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 2kB\n",
    +       "Dimensions:    (step: 2, latitude: 7, longitude: 12)\n",
    +       "Coordinates:\n",
    +       "  * step       (step) timedelta64[ns] 16B 3 days 3 days 01:00:00\n",
    +       "  * latitude   (latitude) float64 56B 90.0 60.0 30.0 0.0 -30.0 -60.0 -90.0\n",
    +       "  * longitude  (longitude) float64 96B 0.0 30.0 60.0 90.0 ... 270.0 300.0 330.0\n",
    +       "Data variables:\n",
    +       "    lsp        (step, latitude, longitude) float64 1kB ...\n",
    +       "Attributes:\n",
    +       "    param:        lsp\n",
    +       "    paramId:      142\n",
    +       "    class:        d1\n",
    +       "    stream:       oper\n",
    +       "    levtype:      sfc\n",
    +       "    type:         fc\n",
    +       "    expver:       0001\n",
    +       "    date:         20250527\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 2kB\n", + "Dimensions: (step: 2, latitude: 7, longitude: 12)\n", + "Coordinates:\n", + " * step (step) timedelta64[ns] 16B 3 days 3 days 01:00:00\n", + " * latitude (latitude) float64 56B 90.0 60.0 30.0 0.0 -30.0 -60.0 -90.0\n", + " * longitude (longitude) float64 96B 0.0 30.0 60.0 90.0 ... 270.0 300.0 330.0\n", + "Data variables:\n", + " lsp (step, latitude, longitude) float64 1kB ...\n", + "Attributes:\n", + " param: lsp\n", + " paramId: 142\n", + " class: d1\n", + " stream: oper\n", + " levtype: sfc\n", + " type: fc\n", + " expver: 0001\n", + " date: 20250527\n", + " time: 0\n", + " domain: g\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = ds_fl.to_xarray()\n", + "ds" + ] + }, + { + "cell_type": "markdown", + "id": "5e0f85a6-30bd-4dfe-ae98-b9c13304a465", + "metadata": {}, + "source": [ + "We can check the \"step\" coordinate in the dataset to see that it matches the \"endStep\" values." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f9a4b868-29dd-4bb1-bbaa-18caa68f405e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[72, 73]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# convert to hours from ns\n", + "[int(x* 1E-9/(3600)) for x in ds[\"step\"].values]" + ] + }, + { + "cell_type": "markdown", + "id": "542a047c-39d8-4ec1-9194-bb362e9de4f7", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "This default behaviour can be overridden by specifying custom ``dim_roles``. E.g. to get the step from the \"startStep\" key we can use:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5ea06ff7-70c6-4967-80ce-7b7b6fa12fa5", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[71, 72]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds = ds_fl.to_xarray(dim_roles={\"step\": \"startStep\"})\n", + "[int(x* 1E-9/(3600)) for x in ds[\"step\"].values]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "495387d6-331c-4dc5-90fa-e05a6da9b998", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/xarray_engine_temporal.ipynb b/docs/examples/xarray_engine_temporal.ipynb index 1b1a76d57..b3c8589e7 100644 --- a/docs/examples/xarray_engine_temporal.ipynb +++ b/docs/examples/xarray_engine_temporal.ipynb @@ -54,7 +54,22 @@ }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8dca45f059a04a48898e26443d3b1a64", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "pl.grib: 0%| | 0.00/48.8k [00:00`)." ] }, { @@ -114,7 +138,7 @@ "tags": [] }, "source": [ - "When ``time_dim_mode=\"raw\"`` the \"date\", \"time\" and \"step\" ecCodes GRIB keys are used to form the temporal dimensions." + "When ``time_dim_mode=\"raw\"`` the \"date\", \"time\" and \"step\" roles are used to form the temporal dimensions." ] }, { @@ -162,14 +186,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -224,6 +248,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -260,7 +285,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -271,7 +296,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -343,15 +368,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -502,20 +527,18 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 176kB\n",
    -       "Dimensions:    (date: 2, time: 2, step: 2, levelist: 2, latitude: 19,\n",
    -       "                longitude: 36)\n",
    +       "Dimensions:    (date: 2, time: 2, step: 2, level: 2, latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
            "  * date       (date) datetime64[ns] 16B 2024-06-03 2024-06-04\n",
            "  * time       (time) timedelta64[ns] 16B 00:00:00 12:00:00\n",
            "  * step       (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    -       "  * levelist   (levelist) int64 16B 500 700\n",
    +       "  * level      (level) int64 16B 500 700\n",
            "  * latitude   (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n",
            "  * longitude  (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0\n",
            "Data variables:\n",
    -       "    r          (date, time, step, levelist, latitude, longitude) float64 88kB ...\n",
    -       "    t          (date, time, step, levelist, latitude, longitude) float64 88kB ...\n",
    +       "    r          (date, time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t          (date, time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
    -       "    param:        t\n",
            "    class:        od\n",
            "    stream:       oper\n",
            "    levtype:      pl\n",
    @@ -524,34 +547,32 @@
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", - "Dimensions: (date: 2, time: 2, step: 2, levelist: 2, latitude: 19,\n", - " longitude: 36)\n", + "Dimensions: (date: 2, time: 2, step: 2, level: 2, latitude: 19, longitude: 36)\n", "Coordinates:\n", " * date (date) datetime64[ns] 16B 2024-06-03 2024-06-04\n", " * time (time) timedelta64[ns] 16B 00:00:00 12:00:00\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0\n", "Data variables:\n", - " r (date, time, step, levelist, latitude, longitude) float64 88kB ...\n", - " t (date, time, step, levelist, latitude, longitude) float64 88kB ...\n", + " r (date, time, step, level, latitude, longitude) float64 88kB ...\n", + " t (date, time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", @@ -599,7 +620,7 @@ "tags": [] }, "source": [ - "When ``time_dim_mode=\"forecast\"`` the \"date\" and \"time\" ecCodes GRIB keys are merged to form the dimension \"forecats_reference_time\". It also adds the \"step\" dimension based on the \"step\" key." + "When ``time_dim_mode=\"forecast\"`` the \"date\" and \"time\" roles are merged to form the dimension \"forecats_reference_time\". It also adds the \"step\" dimension." ] }, { @@ -647,14 +668,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -709,6 +730,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -745,7 +767,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -756,7 +778,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -828,15 +850,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -987,62 +1009,64 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 176kB\n",
    -       "Dimensions:                  (forecast_reference_time: 4, step: 2, levelist: 2,\n",
    +       "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
            "                              latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
            "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
            "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    -       "  * levelist                 (levelist) int64 16B 500 700\n",
    +       "  * level                    (level) int64 16B 500 700\n",
            "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
            "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    r                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    -       "    t                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
    -       "    param:        t\n",
            "    class:        od\n",
            "    stream:       oper\n",
            "    levtype:      pl\n",
            "    type:         fc\n",
            "    expver:       0001\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", - "Dimensions: (forecast_reference_time: 4, step: 2, levelist: 2,\n", + "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", " latitude: 19, longitude: 36)\n", "Coordinates:\n", " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " r (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", - " t (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", " Conventions: CF-1.8\n", @@ -1085,7 +1109,7 @@ "tags": [] }, "source": [ - "When ``time_dim_mode=\"valid_time\"`` the only temporal dimension is \"valid_time\". It is built from the values of the \"validityDate\" and \"validityTime\" ecCodes GRIB keys. This dimension can only be generated if each GRIB field has a distinct valid time, so it typically fits for analysis/climate data." + "When ``time_dim_mode=\"valid_time\"`` the only temporal dimension is \"valid_time\". By default, it is built from the values of the \"validityDate\" and \"validityTime\" ecCodes GRIB keys. This dimension can only be generated if each GRIB field has a distinct valid time, so it typically fits for analysis/climate data." ] }, { @@ -1103,7 +1127,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "", + "model_id": "98fb8660778641739b5bdba816ad80ef", "version_major": 2, "version_minor": 0 }, @@ -1147,14 +1171,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -1209,6 +1233,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -1245,7 +1270,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -1256,7 +1281,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -1328,15 +1353,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -1494,27 +1519,30 @@ " * longitude (longitude) float64 96B -70.0 -60.0 -50.0 ... 20.0 30.0 40.0\n", "Data variables:\n", " msl (valid_time, latitude, longitude) float64 5kB ...\n", - "Attributes:\n", + "Attributes: (12/13)\n", " param: msl\n", + " paramId: 151\n", " class: od\n", " stream: oper\n", " levtype: sfc\n", " type: an\n", - " expver: 0001\n", + " ... ...\n", + " date: 20160925\n", + " time: 0\n", " domain: g\n", " number: 0\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • param :
    msl
    paramId :
    151
    class :
    od
    stream :
    oper
    levtype :
    sfc
    type :
    an
    expver :
    0001
    date :
    20160925
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 6kB\n", @@ -1525,13 +1553,16 @@ " * longitude (longitude) float64 96B -70.0 -60.0 -50.0 ... 20.0 30.0 40.0\n", "Data variables:\n", " msl (valid_time, latitude, longitude) float64 5kB ...\n", - "Attributes:\n", + "Attributes: (12/13)\n", " param: msl\n", + " paramId: 151\n", " class: od\n", " stream: oper\n", " levtype: sfc\n", " type: an\n", - " expver: 0001\n", + " ... ...\n", + " date: 20160925\n", + " time: 0\n", " domain: g\n", " number: 0\n", " Conventions: CF-1.8\n", @@ -1560,7 +1591,7 @@ "tags": [] }, "source": [ - "This mode can also be used for suitable forecasts. To use it for the original forecast data first we need to filter it." + "This mode can also be used for suitable forecasts data. To use it for the original forecast data first we need to filter it." ] }, { @@ -1608,14 +1639,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -1670,6 +1701,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -1706,7 +1738,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -1717,7 +1749,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -1789,15 +1821,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -1948,17 +1980,16 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 44kB\n",
    -       "Dimensions:     (valid_time: 2, levelist: 2, latitude: 19, longitude: 36)\n",
    +       "Dimensions:     (valid_time: 2, level: 2, latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
            "  * valid_time  (valid_time) datetime64[ns] 16B 2024-06-03 2024-06-03T06:00:00\n",
    -       "  * levelist    (levelist) int64 16B 500 700\n",
    +       "  * level       (level) int64 16B 500 700\n",
            "  * latitude    (latitude) float64 152B 90.0 80.0 70.0 ... -70.0 -80.0 -90.0\n",
            "  * longitude   (longitude) float64 288B 0.0 10.0 20.0 ... 330.0 340.0 350.0\n",
            "Data variables:\n",
    -       "    r           (valid_time, levelist, latitude, longitude) float64 22kB ...\n",
    -       "    t           (valid_time, levelist, latitude, longitude) float64 22kB ...\n",
    +       "    r           (valid_time, level, latitude, longitude) float64 22kB ...\n",
    +       "    t           (valid_time, level, latitude, longitude) float64 22kB ...\n",
            "Attributes:\n",
    -       "    param:        t\n",
            "    class:        od\n",
            "    stream:       oper\n",
            "    levtype:      pl\n",
    @@ -1969,31 +2000,30 @@
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 44kB\n", - "Dimensions: (valid_time: 2, levelist: 2, latitude: 19, longitude: 36)\n", + "Dimensions: (valid_time: 2, level: 2, latitude: 19, longitude: 36)\n", "Coordinates:\n", " * valid_time (valid_time) datetime64[ns] 16B 2024-06-03 2024-06-03T06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 70.0 ... -70.0 -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 20.0 ... 330.0 340.0 350.0\n", "Data variables:\n", - " r (valid_time, levelist, latitude, longitude) float64 22kB ...\n", - " t (valid_time, levelist, latitude, longitude) float64 22kB ...\n", + " r (valid_time, level, latitude, longitude) float64 22kB ...\n", + " t (valid_time, level, latitude, longitude) float64 22kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", @@ -2042,7 +2072,7 @@ "tags": [] }, "source": [ - "When ``add_valid_time_dim=True`` it adds coord `valid_time` containing the valid times for all the different temporal dimensions as datetime64. When ``time_dim_mode=\"valid_time\"`` this coordinate is always added irrespectively of the value of ``add_valid_time_dim``." + "When ``add_valid_time_dim=True`` it adds the coordine`valid_time` containing the valid times for all the different temporal dimensions as datetime64. When ``time_dim_mode=\"valid_time\"`` this coordinate is always added irrespective of the value of ``add_valid_time_dim``." ] }, { @@ -2090,14 +2120,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -2152,6 +2182,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -2188,7 +2219,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -2199,7 +2230,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -2271,15 +2302,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -2430,21 +2461,20 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 176kB\n",
    -       "Dimensions:     (date: 2, time: 2, step: 2, levelist: 2, latitude: 19,\n",
    +       "Dimensions:     (date: 2, time: 2, step: 2, level: 2, latitude: 19,\n",
            "                 longitude: 36)\n",
            "Coordinates:\n",
            "  * date        (date) datetime64[ns] 16B 2024-06-03 2024-06-04\n",
            "  * time        (time) timedelta64[ns] 16B 00:00:00 12:00:00\n",
            "  * step        (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    -       "  * levelist    (levelist) int64 16B 500 700\n",
    +       "  * level       (level) int64 16B 500 700\n",
            "    valid_time  (date, time, step) datetime64[ns] 64B ...\n",
            "  * latitude    (latitude) float64 152B 90.0 80.0 70.0 ... -70.0 -80.0 -90.0\n",
            "  * longitude   (longitude) float64 288B 0.0 10.0 20.0 ... 330.0 340.0 350.0\n",
            "Data variables:\n",
    -       "    r           (date, time, step, levelist, latitude, longitude) float64 88kB ...\n",
    -       "    t           (date, time, step, levelist, latitude, longitude) float64 88kB ...\n",
    +       "    r           (date, time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t           (date, time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
    -       "    param:        t\n",
            "    class:        od\n",
            "    stream:       oper\n",
            "    levtype:      pl\n",
    @@ -2453,35 +2483,34 @@
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", - "Dimensions: (date: 2, time: 2, step: 2, levelist: 2, latitude: 19,\n", + "Dimensions: (date: 2, time: 2, step: 2, level: 2, latitude: 19,\n", " longitude: 36)\n", "Coordinates:\n", " * date (date) datetime64[ns] 16B 2024-06-03 2024-06-04\n", " * time (time) timedelta64[ns] 16B 00:00:00 12:00:00\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " valid_time (date, time, step) datetime64[ns] 64B ...\n", " * latitude (latitude) float64 152B 90.0 80.0 70.0 ... -70.0 -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 20.0 ... 330.0 340.0 350.0\n", "Data variables:\n", - " r (date, time, step, levelist, latitude, longitude) float64 88kB ...\n", - " t (date, time, step, levelist, latitude, longitude) float64 88kB ...\n", + " r (date, time, step, level, latitude, longitude) float64 88kB ...\n", + " t (date, time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", @@ -2524,13 +2553,13 @@ "source": [ "When ``decode_times=True`` (the default) the follwing coordinates will be stored as datetime64:\n", "\n", - "- coordinates representing the date-like ecCodes keys (e.g. \"date\", \"validityDate\" etc.)\n", + "- coordinates representing the date-like roles or GRIB keys (e.g. \"date\", \"validityDate\" etc.)\n", "- datetime coordinates (e.g. \"forecast_reference_time\" etc.)\n", "\n", "When ``decode_timedelta=True`` (the default) the following coordinates will be stored as timedelta64:\n", "\n", - "- coordinates representing the time-like ecCodes keys (e.g. \"time\", \"validityTime\" etc.)\n", - "- duration-like coordinates (e.g. \"step\")" + "- coordinates representing the time-like roles or GRIB keys (e.g. \"time\", \"validityTime\" etc.)\n", + "- duration-like coordinates (e.g. \"step\", \"endStep\")" ] }, { @@ -2546,7 +2575,7 @@ " * date (date) datetime64[ns] 16B 2024-06-03 2024-06-04\n", " * time (time) timedelta64[ns] 16B 00:00:00 12:00:00\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0" ] @@ -2568,12 +2597,12 @@ "source": [ "When ``decode_times=False`` the following rules apply:\n", "\n", - "- coordinates representing date-like ecCodes keys (e.g. \"date\", \"validityDate\" etc.) will store the native GRIB int values (as yyyymmdd)\n", + "- coordinates representing date-like GRIB keys (e.g. \"date\", \"validityDate\" etc.) will store the native GRIB int values (as yyyymmdd)\n", "- datetime coordinates (e.g. \"forecast_reference_time\" etc.) will store datetime64 values\n", "\n", "When ``decode_timedelta=False`` the following rules apply:\n", "\n", - "- coordinates representing the time-like ecCodes keys (e.g. \"time\", \"validityTime\" etc.) will store the native GRIB int values (as 100*hours + minutes)\n", + "- coordinates representing the time-like GRIB keys (e.g. \"time\", \"validityTime\" etc.) will store the native GRIB int values (as 100*hours + minutes)\n", "- duration-like (e.g. \"step\") coordinates will store int values with units indicated by the coordinate attribute \"units\"" ] }, @@ -2596,7 +2625,7 @@ " * date (date) int64 16B 20240603 20240604\n", " * time (time) int64 16B 0 1200\n", " * step (step) int64 16B 0 6\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 70.0 60.0 ... -70.0 -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 20.0 30.0 ... 330.0 340.0 350.0" ] @@ -2626,9 +2655,7 @@ { "data": { "text/plain": [ - "{'standard_name': 'forecast_period',\n", - " 'long_name': 'time since forecast_reference_time',\n", - " 'units': 'hours'}" + "{'units': 'hours'}" ] }, "execution_count": 9, diff --git a/docs/examples/xarray_engine_to_grib.ipynb b/docs/examples/xarray_engine_to_grib.ipynb index a69e0264e..8f0efe4fd 100644 --- a/docs/examples/xarray_engine_to_grib.ipynb +++ b/docs/examples/xarray_engine_to_grib.ipynb @@ -58,7 +58,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "", + "model_id": "cf6d8ab8661b476ca0f076c67c8acfea", "version_major": 2, "version_minor": 0 }, @@ -69,13 +69,6 @@ "metadata": {}, "output_type": "display_data" }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "From version 0.11.0 the default engine for to_xarray is 'earthkit'. Use engine=`cfgrib` to invoke the cfgrib engine.\n" - ] - }, { "data": { "text/html": [ @@ -109,14 +102,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -171,6 +164,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -207,7 +201,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -218,7 +212,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -290,15 +284,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -449,17 +443,17 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 176kB\n",
    -       "Dimensions:                  (forecast_reference_time: 4, step: 2, levelist: 2,\n",
    +       "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
            "                              latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
            "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
            "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    -       "  * levelist                 (levelist) int64 16B 500 700\n",
    +       "  * level                    (level) int64 16B 500 700\n",
            "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
            "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    r                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    -       "    t                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
            "    class:        od\n",
            "    stream:       oper\n",
    @@ -471,34 +465,34 @@
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", - "Dimensions: (forecast_reference_time: 4, step: 2, levelist: 2,\n", + "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", " latitude: 19, longitude: 36)\n", "Coordinates:\n", " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " r (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", - " t (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", " class: od\n", " stream: oper\n", @@ -933,7 +927,7 @@ "tags": [] }, "source": [ - "The generated GRIB fieldlist can be saved to disk using the :py:meth:`~data.readers.grib.index.GribFieldList.save` method." + "The generated GRIB fieldlist can be saved to disk using the :func:`to_target` method." ] }, { @@ -1035,7 +1029,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "", + "model_id": "120cbbf74d0c4edeac0cbdeed7bb6e2f", "version_major": 2, "version_minor": 0 }, @@ -1197,9 +1191,9 @@ ], "metadata": { "kernelspec": { - "display_name": "dev_ecc", + "display_name": "dev", "language": "python", - "name": "dev_ecc" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -1211,7 +1205,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/docs/examples/xarray_engine_variable_key.ipynb b/docs/examples/xarray_engine_variable_key.ipynb index 23b95acc3..71c30dc70 100644 --- a/docs/examples/xarray_engine_variable_key.ipynb +++ b/docs/examples/xarray_engine_variable_key.ipynb @@ -31,7 +31,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "id": "08b75c56-0b2f-4cc6-9637-b28ad2aa4455", "metadata": { "editable": true, @@ -41,6 +41,20 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "41fcae342d1e49ac812d5756cd625b1f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "pl.grib: 0%| | 0.00/48.8k [00:00 span {\n", @@ -255,15 +270,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -414,69 +429,71 @@ " fill: currentColor;\n", "}\n", "
    <xarray.Dataset> Size: 176kB\n",
    -       "Dimensions:                  (forecast_reference_time: 4, step: 2, levelist: 2,\n",
    +       "Dimensions:                  (forecast_reference_time: 4, step: 2, level: 2,\n",
            "                              latitude: 19, longitude: 36)\n",
            "Coordinates:\n",
            "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
            "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    -       "  * levelist                 (levelist) int64 16B 500 700\n",
    +       "  * level                    (level) int64 16B 500 700\n",
            "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
            "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
            "Data variables:\n",
    -       "    r                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    -       "    t                        (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
            "Attributes:\n",
    -       "    param:        t\n",
            "    class:        od\n",
            "    stream:       oper\n",
            "    levtype:      pl\n",
            "    type:         fc\n",
            "    expver:       0001\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
            "    domain:       g\n",
            "    number:       0\n",
            "    Conventions:  CF-1.8\n",
    -       "    institution:  ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", - "Dimensions: (forecast_reference_time: 4, step: 2, levelist: 2,\n", + "Dimensions: (forecast_reference_time: 4, step: 2, level: 2,\n", " latitude: 19, longitude: 36)\n", "Coordinates:\n", " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", - " * levelist (levelist) int64 16B 500 700\n", + " * level (level) int64 16B 500 700\n", " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", "Data variables:\n", - " r (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", - " t (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ...\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", " Conventions: CF-1.8\n", " institution: ECMWF" ] }, - "execution_count": 4, + "execution_count": 1, "metadata": {}, "output_type": "execute_result" } @@ -518,7 +535,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "id": "4898ceb3-4657-4397-b1d8-3bc1110b86eb", "metadata": { "editable": true, @@ -561,14 +578,14 @@ " --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee);\n", "}\n", "\n", - "html[theme=dark],\n", - "html[data-theme=dark],\n", - "body[data-theme=dark],\n", + "html[theme=\"dark\"],\n", + "html[data-theme=\"dark\"],\n", + "body[data-theme=\"dark\"],\n", "body.vscode-dark {\n", " --xr-font-color0: rgba(255, 255, 255, 1);\n", " --xr-font-color2: rgba(255, 255, 255, 0.54);\n", " --xr-font-color3: rgba(255, 255, 255, 0.38);\n", - " --xr-border-color: #1F1F1F;\n", + " --xr-border-color: #1f1f1f;\n", " --xr-disabled-color: #515151;\n", " --xr-background-color: #111111;\n", " --xr-background-color-row-even: #111111;\n", @@ -623,6 +640,7 @@ ".xr-section-item input {\n", " display: inline-block;\n", " opacity: 0;\n", + " height: 0;\n", "}\n", "\n", ".xr-section-item input + label {\n", @@ -659,7 +677,7 @@ "\n", ".xr-section-summary-in + label:before {\n", " display: inline-block;\n", - " content: '►';\n", + " content: \"►\";\n", " font-size: 11px;\n", " width: 15px;\n", " text-align: center;\n", @@ -670,7 +688,7 @@ "}\n", "\n", ".xr-section-summary-in:checked + label:before {\n", - " content: '▼';\n", + " content: \"▼\";\n", "}\n", "\n", ".xr-section-summary-in:checked + label > span {\n", @@ -742,15 +760,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -914,30 +932,30 @@ " t500 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " t700 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", - " levelist: 700\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", @@ -954,20 +972,20 @@ " t500 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " t700 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: t\n", " class: od\n", " stream: oper\n", " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", - " levelist: 700\n", " Conventions: CF-1.8\n", " institution: ECMWF" ] }, - "execution_count": 6, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -993,7 +1011,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "id": "f55fe598-7f80-4e9a-87b3-43131f78f2e1", "metadata": { "editable": true, @@ -1003,6 +1021,20 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3aaa4f09dc6e498c8df353d0bce3579e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "mixed_pl_sfc.grib: 0%| | 0.00/390k [00:00 span {\n", @@ -1217,15 +1250,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -1398,30 +1431,29 @@ " z700 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " z850 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: z\n", " class: od\n", " stream: oper\n", - " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", - " levelist: 850\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • class :
    od
    stream :
    oper
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 1MB\n", @@ -1447,20 +1479,19 @@ " z700 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " z850 (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: z\n", " class: od\n", " stream: oper\n", - " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", - " levelist: 850\n", " Conventions: CF-1.8\n", " institution: ECMWF" ] }, - "execution_count": 8, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -1501,7 +1532,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 4, "id": "e1bc4e80-2efc-460c-96b0-26553bed7591", "metadata": { "editable": true, @@ -1511,6 +1542,20 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b3618ce82f674f7d9c28e3bc7cc87fa9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "mixed_pl_sfc.grib: 0%| | 0.00/390k [00:00 span {\n", @@ -1725,15 +1771,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -1906,30 +1952,29 @@ " z_700_pl (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " z_850_pl (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: z\n", " class: od\n", " stream: oper\n", - " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", - " levelist: 850\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • class :
    od
    stream :
    oper
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 1MB\n", @@ -1955,20 +2000,19 @@ " z_700_pl (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " z_850_pl (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: z\n", " class: od\n", " stream: oper\n", - " levtype: pl\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", " number: 0\n", - " levelist: 850\n", " Conventions: CF-1.8\n", " institution: ECMWF" ] }, - "execution_count": 10, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -1990,12 +2034,12 @@ "tags": [] }, "source": [ - "This technique is partuculary useful when the same parameter is available on multiple level types in the input data. In this case using \"param_level\" does not result in a full hypercube, however the same `remapping`` that we used above does." + "This technique is partuculary useful when the same parameter is available on multiple level types in the input data. In this case using \"param_level\" does not result in a full hypercube, however the same ``remapping`` that we used above does." ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 5, "id": "39cbb360-43c4-416e-956b-8b6cfadda26c", "metadata": { "editable": true, @@ -2005,6 +2049,20 @@ "tags": [] }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ad9990991ee44e4b81fad30f4cef90e4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "mixed_pl_ml.grib: 0%| | 0.00/176k [00:00 span {\n", @@ -2219,15 +2278,15 @@ "}\n", "\n", ".xr-dim-list:before {\n", - " content: '(';\n", + " content: \"(\";\n", "}\n", "\n", ".xr-dim-list:after {\n", - " content: ')';\n", + " content: \")\";\n", "}\n", "\n", ".xr-dim-list li:not(:last-child):after {\n", - " content: ',';\n", + " content: \",\";\n", " padding-right: 5px;\n", "}\n", "\n", @@ -2395,29 +2454,28 @@ " u_700_pl (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " u_90_ml (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: u\n", " class: od\n", " stream: oper\n", - " levtype: ml\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", - " levelist: 90\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • class :
    od
    stream :
    oper
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 351kB\n", @@ -2438,19 +2496,18 @@ " u_700_pl (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", " u_90_ml (forecast_reference_time, step, latitude, longitude) float64 44kB ...\n", "Attributes:\n", - " param: u\n", " class: od\n", " stream: oper\n", - " levtype: ml\n", " type: fc\n", " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", " domain: g\n", - " levelist: 90\n", " Conventions: CF-1.8\n", " institution: ECMWF" ] }, - "execution_count": 11, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -2478,9 +2535,9 @@ ], "metadata": { "kernelspec": { - "display_name": "dev_ecc", + "display_name": "dev", "language": "python", - "name": "dev_ecc" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -2492,7 +2549,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/docs/guide/xarray/dim.rst b/docs/guide/xarray/dim.rst new file mode 100644 index 000000000..ef5f8adce --- /dev/null +++ b/docs/guide/xarray/dim.rst @@ -0,0 +1,110 @@ +.. _xr_dim: + +Dimensions +================== + +One of the most important aspect of the :ref:`xr_engine` is how it generates dimensions in the Xarray dataset with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. + +.. _xr_dim_roles: +.. _xr_predefined_dims: + + +Predefined dimensions and dimension roles +------------------------------------------- + +By default, a list of predefined dimensions are generated. Their order is fixed: + +- ensemble forecast member dimension +- temporal dimensions (controlled by ``time_dim_mode``) +- vertical dimensions (controlled by ``level_dim_mode``) + +The predefined dimensions are based on the ``dim_roles``, which is a mapping between the "roles" and the metadata keys associated with the roles. +The possible roles are as follows: + +.. list-table:: Default dimension roles + :header-rows: 1 + + * - Dimension role + - Description + - Key (profile: :ref:`mars `) + - Key (profile: :ref:`grib `) + * - "number" + - metadata key interpreted as ensemble forecast members + - "number" + - "number" + * - "date" + - metadata key interpreted as date part of the "forecast_reference_time" + - "date" + - "date" + * - "time" + - metadata key interpreted as time part of the "forecast_reference_time" + - "time" + - "time" + * - "step" + - metadata key interpreted as forecast step + - "step_timedelta" + - "step_timedelta" + * - "forecast_reference_time" + - if not specified or None or empty the forecast reference time is built using the "date" and "time" roles + - None + - None + * - "valid_time" + - if not specified or None or empty the valid time is built using the "validityDate" and "validityTime" metadata keys + - None + - None + * - "level" + - metadata key interpreted as level + - "levelist" + - "level" + * - "level_type" + - metadata key interpreted as level type + - "levtype" + - "typeOfLevel" + +By default, the dimension names are the same as the role names. To use the associated metadata keys instead use the ``dim_name_from_role_name=False`` option. + +the metadata keys. However, this can be controlled with the ``dim_name_from_role_name`` option. If set to ``False``, the dimension names will be the same as the dimension roles. This is useful when you want to use the dimension roles in your code, as they are more descriptive than the metadata keys. + +.. note:: + + For GRIB data, "step_timedelta" is a generated metadata key (by earthkit-data), which is the representation of the value of the "endStep" key as a `datetime.timedelta`. + + +Dimension modes +---------------------- + +The ``time_dim_mode`` and ``level_dim_mode`` options control how the temporal and vertical dimensions are generated in the Xarray dataset using ``dim_roles``. See the following notebooks for examples of how these modes work: + +``time_dim_mode``: + +- :ref:`/examples/xr_engine_temporal.ipynb` +- :ref:`/examples/xr_engine_seasonal.ipynb` + + +``level_dim_mode``: +- :ref:`/examples/xr_engine_level.ipynb` + + +Squeezing/ensuring dimensions +---------------------------------- + +By default, the dimensions are squeezed. This means that if a dimension has only one value, it is removed from the dataset. This can be controlled with the ``squeeze`` option. Alternatively, the ``ensure_dims`` option can be used to ensure that certain dimensions are always present in the dataset, even if they have only one value. This is useful when you want to keep the dimensions for consistency or for further processing. + +See the following notebooks for examples of how this works: + +- :ref:`/examples/xr_engine_squeeze.ipynb` + + +Extra dimensions +---------------------- + +The ``extra_dims`` option allows to add extra dimensions to the Xarray dataset on top of the predefined ones. E.g. + + + +Fixed dimensions +---------------------- + + +Split dimensions +---------------------- diff --git a/docs/guide/xarray/overview.rst b/docs/guide/xarray/overview.rst index aa8031cb6..57af5d94e 100644 --- a/docs/guide/xarray/overview.rst +++ b/docs/guide/xarray/overview.rst @@ -25,12 +25,12 @@ We can convert :ref:`grib` data into an Xarray dataset by using :py:meth:`~data. Coordinates: * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202... * step (step) timedelta64[ns] 16B 00:00:00 06:00:00 - * levelist (levelist) int64 16B 500 700 + * level (level) int64 16B 500 700 * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0 * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0 Data variables: - r (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ... - t (forecast_reference_time, step, levelist, latitude, longitude) float64 88kB ... + r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ... + t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ... ... .. note:: @@ -51,6 +51,11 @@ We can also use the Xarray engine to read GRIB data directly with the :py:func:` Size: 176kB ... +Dimensions +++++++++++ + +The pivotal question when generating the Xarray dataset is how to form the dimensions. The :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` method has a number of options to control the dimensions. Please see more details in the :ref:`dimensions ` section. + Profiles +++++++++ diff --git a/docs/release_notes/deprecations.rst b/docs/release_notes/deprecations.rst index 6d142fd63..110bab34e 100644 --- a/docs/release_notes/deprecations.rst +++ b/docs/release_notes/deprecations.rst @@ -1,6 +1,34 @@ Deprecations ============= + +.. _deprecated-0.15.0: + +Version 0.15.0 +----------------- + +.. _deprecated-ens-dim-role: + +The "ens" dimension role has been renamed to "number" +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The name of the ensemble member :ref:`dimension role <_xr_dim_roles>` changed to "number" from "ens" in the ``dim_roles`` option of :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. The old name is still available for backward compatibility but will be removed in a future release. + +.. list-table:: + :header-rows: 0 + + * - Deprecated code + * - + + .. literalinclude:: include/deprec_ens_dim_role.py + + * - New code + * - + + .. literalinclude:: include/migrated_ens_dim_role.py + + + .. _deprecated-0.13.0: Version 0.13.0 diff --git a/docs/release_notes/include/deprec_ens_dim_role.py b/docs/release_notes/include/deprec_ens_dim_role.py new file mode 100644 index 000000000..965e63215 --- /dev/null +++ b/docs/release_notes/include/deprec_ens_dim_role.py @@ -0,0 +1,7 @@ +import earthkit.data as ekd + +ds_fl = ekd.from_source("sample", "ens_cf_pf.grib") + +ds = ds_fl.to_xarray( + dim_roles={"ens": "perturbationNumber"}, +) diff --git a/docs/release_notes/include/migrated_ens_dim_role.py b/docs/release_notes/include/migrated_ens_dim_role.py new file mode 100644 index 000000000..1c98f6af8 --- /dev/null +++ b/docs/release_notes/include/migrated_ens_dim_role.py @@ -0,0 +1,7 @@ +import earthkit.data as ekd + +ds_fl = ekd.from_source("sample", "ens_cf_pf.grib") + +ds = ds_fl.to_xarray( + dim_roles={"number": "perturbationNumber"}, +) diff --git a/docs/release_notes/version_0.15_updates.rst b/docs/release_notes/version_0.15_updates.rst index bc8a5d815..f53d1e020 100644 --- a/docs/release_notes/version_0.15_updates.rst +++ b/docs/release_notes/version_0.15_updates.rst @@ -5,13 +5,52 @@ Version 0.15 Updates Version 0.15.0 =============== +Deprecations ++++++++++++++++++++ + +- :ref:`deprecated-ens-dim-role` + Xarray engine ++++++++++++++++++++++++++++++ +Breaking changes +------------------- + +- Separated the dimension names from the metadata keys used to generate the dimensions. Dimensions associated with the dimension roles are now taking the name of the :ref:`dimension role <_xr_dim_roles>`, irrespective of the metadata key the dimension role is mapped to. E.g.: the "level_type" dimension role now generates a dimension called "level_type". Previously, the dimension name was the name of the associated metadata key: e.g. it was "levtype" in the :ref:`default ` profile. The old behaviour can still be invoked by using the newly added ``dim_name_from_role_name=False`` option. See: :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. + + +- The ``step`` dimension role is now mapped to the ``step_timedelta`` metadata key, which is the ``datetime.timedelta`` representation of the ``"endStep"`` GRIB/metadata key. Previously, this role was mapped to the ``"step"`` key. Please note that due to this change when ``dim_name_from_role_name=False`` is used the step dimension will be called "step_timedelta" instead of "step". + + +Other changes +------------------- + +- Allowed using mappings in the ``extra_dims`` and ``fixed_dims`` options to define both the name of the dimensions and the metadata keys to generate their values. Previously, these options only took a single/multiple metadata keys. E.g. both the options below will generate the "expver", "mars_stream" and "mars_class" dimensions using the "expver", "stream" and "class" metadata keys. + + .. code-block:: python + + extra_dims = ["expver", {"mars_stream": "stream"}, ("mars_class", "class")] + extra_dims = { + "expver": "expver", + "mars_stream": "stream", + "mars_class": "class", + } + + - Improved the serialisation of GRIB fieldlists to reduce memory usage when Xarray is generated with chunks (:pr:`700`). See the :ref:`/examples/xarray_engine_chunks.ipynb` notebook example. - TensorBackendArray, which implements the lazy loading of DataArrays in the Xarray engine, now uses a ``dask.utils.SerializableLock`` when accessing the data (:pr:`700`). - Enabled converting :ref:`data-sources-lod` fieldlists into Xarray (:pr:`701`). See the :ref:`/examples/list_of_dicts_to_xarray.ipynb` notebook example. +New Xarray engine notebooks +------------------------------ + +- :ref:`/examples/xr_engine_step_range.ipynb` +- :ref:`/examples/xr_engine_ensemble.ipynb` +- :ref:`/examples/xr_engine_squeeze.ipynb` +- :ref:`/examples/xarray_engine_chunks.ipynb` +- :ref:`/examples/list_of_dicts_to_xarray.ipynb` + + New features +++++++++++++++++ diff --git a/src/earthkit/data/core/select.py b/src/earthkit/data/core/select.py index 2e6a1fdf2..104cc9c31 100644 --- a/src/earthkit/data/core/select.py +++ b/src/earthkit/data/core/select.py @@ -33,7 +33,7 @@ def normalize_selection(*args, **kwargs): or v is ALL or callable(v) or isinstance(v, (list, tuple, set, slice)) - or isinstance(v, (str, int, float, datetime.datetime)) + or isinstance(v, (str, int, float, datetime.datetime, datetime.timedelta)) ), f"Unsupported type: {type(v)} for key {k}" return _kwargs diff --git a/src/earthkit/data/indexing/tensor.py b/src/earthkit/data/indexing/tensor.py index 533f9d4f6..4e6460eb5 100644 --- a/src/earthkit/data/indexing/tensor.py +++ b/src/earthkit/data/indexing/tensor.py @@ -443,11 +443,38 @@ def _subset(self, indexes): ds = self.source[tuple(dataset_indexes)] return self.from_tensor(self, ds, coords) - def make_valid_datetime(self, dtype="datetime64[ns]"): + def make_valid_datetime(self, dims_map, dtype="datetime64[ns]"): # TODO: make it more general - dims_opt = [ - ["base_datetime", "step"], - ["base_datetime"], + + for k in ["valid_datetime", "valid_time"]: + if k in self.user_coords: + import datetime + + return (k,), [datetime.datetime.fromisoformat(x) for x in self.user_coords[k]] + + # in the tensor the dims.coords are GRIB keys + # dims_map is a mapping from dim names to GRIB keys + DIM_ROLES = { + "forecast_reference_time": ("forecast_reference_time", "base_datetime"), + "step": ("step_timedelta", "step", "ensStep", "stepRange"), + "date": ("date", "dataDate"), + "time": ("time", "dataTime"), + } + + # map dim roles to keys available in the tensor + keys = {} + for k in DIM_ROLES: + for d in dims_map: + if d.name == k: + keys[k] = d.key + break + if k not in keys: + for d in self.user_dims: + if d in DIM_ROLES[k]: + keys[k] = d + break + + DIM_COMBINATIONS = [ ["forecast_reference_time", "step"], ["forecast_reference_time"], ["date", "time", "step"], @@ -457,19 +484,14 @@ def make_valid_datetime(self, dtype="datetime64[ns]"): ["step"], ] - for k in ["valid_datetime", "valid_time"]: - if k in self.user_coords: - import datetime - - return (k,), [datetime.datetime.fromisoformat(x) for x in self.user_coords[k]] - - # print(f"{self.user_dims=}") - for dims in dims_opt: - if all(d in self.user_dims for d in dims): + for dims in DIM_COMBINATIONS: + if all(d in keys for d in dims): + dims_step = [keys[d] for d in dims] # use same dim order as in user_dims - dims = [d for d in dims if d in self.user_dims] + dims = [d for d in self.user_dims if d in dims_step] + assert len(dims) == len(dims_step), f"Duplicate dims in {dims}" other_dims = [d for d in self.user_dims if d not in dims] - # print(f"{dims=} {other_dims=}") + if other_dims: import datetime diff --git a/src/earthkit/data/readers/grib/metadata.py b/src/earthkit/data/readers/grib/metadata.py index 9de63e1cf..5f3bc2e51 100644 --- a/src/earthkit/data/readers/grib/metadata.py +++ b/src/earthkit/data/readers/grib/metadata.py @@ -565,7 +565,10 @@ def indexing_datetime(self): return self._datetime("indexingDate", "indexingTime") def step_timedelta(self): - return to_timedelta(self.get("step", None)) + v = self.get("endStep", None) + if v is None: + v = self.get("step", None) + return to_timedelta(v) def _datetime(self, date_key, time_key): date = self.get(date_key, None) diff --git a/src/earthkit/data/readers/grib/xarray.py b/src/earthkit/data/readers/grib/xarray.py index 84837d546..dcc6405d3 100644 --- a/src/earthkit/data/readers/grib/xarray.py +++ b/src/earthkit/data/readers/grib/xarray.py @@ -112,19 +112,62 @@ def to_xarray(self, engine="earthkit", xarray_open_dataset_kwargs=None, **kwargs A variable or list of variables to drop from the dataset. Default is None. * rename_variables: dict, None Mapping to rename variables. Default is None. - * extra_dims: str, or iterable of str, None - Metadata key or list of metadata keys to be used as additional dimensions on top of the - predefined dimensions. Only enabled when no ``fixed_dims`` is specified. Default is None. + * extra_dims: str, or iterable of str, None + Define additional dimensions on top of the predefined dimensions. Only enabled when no + ``fixed_dims`` is specified. Default is None. It can be a single item or a list. Each + item is either a metadata key, or a dict/tuple defining mapping between the dimension + name and the metadata key. The whole option can be a dict. E.g. + + .. code-block:: python + + # use key "expver" as a dimension + extra_dims = "expver" + # use keys "expver" and "steam" as a dimension + extra_dims = ["expver", "stream"] + # define dimensions "expver", mars_stream" and "mars_type" from + # metadata keys "expver", "stream" and "type" + extra_dims = [ + "expver", + {"mars_stream": "stream"}, + ("mars_type", "type"), + ] + extra_dims = [ + { + "expver": "expver", + "mars_stream": "stream", + "mars_type": "type", + } + ] + * drop_dims: str, or iterable of str, None - Metadata key or list of metadata keys to be ignored as dimensions. Default is None. + Single or multiple dimensions to be ignored. Default is None. Default is None. * ensure_dims: str, or iterable of str, None - Metadata key or list of metadata keys that should be used as dimensions even - when ``squeeze=True``. Default is None. + Dimension or dimensions that should be kept even when ``squeeze=True`` and their size + is only 1. Default is None. * fixed_dims: str, or iterable of str, None - Metadata key or list of metadata keys in the order they should be used as dimensions. When - defined no other dimensions will be used. Might be incompatible with other settings. - Default is None. + Define all the dimensions to be generated. When used no other dimensions will be created. + Might be incompatible with other settings. Default is None. It can be a single item or a list. + Each item is either a metadata key, or a dict/tuple defining mapping between the dimension + name and the metadata key. The whole option can be a dict. E.g.: + + .. code-block:: python + + # use key "step" as a dimension + fixed_dims = "step" + # use keys "step" and "levelist" as a dimension + extra_dims = ["step", "levelist"] + # define dimensions "step", level" and "level_type" from + # metadata keys "step", "levelist" and "levtype" + extra_dims = [ + "step", + {"level": "levelist"}, + ("level_type", "levtype"), + ] + extra_dims = [ + {"step": "step", "level": "levelist", "level_type": "levtype"} + ] + * dim_roles: dict, None Specify the "roles" used to form the predefined dimensions. The predefined dimensions are automatically generated when no ``fixed_dims`` specified and comprise the following @@ -137,7 +180,7 @@ def to_xarray(self, engine="earthkit", xarray_open_dataset_kwargs=None, **kwargs ``dim_roles`` is a mapping between the "roles" and the metadata keys representing the roles. The possible roles are as follows: - - "ens": metadata key interpreted as ensemble forecast members + - "number": metadata key interpreted as ensemble forecast members - "date": metadata key interpreted as date part of the "forecast_reference_time" - "time": metadata key interpreted as time part of the "forecast_reference_time" - "step": metadata key interpreted as forecast step @@ -153,7 +196,7 @@ def to_xarray(self, engine="earthkit", xarray_open_dataset_kwargs=None, **kwargs .. code-block:: python { - "ens": "number", + "number": "number", "date": "dataDate", "time": "dataTime", "step": "step", @@ -166,6 +209,11 @@ def to_xarray(self, engine="earthkit", xarray_open_dataset_kwargs=None, **kwargs ``dims_roles`` behaves differently to the other kwargs in the sense that it does not override but update the default values. So e.g. to change only "ens" in the defaults it is enough to specify: "dim_roles={"ens": "perturbationNumber"}. + * dim_name_from_role_name: bool, None + If True, the dimension names are formed from the role names. Otherwise the + dimension names are formed from the metadata keys specified in ``dim_roles``. + Its default value (None) expands to True unless the ``profile`` overwrites it. + Only used when no `fixed_dims`` are specified. *New in version 0.15.0*. * rename_dims: dict, None Mapping to rename dimensions. Default is None. * dims_as_attrs: str, or iterable of str, None diff --git a/src/earthkit/data/utils/xarray/builder.py b/src/earthkit/data/utils/xarray/builder.py index f752ab3e3..bd4def9b9 100644 --- a/src/earthkit/data/utils/xarray/builder.py +++ b/src/earthkit/data/utils/xarray/builder.py @@ -297,7 +297,7 @@ def collect_date_coords(self, tensor): ): from .coord import Coord - _dims, _vals = tensor.make_valid_datetime() + _dims, _vals = tensor.make_valid_datetime(self.dims) if _dims is not None and _vals is not None: self.tensor_coords["valid_time"] = Coord.make("valid_time", _vals, dims=_dims) @@ -318,8 +318,13 @@ def build(self): # build dataset dataset = xarray.Dataset(xr_vars, coords=xr_coords, attrs=xr_attrs) - if self.profile.rename_dims_map(): - dataset = dataset.rename(self.profile.rename_dims_map()) + dataset = self.profile.rename_dataset_dims(dataset) + + # dim_map = self.profile.rename_dims_map() + # if dim_map: + # d = {k: v for k, v in dim_map.items() if k in dataset.dims} + # if d: + # dataset = dataset.rename(d) if "source" not in dataset.encoding: dataset.encoding["source"] = None @@ -544,7 +549,7 @@ def parse(self, ds, profile=None, full=False): # LOG.debug(f"{remapping=}") # LOG.debug(f"{profile.remapping=}") - # LOG.debug(f"{profile.index_keys=}") + LOG.debug(f"{profile.index_keys=}") # create a new fieldlist for optimised access to unique values ds_xr = XArrayInputFieldList( diff --git a/src/earthkit/data/utils/xarray/coord.py b/src/earthkit/data/utils/xarray/coord.py index 1268e0118..32950fdcd 100644 --- a/src/earthkit/data/utils/xarray/coord.py +++ b/src/earthkit/data/utils/xarray/coord.py @@ -168,7 +168,10 @@ def attrs(self, name, profile): class MonthCoord(Coord): - pass + def attrs(self, name, profile): + attrs = super().attrs(name, profile) + attrs["units"] = "months" + return attrs class LevelCoord(Coord): diff --git a/src/earthkit/data/utils/xarray/defaults.yaml b/src/earthkit/data/utils/xarray/defaults.yaml index c4c4d1ccb..0aaa1ae9f 100644 --- a/src/earthkit/data/utils/xarray/defaults.yaml +++ b/src/earthkit/data/utils/xarray/defaults.yaml @@ -43,7 +43,7 @@ strict: false errors: raise dim_roles: - ens: number + number: number date: date time: time step: step @@ -52,6 +52,8 @@ dim_roles: level: level level_type: typeOfLevel +dim_name_from_role_name: true + coord_attrs: latitude: units: degrees_north diff --git a/src/earthkit/data/utils/xarray/diff.py b/src/earthkit/data/utils/xarray/diff.py index 1493c7689..8d4bb2a4f 100644 --- a/src/earthkit/data/utils/xarray/diff.py +++ b/src/earthkit/data/utils/xarray/diff.py @@ -7,6 +7,7 @@ # nor does it submit to any jurisdiction. # +import datetime import logging import math @@ -69,6 +70,8 @@ def _compare(v1, v2): return math.isclose(v1, v2, rel_tol=1e-9), ListDiff.VALUE_DIFF elif isinstance(v1, str) and isinstance(v2, str): return v1 == v2, ListDiff.VALUE_DIFF + elif isinstance(v1, datetime.timedelta) and isinstance(v2, datetime.timedelta): + return v1 == v2, ListDiff.VALUE_DIFF elif type(v1) is not type(v2): return False, ListDiff.TYPE_DIFF else: diff --git a/src/earthkit/data/utils/xarray/dim.py b/src/earthkit/data/utils/xarray/dim.py index b79aab9d3..9fd9c8cfa 100644 --- a/src/earthkit/data/utils/xarray/dim.py +++ b/src/earthkit/data/utils/xarray/dim.py @@ -42,7 +42,7 @@ class ParamLevelKey(CompoundKey): LEVEL_TYPE_KEYS = ["typeOfLevel", "levtype"] DATE_KEYS = ["date", "andate", "validityDate", "dataDate", "hdate", "referenceDate", "indexingDate"] TIME_KEYS = ["time", "antime", "validityTime", "dataTime", "referenceTime", "indexingTime"] -STEP_KEYS = ["step", "endStep", "stepRange", "forecastMonth", "fcmonth"] +STEP_KEYS = ["step_timedelta", "step", "endStep", "stepRange", "forecastMonth", "fcmonth"] MONTH_KEYS = ["forecastMonth", "fcmonth"] VALID_DATETIME_KEYS = ["valid_time", "valid_datetime"] BASE_DATETIME_KEYS = [ @@ -89,15 +89,18 @@ def find_alias(key, drop=None): return r -def make_dim(owner, name, *args, **kwargs): - if name in PREDEFINED_DIMS: - return PREDEFINED_DIMS[name](owner, *args, key=name, **kwargs) +def make_dim(owner, *args, name=None, key=None, **kwargs): + predef_key = key or name + + if predef_key in PREDEFINED_DIMS: + return PREDEFINED_DIMS[predef_key](owner, *args, name=name, key=key, **kwargs) ck = CompoundKey.make(name) if ck is not None: d = CompoundKeyDim(owner, ck) else: - d = OtherDim(owner, name, *args, **kwargs) + # print("args", args, "kwargs", kwargs, "name", name, "key", key) + d = OtherDim(owner, *args, name=name, key=key, **kwargs) return d @@ -123,6 +126,7 @@ class Dim: name = None key = None + label = None alias = None drop = None enforce_unique = False @@ -233,38 +237,43 @@ class NumberDim(Dim): class DateDim(Dim): name = "date" - drop = get_keys(DATE_KEYS + DATETIME_KEYS, drop=name) + drop = get_keys(DATE_KEYS + DATETIME_KEYS, drop="date") class TimeDim(Dim): name = "time" - drop = get_keys(TIME_KEYS + DATETIME_KEYS, drop=name) + drop = get_keys(TIME_KEYS + DATETIME_KEYS, drop="time") + + +# class StepDim(Dim): +# name = "step" +# drop = get_keys(STEP_KEYS + VALID_DATETIME_KEYS, drop="step") class StepDim(Dim): name = "step" - drop = get_keys(STEP_KEYS + VALID_DATETIME_KEYS, drop=name) + drop = get_keys(STEP_KEYS + VALID_DATETIME_KEYS, drop=["step_timedelta"]) class ValidTimeDim(Dim): name = "valid_time" - drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop=name) + drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop="valid_time") class ForecastRefTimeDim(Dim): name = "forecast_reference_time" - drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop=name) + drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop="forecast_reference_time") alias = ["base_datetime"] class IndexingTimeDim(Dim): name = "indexing_time" - drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop=name) + drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop="indexing_time") class ReferenceTimeDim(Dim): name = "reference_time" - drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop=name) + drop = get_keys(DATE_KEYS + TIME_KEYS + DATETIME_KEYS, drop="reference_time") class CustomForecastRefDim(Dim): @@ -378,13 +387,41 @@ class OtherDim(Dim): pass +class DimRole: + NAMES = ("number", "date", "time", "step", "level", "level_type", "forecast_reference_time", "valid_time") + + def __init__(self, d, name_as_key=True): + self.d = d + self.name_as_key = name_as_key + + if "ens" in d: + import warnings + + warnings.warn("'ens' key in dim_roles is deprecated. Use 'number' instead", DeprecationWarning) + self.d["number"] = self.d.pop("ens") + + for k in d: + if k not in self.NAMES: + raise ValueError(f"Invalid dim role name={k}. Must be one of {self.NAMES}") + + def role(self, name, default=None, raise_error=True): + if name in self.d: + return self.d[name], name if self.name_as_key else self.d[name] + if default is not None: + return default + if raise_error: + raise ValueError(f"Dim role {name} not found in {self.d}") + else: + return default, default + + class DimMode: - default = [] + default = {} # maps key to name def build(self, profile, owner, active=True, dims=None): if not dims: dims = self.default - return {name: make_dim(owner, name, active=active) for name in dims} + return {name: make_dim(owner, name=name, key=key, active=active) for name, key in dims.items()} class ForecastTimeDimMode(DimMode): @@ -393,25 +430,26 @@ class ForecastTimeDimMode(DimMode): TIMES = ["time", "dataTime"] def build(self, profile, owner, active=True): - ref_time = owner.dim_roles.get("forecast_reference_time", None) - if ref_time == "forecast_reference_time": - ref_time_dim = ForecastRefTimeDim(owner, active=active) - elif ref_time: - ref_time_dim = make_dim(owner, ref_time, active=active) + ref_time_key, ref_time_name = owner.dim_roles.role("forecast_reference_time", raise_error=False) + + if ref_time_key == "forecast_reference_time": + ref_time_dim = ForecastRefTimeDim(owner, name=ref_time_name, active=active) + elif ref_time_key: + ref_time_dim = make_dim(owner, name=ref_time_name, key=ref_time_key, active=active) else: - date = owner.dim_roles["date"] - time = owner.dim_roles["time"] + date, _ = owner.dim_roles.role("date") + time, _ = owner.dim_roles.role("time") built_in = date in self.DATES and time in self.TIMES if built_in: - ref_time_dim = ForecastRefTimeDim(owner, active=active) + ref_time_dim = ForecastRefTimeDim(owner, name=ref_time_name, active=active) else: - ref_time_dim = CustomForecastRefDim(owner, [date, time], active=active) + ref_time_dim = CustomForecastRefDim(owner, [date, time], name=ref_time_name, active=active) - step = owner.dim_roles["step"] - step_dim = make_dim(owner, step, active=active) + step_key, step_name = owner.dim_roles.role("step") + step_dim = make_dim(owner, name=step_name, key=step_key, active=active) - self.register_ref_time_key(ref_time_dim.name) - self.register_step_key(step_dim.name) + self.register_ref_time_key(ref_time_dim.key) + self.register_step_key(step_dim.key) return {d.name: d for d in [ref_time_dim, step_dim]} @@ -429,30 +467,33 @@ def register_step_key(self, name): class ValidTimeDimMode(DimMode): name = "valid_time" - default = ["valid_time"] + default = {"valid_time": "valid_time"} class RawTimeDimMode(DimMode): name = "raw" - default = ["date", "time", "step"] def build(self, profile, owner, active=True): - date = owner.dim_roles["date"] - time = owner.dim_roles["time"] - step = owner.dim_roles["step"] - return super().build(profile, owner, active=active, dims=[date, time, step]) + dims = {} + for k in ["date", "time", "step"]: + key, name = owner.dim_roles.role(k) + dims[name] = key + return super().build(profile, owner, active=active, dims=dims) class LevelDimMode(DimMode): name = "level" def build(self, profile, owner, **kwargs): - level_key = owner.dim_roles["level"] - level_type_key = owner.dim_roles["level_type"] - return { - level_key: LevelDim(owner, key=level_key, **kwargs), - level_type_key: LevelTypeDim(owner, key=level_type_key, **kwargs), - } + # level + key, name = owner.dim_roles.role("level") + level_dim = LevelDim(owner, name=name, key=key, **kwargs) + + # level_type + key, name = owner.dim_roles.role("level_type") + level_type_dim = LevelTypeDim(owner, name=name, key=key, **kwargs) + + return {level_dim.key: level_dim, level_type_dim.key: level_type_dim} class LevelAndTypeDimMode(DimMode): @@ -460,8 +501,9 @@ class LevelAndTypeDimMode(DimMode): dim = LevelAndTypeDim def build(self, profile, owner, **kwargs): - level_key = owner.dim_roles["level"] - level_type_key = owner.dim_roles["level_type"] + + level_key, _ = owner.dim_roles.role("level") + level_type_key, _ = owner.dim_roles.role("level_type") return {self.name: self.dim(owner, level_key, level_type_key, **kwargs)} @@ -486,8 +528,8 @@ class NumberDimBuilder(DimBuilder): name = "number" def __init__(self, profile, owner): - ens_key = owner.dim_roles["ens"] - self.used = {self.name: NumberDim(owner, key=ens_key)} + key, name = owner.dim_roles.role("number") + self.used = {self.name: NumberDim(owner, name=name, key=key)} class TimeDimBuilder(DimBuilder): @@ -523,7 +565,23 @@ def __init__(self, profile, owner): DIM_BUILDERS = {v.name: v for v in [NumberDimBuilder, TimeDimBuilder, LevelDimBuilder]} -class Dims: +def ensure_dim_map(d): + if isinstance(d, dict): + return d + d = ensure_iterable(d) + r = {} + for k in d: + if isinstance(k, str): + r[k] = k + elif isinstance(k, tuple) and len(k) == 2: + r[k[0]] = k[1] + elif isinstance(k, dict): + for kk, vv in k.items(): + r[kk] = vv + return r + + +class DimHandler: def __init__( self, profile, @@ -534,6 +592,7 @@ def __init__( split_dims, rename_dims, dim_roles, + dim_name_from_role_name, dims_as_attrs, time_dim_mode, level_dim_mode, @@ -542,11 +601,12 @@ def __init__( self.profile = profile - self.dim_roles = dim_roles - self.extra_dims = ensure_iterable(extra_dims) + self.dim_roles = DimRole(dim_roles, name_as_key=dim_name_from_role_name) + # self.dim_name_from_role_name = dim_name_from_role_name + self.extra_dims = ensure_dim_map(extra_dims) self.drop_dims = ensure_iterable(drop_dims) self.ensure_dims = ensure_iterable(ensure_dims) - self.fixed_dims = ensure_iterable(fixed_dims) + self.fixed_dims = ensure_dim_map(fixed_dims) self.split_dims = ensure_iterable(split_dims) self.rename_dims_map = ensure_dict(rename_dims) self.dims_as_attrs = list(ensure_iterable(dims_as_attrs)) @@ -554,6 +614,19 @@ def __init__( self.level_dim_mode = level_dim_mode self.squeeze = squeeze + # if "ens" in self.dim_roles: + # Warning.deprecated("'ens' key in dim_roles is deprecated. Use 'number' instead") + # self.dim_roles["number"] = self.dim_roles.pop("ens") + + # if self.dim_name_from_role_name: + # d = {v: k for k, v in self.dim_roles.items()} + # for k in list(self.rename_dims_map.keys()): + # if k in self.dim_roles: + # d[self.dim_roles[k]] = self.rename_dims_map.pop(k) + + # d.update(self.rename_dims_map) + # self.rename_dims_map = d + self.var_key_dim = None if self.fixed_dims: @@ -594,6 +667,28 @@ def __init__( self.dims = dims + # LOG.debug(f"self.dims={self.dims}") + + # for d in self.dims.values(): + # if d.name != d.key: + # if d.name in self.rename_dims_map: + # self.rename_dims_map[d.key] = d.name + # else d + # if d.key not in self.rename_dims_map: + # self.rename_dims_map[d.key] = d.name + # else d + + # if self.dim_name_from_role_name: + # d = {v: k for k, v in self.dim_roles.items()} + # for k in list(self.rename_dims_map.keys()): + # if k in self.dim_roles: + # d[self.dim_roles[k]] = self.rename_dims_map.pop(k) + + # d.update(self.rename_dims_map) + # self.rename_dims_map = d + + self.var_key_dim = None + # ensure all the required keys are in the profile keys = [] for d in self.dims.values(): @@ -640,8 +735,9 @@ def _init_fixed_dims(self): # ) # ) - self.ensure_dims = [k for k in self.fixed_dims] - dims = {k: make_dim(self, name=k) for k in self.fixed_dims} + # self.ensure_dims = [k for k in self.fixed_dims] + self.ensure_dims = list(self.fixed_dims.keys()) + dims = {k: make_dim(self, name=k, key=v) for k, v in self.fixed_dims.items()} return dims def _init_dims(self): @@ -667,7 +763,7 @@ def _remove_duplicates(keys): var_keys = [self.profile.variable_key] # non-core dims - keys = self.extra_dims + self.ensure_dims + keys = list(self.extra_dims.keys()) + self.ensure_dims keys = _remove_duplicates(keys) remapping_dims = self._init_remapping_dims(keys) @@ -806,6 +902,23 @@ def get_dims(self, names): r.append(make_dim(self, name=name)) return r + def rename_dataset_dims(self, dataset): + # first rename the dimensions where the name and key are different + mapping = {} + for d in self.dims.values(): + if d.key in dataset.dims and d.name != d.key: + mapping[d.key] = d.name + if mapping: + dataset = dataset.rename(mapping) + + # then apply the user defined rename_dims_map + if self.rename_dims_map: + mapping = {k: v for k, v in self.rename_dims_map.items() if k in dataset.dims} + if mapping: + dataset = dataset.rename(mapping) + + return dataset + PREDEFINED_DIMS = {} for i, d in enumerate( diff --git a/src/earthkit/data/utils/xarray/engine.py b/src/earthkit/data/utils/xarray/engine.py index dd4f10d24..7a8789841 100644 --- a/src/earthkit/data/utils/xarray/engine.py +++ b/src/earthkit/data/utils/xarray/engine.py @@ -29,6 +29,7 @@ def open_dataset( ensure_dims=None, fixed_dims=None, dim_roles=None, + dim_name_from_role_name=None, rename_dims=None, dims_as_attrs=None, time_dim_mode=None, @@ -71,18 +72,61 @@ def open_dataset( rename_variables: dict, None Mapping to rename variables. Default is None. extra_dims: str, or iterable of str, None - Metadata key or list of metadata keys to be used as additional dimensions on top of the - predefined dimensions. Only enabled when no ``fixed_dims`` is specified. Default is None. + Define additional dimensions on top of the predefined dimensions. Only enabled when no + ``fixed_dims`` is specified. Default is None. It can be a single item or a list. Each + item is either a metadata key, or a dict/tuple defining mapping between the dimension + name and the metadata key. The whole option can be a dict. E.g. + + .. code-block:: python + + # use key "expver" as a dimension + extra_dims = "expver" + # use keys "expver" and "steam" as a dimension + extra_dims = ["expver", "stream"] + # define dimensions "expver", mars_stream" and "mars_type" from + # metadata keys "expver", "stream" and "type" + extra_dims = [ + "expver", + {"mars_stream": "stream"}, + ("mars_type", "type"), + ] + extra_dims = [ + { + "expver": "expver", + "mars_stream": "stream", + "mars_type": "type", + } + ] + drop_dims: str, or iterable of str, None - Metadata key or list of metadata keys to be ignored as dimensions. Default is None. + Single or multiple dimensions to be ignored. Default is None. Default is None. ensure_dims: str, or iterable of str, None - Metadata key or list of metadata keys that should be used as dimensions even - when ``squeeze=True``. Default is None. + Dimension or dimensions that should be kept even when ``squeeze=True`` and their size + is only 1. Default is None. fixed_dims: str, or iterable of str, None - Metadata key or list of metadata keys in the order they should be used as dimensions. When - defined no other dimensions will be used. Might be incompatible with other settings. - Default is None. + Define all the dimensions to be generated. When used no other dimensions will be created. + Might be incompatible with other settings. Default is None. It can be a single item or a list. + Each item is either a metadata key, or a dict/tuple defining mapping between the dimension + name and the metadata key. The whole option can be a dict. E.g. + + .. code-block:: python + + # use key "step" as a dimension + fixed_dims = "step" + # use keys "step" and "levelist" as a dimension + extra_dims = ["step", "levelist"] + # define dimensions "step", level" and "level_type" from + # metadata keys "step", "levelist" and "levtype" + extra_dims = [ + "step", + {"level": "levelist"}, + ("level_type", "levtype"), + ] + extra_dims = [ + {"step": "step", "level": "levelist", "level_type": "levtype"} + ] + dim_roles: dict, None Specify the "roles" used to form the predefined dimensions. The predefined dimensions are automatically generated when no ``fixed_dims`` specified and comprise the following @@ -95,7 +139,7 @@ def open_dataset( ``dim_roles`` is a mapping between the "roles" and the metadata keys representing the roles. The possible roles are as follows: - - "ens": metadata key interpreted as ensemble forecast members + - "number": metadata key interpreted as ensemble forecast members - "date": metadata key interpreted as date part of the "forecast_reference_time" - "time": metadata key interpreted as time part of the "forecast_reference_time" - "step": metadata key interpreted as forecast step @@ -111,7 +155,7 @@ def open_dataset( .. code-block:: python { - "ens": "number", + "number": "number", "date": "dataDate", "time": "dataTime", "step": "step", @@ -123,7 +167,12 @@ def open_dataset( ``dims_roles`` behaves differently to the other kwargs in the sense that it does not override but update the default values. So e.g. to change only "ens" in - the defaults it is enough to specify: "dim_roles={"ens": "perturbationNumber"}. + the defaults it is enough to specify: "dim_roles={"number": "perturbationNumber"}. + dim_name_from_role_name: bool, None + If True, the dimension names are formed from the role names. Otherwise the + dimension names are formed from the metadata keys specified in ``dim_roles``. + Its default value (None) expands to True unless the ``profile`` overwrites it. + Only used when no `fixed_dims`` are specified. *New in version 0.15.0*. rename_dims: dict, None Mapping to rename dimensions. Default is None. dims_as_attrs: str, or iterable of str, None @@ -266,6 +315,7 @@ def open_dataset( fixed_dims=fixed_dims, rename_dims=rename_dims, dim_roles=dim_roles, + dim_name_from_role_name=dim_name_from_role_name, dims_as_attrs=dims_as_attrs, time_dim_mode=time_dim_mode, level_dim_mode=level_dim_mode, diff --git a/src/earthkit/data/utils/xarray/fieldlist.py b/src/earthkit/data/utils/xarray/fieldlist.py index 474cd62e5..c4d6c441e 100644 --- a/src/earthkit/data/utils/xarray/fieldlist.py +++ b/src/earthkit/data/utils/xarray/fieldlist.py @@ -7,7 +7,7 @@ # nor does it submit to any jurisdiction. # - +import datetime import logging from collections import defaultdict @@ -210,7 +210,7 @@ def unique_values(self, names, component=False): for k, v in vals.items(): v = [x for x in v if x is not None] - if all(isinstance(x, int) for x in v): + if all(isinstance(x, (int, datetime.timedelta)) for x in v): vals[k] = sorted(v) else: vals[k] = sorted(v, key=str) diff --git a/src/earthkit/data/utils/xarray/grib.yaml b/src/earthkit/data/utils/xarray/grib.yaml index 9384a00ac..05d544e9e 100644 --- a/src/earthkit/data/utils/xarray/grib.yaml +++ b/src/earthkit/data/utils/xarray/grib.yaml @@ -1,8 +1,8 @@ dim_roles: - ens: number + number: number date: dataDate time: dataTime - step: step + step: step_timedelta level: level level_type: typeOfLevel diff --git a/src/earthkit/data/utils/xarray/mars.yaml b/src/earthkit/data/utils/xarray/mars.yaml index 1d17dcdbf..5c07bfed6 100644 --- a/src/earthkit/data/utils/xarray/mars.yaml +++ b/src/earthkit/data/utils/xarray/mars.yaml @@ -1,8 +1,8 @@ dim_roles: - ens: number + number: number date: date time: time - step: step + step: step_timedelta level: levelist level_type: levtype diff --git a/src/earthkit/data/utils/xarray/profile.py b/src/earthkit/data/utils/xarray/profile.py index 7e1bbd843..bdf8b60f8 100644 --- a/src/earthkit/data/utils/xarray/profile.py +++ b/src/earthkit/data/utils/xarray/profile.py @@ -96,7 +96,7 @@ def __init__( **kwargs, ): from .attrs import Attrs - from .dim import Dims + from .dim import DimHandler self._kwargs = dict(**kwargs) self.name = name @@ -116,7 +116,7 @@ def __init__( self.rename_variables_map = kwargs.pop("rename_variables") # dims - self.dims = Dims( + self.dims = DimHandler( self, kwargs.pop("extra_dims"), kwargs.pop("drop_dims"), @@ -125,6 +125,7 @@ def __init__( kwargs.pop("split_dims"), kwargs.pop("rename_dims"), kwargs.pop("dim_roles"), + kwargs.pop("dim_name_from_role_name"), kwargs.pop("dims_as_attrs"), kwargs.pop("time_dim_mode"), kwargs.pop("level_dim_mode"), @@ -346,3 +347,6 @@ def rename_dims_map(self): def rename_variable(self, v): return self.rename_variables_map.get(v, v) + + def rename_dataset_dims(self, dataset): + return self.dims.rename_dataset_dims(dataset) diff --git a/tests/xr_engine/test_xr_attrs.py b/tests/xr_engine/test_xr_attrs.py index 92f1add25..5639ecd72 100644 --- a/tests/xr_engine/test_xr_attrs.py +++ b/tests/xr_engine/test_xr_attrs.py @@ -9,6 +9,7 @@ # nor does it submit to any jurisdiction. # +import datetime import os import sys @@ -47,14 +48,15 @@ def _get_attrs_for_key_2(key, metadata): "decode_times": False, "decode_timedelta": False, "strict": True, + "dim_name_from_role_name": False, }, { "date": [20240603, 20240604], "time": [0, 1200], - "step": [0, 6], + "step_timedelta": [datetime.timedelta(hours=0), datetime.timedelta(hours=6)], "levelist": [500, 700], }, - {"date": 2, "time": 2, "step": 2, "levelist": 2}, + {"date": 2, "time": 2, "step_timedelta": 2, "levelist": 2}, {}, ), ( @@ -65,14 +67,15 @@ def _get_attrs_for_key_2(key, metadata): "decode_times": False, "decode_timedelta": False, "strict": True, + "dim_name_from_role_name": False, }, { "date": [20240603, 20240604], "time": [0, 1200], - "step": [0, 6], + "step_timedelta": [datetime.timedelta(hours=0), datetime.timedelta(hours=6)], "levelist": [500, 700], }, - {"date": 2, "time": 2, "step": 2, "levelist": 2}, + {"date": 2, "time": 2, "step_timedelta": 2, "levelist": 2}, {"levtype": 2}, ), ( @@ -83,14 +86,15 @@ def _get_attrs_for_key_2(key, metadata): "decode_times": False, "decode_timedelta": False, "strict": False, + "dim_name_from_role_name": False, }, { "date": [20240603, 20240604], "time": [0, 1200], - "step": [0, 6], + "step_timedelta": [datetime.timedelta(hours=0), datetime.timedelta(hours=6)], "levelist": [500, 700], }, - {"date": 2, "time": 2, "step": 2, "levelist": 2}, + {"date": 2, "time": 2, "step_timedelta": 2, "levelist": 2}, {}, ), ( @@ -101,14 +105,15 @@ def _get_attrs_for_key_2(key, metadata): "decode_times": False, "decode_timedelta": False, "strict": False, + "dim_name_from_role_name": False, }, { "date": [20240603, 20240604], "time": [0, 1200], - "step": [0, 6], + "step_timedelta": [datetime.timedelta(hours=0), datetime.timedelta(hours=6)], "levelist": [500, 700], }, - {"date": 2, "time": 2, "step": 2, "levelist": 2}, + {"date": 2, "time": 2, "step_timedelta": 2, "levelist": 2}, {"levtype": 2}, ), ], @@ -151,14 +156,15 @@ def test_xr_dims_as_attrs(kwargs, coords, dims, attrs): "decode_times": False, "decode_timedelta": False, "strict": False, + "dim_name_from_role_name": False, }, { "date": [20240603, 20240604], "time": [0, 1200], - "step": [0, 6], + "step_timedelta": [datetime.timedelta(hours=0), datetime.timedelta(hours=6)], "levelist": [500, 700], }, - {"date": 2, "time": 2, "step": 2, "levelist": 2}, + {"date": 2, "time": 2, "step_timedelta": 2, "levelist": 2}, { "shortName": "t", "levtype": "pl", diff --git a/tests/xr_engine/test_xr_dims.py b/tests/xr_engine/test_xr_dims.py index d3f4e3e97..d2fb5dc97 100644 --- a/tests/xr_engine/test_xr_dims.py +++ b/tests/xr_engine/test_xr_dims.py @@ -9,6 +9,7 @@ # nor does it submit to any jurisdiction. # +import datetime import os import sys @@ -116,22 +117,25 @@ def test_xr_dims_input_fieldlist(): @pytest.mark.parametrize( "kwargs,var_key,variables,dim_keys", [ - ({}, "param", ["r", "t"], ["step", "levelist"]), + ({}, "param", ["r", "t"], ["step_timedelta", "levelist"]), ( - {"time_dim_mode": "forecast"}, + {"time_dim_mode": "forecast", "dim_name_from_role_name": False}, "param", ["r", "t"], - ["step", "levelist"], + ["step_timedelta", "levelist"], ), ( - {"squeeze": False, "time_dim_mode": "raw"}, + {"squeeze": False, "time_dim_mode": "raw", "dim_name_from_role_name": False}, "param", ["r", "t"], - ["time", "step", "levelist"], + ["time", "step_timedelta", "levelist"], ), ], ) def test_xr_dims_ds_lev(kwargs, var_key, variables, dim_keys): + """Test for the internal profile/dimension object. Cannot use all the options since + many tasks are performed elsewhere in the engine.""" + # TODO: consider removing this test prof = Profile.make("mars", **kwargs) ds = load_wrapped_fieldlist(DS_LEV, prof) # prof.update(ds, _attributes(ds)) @@ -144,128 +148,144 @@ def test_xr_dims_ds_lev(kwargs, var_key, variables, dim_keys): @pytest.mark.parametrize( "kwargs,var_key,variables,dims", [ - # ({"time_dim_mode": "raw"}, "param", ["r", "t"], ["date", "time", "step", "levelist"]), ( - {"time_dim_mode": "forecast"}, + {"time_dim_mode": "forecast", "dim_name_from_role_name": False}, "param", ["r", "t"], - ["forecast_reference_time", "step", "levelist", "levtype"], + ["forecast_reference_time", "step_timedelta", "levelist", "levtype"], ), ( - {"time_dim_mode": "raw", "variable_key": "param_level"}, + {"time_dim_mode": "raw", "variable_key": "param_level", "dim_name_from_role_name": False}, "param_level", ["r1000", "r850", "t1000", "t850"], - ["date", "time", "step", "levtype"], + ["date", "time", "step_timedelta", "levtype"], ), - # ( - # {"time_dim_mode": "raw", "extra_dims": "param_level"}, - # "param_level", - # [ - # "r1000", - # "r850", - # "t1000", - # "t850", - # ], - # ["date"], - # ), ( { "time_dim_mode": "raw", "variable_key": "param_level", "remapping": {"param_level": "{param}_{level}"}, + "dim_name_from_role_name": False, }, "param_level", ["r_1000", "r_850", "t_1000", "t_850"], - ["date", "time", "step", "levtype"], + ["date", "time", "step_timedelta", "levtype"], ), ( - {"time_dim_mode": "raw", "variable_key": "shortName"}, + {"time_dim_mode": "raw", "variable_key": "shortName", "dim_name_from_role_name": False}, "shortName", ["r", "t"], - ["date", "time", "step", "levelist", "levtype"], + ["date", "time", "step_timedelta", "levelist", "levtype"], ), ( - {"time_dim_mode": "raw", "variable_key": "shortName", "drop_variables": ["r"]}, + { + "time_dim_mode": "raw", + "variable_key": "shortName", + "drop_variables": ["r"], + "dim_name_from_role_name": False, + }, "shortName", ["t"], - ["date", "time", "step", "levelist", "levtype"], + ["date", "time", "step_timedelta", "levelist", "levtype"], ), ( - {"time_dim_mode": "raw", "variable_key": "param_level", "drop_variables": ["r", "r1000"]}, + { + "time_dim_mode": "raw", + "variable_key": "param_level", + "drop_variables": ["r", "r1000"], + "dim_name_from_role_name": False, + }, "param_level", ["r850", "t1000", "t850"], [ "date", "time", - "step", + "step_timedelta", "levtype", ], ), - # ( - # {"use_level_per_type_dim": True}, - # "param", - # ["r", "t"], - # {"date": ["20210101", "20210102"], "level_per_type": ["850pl", "1000pl"]}, - # ), ( - {"time_dim_mode": "raw", "level_dim_mode": "level_and_type"}, + {"time_dim_mode": "raw", "level_dim_mode": "level_and_type", "dim_name_from_role_name": False}, "param", ["r", "t"], { "date": ["20210101", "20210102"], "time": ["12"], - "step": [0], + "step_timedelta": [datetime.timedelta(hours=0)], "level_and_type": ["1000pl", "850pl"], }, ), ( - {"time_dim_mode": "raw", "extra_dims": "class"}, + {"time_dim_mode": "raw", "extra_dims": "class", "dim_name_from_role_name": False}, "param", ["r", "t"], { "class": ["od"], "date": ["20210101", "20210102"], "time": ["12"], - "step": [0], + "step_timedelta": [datetime.timedelta(hours=0)], "levelist": [850, 1000], "levtype": ["pl"], }, ), ( - {"time_dim_mode": "raw", "ensure_dims": "class"}, + {"time_dim_mode": "raw", "ensure_dims": "class", "dim_name_from_role_name": False}, "param", ["r", "t"], { "class": ["od"], "date": ["20210101", "20210102"], "time": ["12"], + "step_timedelta": [datetime.timedelta(hours=0)], + "levelist": [850, 1000], + "levtype": ["pl"], + }, + ), + ( + {"time_dim_mode": "raw", "ensure_dims": ["class", "step"], "dim_name_from_role_name": False}, + "param", + ["r", "t"], + { + "class": ["od"], "step": [0], + "date": ["20210101", "20210102"], + "time": ["12"], + "step_timedelta": [datetime.timedelta(hours=0)], "levelist": [850, 1000], "levtype": ["pl"], }, ), ( - {"time_dim_mode": "raw", "ensure_dims": ["class", "step"]}, + { + "time_dim_mode": "raw", + "ensure_dims": ["class", "step_timedelta"], + "dim_name_from_role_name": False, + }, "param", ["r", "t"], { "class": ["od"], "date": ["20210101", "20210102"], "time": ["12"], - "step": [0], + "step_timedelta": [datetime.timedelta(hours=0)], "levelist": [850, 1000], "levtype": ["pl"], }, ), ( - {"time_dim_mode": "raw", "extra_dims": "class", "squeeze": False}, + { + "time_dim_mode": "raw", + "extra_dims": "class", + "squeeze": False, + "dim_name_from_role_name": False, + }, "param", ["r", "t"], { "class": ["od"], "date": ["20210101", "20210102"], "time": ["12"], - "step": [0], + "step_timedelta": [datetime.timedelta(hours=0)], "levelist": [850, 1000], "levtype": ["pl"], }, @@ -273,6 +293,9 @@ def test_xr_dims_ds_lev(kwargs, var_key, variables, dim_keys): ], ) def test_xr_dims_ds_date_lev(kwargs, var_key, variables, dims): + """Test for the internal profile/dimension object. Cannot use all the options since + many tasks are performed elsewhere in the engine.""" + # TODO: consider removing this test prof = Profile.make("mars", **kwargs) ds = load_wrapped_fieldlist(DS_DATE_LEV, prof, remapping=prof.remapping.build()) @@ -303,13 +326,16 @@ def test_xr_dims_ds_date_lev(kwargs, var_key, variables, dims): {"time_dim_mode": "raw"}, "param", ["2t", "msl", "r", "t"], - ["date", "time", "step", "levelist", "levtype"], + ["date", "time", "step_timedelta", "levelist", "levtype"], ), # ({"base_datetime_dim": True}, "param", ["r", "t"], ["levelist", "levtype"]), # ({"squeeze": False}, "param", ["r", "t"], ["time", "step", "levelist", "levtype"]), ], ) def test_xr_dims_ds_sfc_and_pl(kwargs, var_key, variables, dim_keys): + """Test for the internal profile/dimension object. Cannot use all the options since + many tasks are performed elsewhere in the engine.""" + # TODO: consider removing this test prof = Profile.make("mars", **kwargs) ds = load_wrapped_fieldlist(DS_DATE_SFC_PL, prof) # prof.update(ds, _attributes(ds)) @@ -324,7 +350,20 @@ def test_xr_dims_ds_sfc_and_pl(kwargs, var_key, variables, dim_keys): "kwargs,dim_keys", [ ( - {"profile": "mars", "time_dim_mode": "raw", "rename_dims": {"levelist": "zz"}}, + { + "profile": "mars", + "time_dim_mode": "raw", + "rename_dims": {"levelist": "zz"}, + "dim_name_from_role_name": False, + }, + ["date", "time", "step_timedelta", "zz"], + ), + ( + { + "profile": "mars", + "time_dim_mode": "raw", + "rename_dims": {"level": "zz"}, + }, ["date", "time", "step", "zz"], ), ], @@ -339,3 +378,114 @@ def test_xr_rename_dims(kwargs, dim_keys): for v in ds: compare_dim_order(ds, dim_keys, v) + + +@pytest.mark.cache +@pytest.mark.parametrize( + "kwargs,dim_keys", + [ + ( + { + "profile": "mars", + "fixed_dims": ["date", "time", "step", "level"], + }, + ["date", "time", "step", "level"], + ), + ( + { + "profile": "mars", + "fixed_dims": ["level", "date", "time", "step"], + }, + ["level", "date", "time", "step"], + ), + ( + { + "profile": "mars", + "fixed_dims": [{"my_date": "date"}, ("my_time", "time"), "step", "level"], + }, + ["my_date", "my_time", "step", "level"], + ), + ( + { + "profile": "mars", + "fixed_dims": ["forecast_reference_time", "endStep", "level"], + }, + ["forecast_reference_time", "endStep", "level"], + ), + ( + { + "profile": "mars", + "fixed_dims": ["forecast_reference_time", ("step", "endStep"), "level"], + }, + ["forecast_reference_time", "step", "level"], + ), + ], +) +def test_xr_fixed_dims(kwargs, dim_keys): + ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl.grib")) + ds = ds_ek.to_xarray(**kwargs) + num = len(ds) + + dim_keys = dim_keys + ["latitude", "longitude"] + assert len(ds) == num + + for v in ds: + compare_dim_order(ds, dim_keys, v) + + +@pytest.mark.cache +@pytest.mark.parametrize( + "kwargs,dim_keys", + [ + ( + { + "profile": "mars", + "drop_dims": "number", + "time_dim_mode": "raw", + "squeeze": False, + "dim_name_from_role_name": True, + }, + ["date", "time", "step", "level", "level_type"], + ), + ( + { + "profile": "mars", + "drop_dims": ["level_type", "number"], + "time_dim_mode": "raw", + "squeeze": False, + "dim_name_from_role_name": True, + }, + ["date", "time", "step", "level"], + ), + ( + { + "profile": "mars", + "drop_dims": "number", + "time_dim_mode": "raw", + "squeeze": False, + "dim_name_from_role_name": False, + }, + ["date", "time", "step_timedelta", "levelist", "levtype"], + ), + ( + { + "profile": "mars", + "drop_dims": ["levtype", "number"], + "time_dim_mode": "raw", + "squeeze": False, + "dim_name_from_role_name": False, + }, + ["date", "time", "step_timedelta", "levelist"], + ), + ], +) +def test_xr_drop_dims(kwargs, dim_keys): + ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl.grib")) + ds = ds_ek.to_xarray(**kwargs) + num = len(ds) + + dim_keys = dim_keys + ["latitude", "longitude"] + assert len(ds) == num + + for v in ds: + compare_dim_order(ds, dim_keys, v) diff --git a/tests/xr_engine/test_xr_engine.py b/tests/xr_engine/test_xr_engine.py index f8d9be3e9..7c574b61a 100644 --- a/tests/xr_engine/test_xr_engine.py +++ b/tests/xr_engine/test_xr_engine.py @@ -20,6 +20,7 @@ here = os.path.dirname(__file__) sys.path.insert(0, here) +from xr_engine_fixtures import compare_coords # noqa: E402 from xr_engine_fixtures import load_grib_data # noqa: E402 @@ -63,12 +64,16 @@ def test_xr_engine_basic(file): @pytest.mark.cache @pytest.mark.parametrize("api", ["earthkit", "xr"]) -def test_xr_engine_detailed_check(api): +def test_xr_engine_detailed_check_1(api): ds_ek = from_source("url", earthkit_remote_test_data_file("test-data", "xr_engine", "level", "pl.grib")) if api == "earthkit": ds = ds_ek.to_xarray( - time_dim_mode="raw", decode_times=False, decode_timedelta=False, add_valid_time_coord=False + time_dim_mode="raw", + decode_times=False, + decode_timedelta=False, + add_valid_time_coord=False, + dim_name_from_role_name=False, ) else: import xarray as xr @@ -80,6 +85,7 @@ def test_xr_engine_detailed_check(api): decode_times=False, decode_timedelta=False, add_valid_time_coord=False, + dim_name_from_role_name=False, ) assert ds is not None @@ -92,7 +98,7 @@ def test_xr_engine_detailed_check(api): coords_ref_full = { "date": np.array([20240603, 20240604]), "time": np.array([0, 1200]), - "step": np.array([0, 6]), + "step_timedelta": [0, 6], "levelist": np.array([300, 400, 500, 700, 850, 1000]), "latitude": lats, "longitude": lons, @@ -101,16 +107,14 @@ def test_xr_engine_detailed_check(api): dims_ref_full = { "date": 2, "time": 2, - "step": 2, + "step_timedelta": 2, "levelist": 6, "latitude": 19, "longitude": 36, } assert len(ds.dims) == len(dims_ref_full) - assert len(ds.coords) == len(coords_ref_full) - for k, v in coords_ref_full.items(): - assert np.allclose(ds.coords[k].values, v) + compare_coords(ds, coords_ref_full) assert [v for v in ds.data_vars] == data_vars # data variable @@ -119,47 +123,37 @@ def test_xr_engine_detailed_check(api): assert ds["u"].as_numpy().shape == (2, 2, 2, 6, 19, 36) assert ds["u"].to_numpy().shape == (2, 2, 2, 6, 19, 36) r = ds["u"] - assert len(r.coords) == len(coords_ref_full) - for k, v in coords_ref_full.items(): - assert np.allclose(r.coords[k].values, v) + compare_coords(r, coords_ref_full) # sel() on dataset r = ds.sel(date=20240603, time=[0, 1200]) coords_ref = dict(coords_ref_full) coords_ref["date"] = np.array([20240603]) - assert len(r.coords) == len(coords_ref) - for k, v in coords_ref.items(): - assert np.allclose(r.coords[k].values, v) + compare_coords(r, coords_ref) assert [v for v in r.data_vars] == data_vars # sel() on data variable of filtered dataset assert r["u"].shape == (2, 2, 6, 19, 36) - r1 = r["u"].sel(step=6, levelist=[1000, 300]) + r1 = r["u"].sel(step_timedelta=6, levelist=[1000, 300]) assert r1.shape == (2, 2, 19, 36) - coords_ref["step"] = np.array([6]) + coords_ref["step_timedelta"] = [6] coords_ref["levelist"] = np.array([1000, 300]) - assert len(r1.coords) == len(coords_ref) - for k, v in coords_ref.items(): - assert np.allclose(r1.coords[k].values, v) + compare_coords(r1, coords_ref) # isel() on dataset r = ds.isel(date=0, time=[0, 1]) coords_ref = dict(coords_ref_full) coords_ref["date"] = np.array([20240603]) - assert len(r.coords) == len(coords_ref) - for k, v in coords_ref.items(): - assert np.allclose(r.coords[k].values, v) + compare_coords(r, coords_ref) assert [v for v in r.data_vars] == data_vars # isel() on data variable of filtered dataset assert r["u"].shape == (2, 2, 6, 19, 36) - r1 = r["u"].isel(step=1, levelist=[0, -1]) + r1 = r["u"].isel(step_timedelta=1, levelist=[0, -1]) assert r1.shape == (2, 2, 19, 36) - coords_ref["step"] = np.array([6]) + coords_ref["step_timedelta"] = [6] coords_ref["levelist"] = np.array([300, 1000]) - assert len(r1.coords) == len(coords_ref) - for k, v in coords_ref.items(): - assert np.allclose(r1.coords[k].values, v) + compare_coords(r1, coords_ref) # slicing of data variable da = ds["u"] @@ -173,8 +167,7 @@ def test_xr_engine_detailed_check(api): assert len(r.dims) == len(dims_ref) coords_ref = dict(coords_ref_full) coords_ref["time"] = np.array([0]) - for k, v in coords_ref.items(): - assert np.allclose(r.coords[k].values, v) + compare_coords(r, coords_ref) r = da[:, 0, :, 3:5] assert r.shape == (2, 2, 2, 19, 36) @@ -186,8 +179,7 @@ def test_xr_engine_detailed_check(api): coords_ref = dict(coords_ref_full) coords_ref["time"] = np.array([0]) coords_ref["levelist"] = np.array([700, 850]) - for k, v in coords_ref.items(): - assert np.allclose(r.coords[k].values, v) + compare_coords(r, coords_ref) r = da.loc[:, 0, :, [700, 850]] assert r.shape == (2, 2, 2, 19, 36) @@ -199,8 +191,184 @@ def test_xr_engine_detailed_check(api): coords_ref = dict(coords_ref_full) coords_ref["time"] = np.array([0]) coords_ref["levelist"] = np.array([700, 850]) - for k, v in coords_ref.items(): - assert np.allclose(r.coords[k].values, v) + compare_coords(r, coords_ref) + + # lat-lon + da = ds["t"] + + r = da[:, 0, :, 2, 9, 0] + assert r.shape == (2, 2) + vals_ref = np.array([[269.00918579, 268.78610229], [268.57771301, 268.08932495]]) + assert np.allclose(r.values, vals_ref) + + r = da[:, 0, :, 2, 9:12, :2] + assert r.shape == (2, 2, 3, 2) + vals_ref = np.array( + [ + [ + [ + [269.00918579, 269.31680298], + [269.70254517, 269.81387329], + [267.50527954, 266.83828735], + ], + [ + [268.78610229, 268.80758667], + [269.52731323, 269.75680542], + [266.61813354, 267.12106323], + ], + ], + [ + [ + [268.57771301, 269.03767395], + [269.33357239, 269.56111145], + [264.75154114, 266.55036926], + ], + [ + [268.08932495, 268.35983276], + [269.01803589, 269.02389526], + [264.29733276, 266.08248901], + ], + ], + ] + ) + assert np.allclose(r.values, vals_ref) + + r = da.loc[:, 0, :, 500, 0, 0] + assert r.shape == (2, 2) + vals_ref = np.array([[269.00918579, 268.78610229], [268.57771301, 268.08932495]]) + assert np.allclose(r.values, vals_ref) + + +@pytest.mark.cache +@pytest.mark.parametrize("api", ["earthkit", "xr"]) +def test_xr_engine_detailed_check_2(api): + ds_ek = from_source("url", earthkit_remote_test_data_file("test-data", "xr_engine", "level", "pl.grib")) + + if api == "earthkit": + ds = ds_ek.to_xarray( + time_dim_mode="raw", + decode_times=False, + decode_timedelta=False, + add_valid_time_coord=False, + dim_name_from_role_name=True, + ) + else: + import xarray as xr + + ds = xr.open_dataset( + ds_ek.path, + engine="earthkit", + time_dim_mode="raw", + decode_times=False, + decode_timedelta=False, + add_valid_time_coord=False, + dim_name_from_role_name=True, + ) + + assert ds is not None + + # dataset + lats = np.linspace(90, -90, 19) + lons = np.linspace(0, 350, 36) + data_vars = ["r", "t", "u", "v", "z"] + + coords_ref_full = { + "date": np.array([20240603, 20240604]), + "time": np.array([0, 1200]), + "step": [0, 6], + "level": np.array([300, 400, 500, 700, 850, 1000]), + "latitude": lats, + "longitude": lons, + } + + dims_ref_full = { + "date": 2, + "time": 2, + "step": 2, + "level": 6, + "latitude": 19, + "longitude": 36, + } + + assert len(ds.dims) == len(dims_ref_full) + compare_coords(ds, coords_ref_full) + assert [v for v in ds.data_vars] == data_vars + + # data variable + assert ds["u"].shape == (2, 2, 2, 6, 19, 36) + assert ds["u"].values.shape == (2, 2, 2, 6, 19, 36) + assert ds["u"].as_numpy().shape == (2, 2, 2, 6, 19, 36) + assert ds["u"].to_numpy().shape == (2, 2, 2, 6, 19, 36) + r = ds["u"] + compare_coords(r, coords_ref_full) + + # sel() on dataset + r = ds.sel(date=20240603, time=[0, 1200]) + coords_ref = dict(coords_ref_full) + coords_ref["date"] = np.array([20240603]) + compare_coords(r, coords_ref) + assert [v for v in r.data_vars] == data_vars + + # sel() on data variable of filtered dataset + assert r["u"].shape == (2, 2, 6, 19, 36) + r1 = r["u"].sel(step=6, level=[1000, 300]) + assert r1.shape == (2, 2, 19, 36) + coords_ref["step"] = [6] + coords_ref["level"] = np.array([1000, 300]) + compare_coords(r1, coords_ref) + + # isel() on dataset + r = ds.isel(date=0, time=[0, 1]) + coords_ref = dict(coords_ref_full) + coords_ref["date"] = np.array([20240603]) + compare_coords(r, coords_ref) + assert [v for v in r.data_vars] == data_vars + + # isel() on data variable of filtered dataset + assert r["u"].shape == (2, 2, 6, 19, 36) + r1 = r["u"].isel(step=1, level=[0, -1]) + assert r1.shape == (2, 2, 19, 36) + coords_ref["step"] = [6] + coords_ref["level"] = np.array([300, 1000]) + compare_coords(r1, coords_ref) + + # slicing of data variable + da = ds["u"] + + r = da[:, 0] + assert r.shape == (2, 2, 6, 19, 36) + assert r.values.shape == (2, 2, 6, 19, 36) + assert r.to_numpy().shape == (2, 2, 6, 19, 36) + dims_ref = dict(dims_ref_full) + dims_ref.pop("time") + assert len(r.dims) == len(dims_ref) + coords_ref = dict(coords_ref_full) + coords_ref["time"] = np.array([0]) + compare_coords(r, coords_ref) + + r = da[:, 0, :, 3:5] + assert r.shape == (2, 2, 2, 19, 36) + assert r.values.shape == (2, 2, 2, 19, 36) + assert r.to_numpy().shape == (2, 2, 2, 19, 36) + dims_ref = dict(dims_ref_full) + dims_ref.pop("time") + assert len(r.dims) == len(dims_ref) + coords_ref = dict(coords_ref_full) + coords_ref["time"] = np.array([0]) + coords_ref["level"] = np.array([700, 850]) + compare_coords(r, coords_ref) + + r = da.loc[:, 0, :, [700, 850]] + assert r.shape == (2, 2, 2, 19, 36) + assert r.values.shape == (2, 2, 2, 19, 36) + assert r.to_numpy().shape == (2, 2, 2, 19, 36) + dims_ref = dict(dims_ref_full) + dims_ref.pop("time") + assert len(r.dims) == len(dims_ref) + coords_ref = dict(coords_ref_full) + coords_ref["time"] = np.array([0]) + coords_ref["level"] = np.array([700, 850]) + compare_coords(r, coords_ref) # lat-lon da = ds["t"] @@ -253,7 +421,7 @@ def test_xr_engine_detailed_check(api): @pytest.mark.parametrize("lazy_load", [False, True]) @pytest.mark.parametrize("release_source", [False, True]) @pytest.mark.parametrize("direct_backend", [False, True]) -def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, direct_backend): +def test_xr_engine_detailed_flatten_check_1(stream, lazy_load, release_source, direct_backend): filename = "test-data/xr_engine/level/pl.grib" ds_ek, ds_ek_ref = load_grib_data(filename, "url", stream=stream) @@ -268,6 +436,7 @@ def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, dir "lazy_load": lazy_load, "release_source": release_source, "direct_backend": direct_backend, + "dim_name_from_role_name": False, } } } @@ -284,7 +453,7 @@ def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, dir coords_ref_full = { "date": np.array([20240603, 20240604]), "time": np.array([0, 1200]), - "step": np.array([0, 6]), + "step_timedelta": np.array([0, 6]), "levelist": np.array([300, 400, 500, 700, 850, 1000]), "latitude": lats, "longitude": lons, @@ -293,7 +462,7 @@ def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, dir dims_ref_full = { "date": 2, "time": 2, - "step": 2, + "step_timedelta": 2, "levelist": 6, "values": 684, } @@ -325,9 +494,9 @@ def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, dir # sel() on data variable of filtered dataset assert r["u"].shape == (2, 2, 6, 684) - r1 = r["u"].sel(step=6, levelist=[1000, 300]) + r1 = r["u"].sel(step_timedelta=6, levelist=[1000, 300]) assert r1.shape == (2, 2, 684) - coords_ref["step"] = np.array([6]) + coords_ref["step_timedelta"] = np.array([6]) coords_ref["levelist"] = np.array([1000, 300]) assert len(r1.coords) == len(coords_ref) for k, v in coords_ref.items(): @@ -344,9 +513,9 @@ def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, dir # isel() on data variable of filtered dataset assert r["u"].shape == (2, 2, 6, 684) - r1 = r["u"].isel(step=1, levelist=[0, -1]) + r1 = r["u"].isel(step_timedelta=1, levelist=[0, -1]) assert r1.shape == (2, 2, 684) - coords_ref["step"] = np.array([6]) + coords_ref["step_timedelta"] = np.array([6]) coords_ref["levelist"] = np.array([300, 1000]) assert len(r1.coords) == len(coords_ref) for k, v in coords_ref.items(): @@ -426,6 +595,170 @@ def test_xr_engine_detailed_flatten_check(stream, lazy_load, release_source, dir assert np.allclose(r.values, vals_ref) +@pytest.mark.cache +@pytest.mark.parametrize("stream", [False, True]) +@pytest.mark.parametrize("lazy_load", [False, True]) +@pytest.mark.parametrize("release_source", [False, True]) +@pytest.mark.parametrize("direct_backend", [False, True]) +def test_xr_engine_detailed_flatten_check_2(stream, lazy_load, release_source, direct_backend): + filename = "test-data/xr_engine/level/pl.grib" + ds_ek, ds_ek_ref = load_grib_data(filename, "url", stream=stream) + + kwargs = { + "xarray_open_dataset_kwargs": { + "backend_kwargs": { + "time_dim_mode": "raw", + "decode_times": False, + "decode_timedelta": False, + "flatten_values": True, + "add_valid_time_coord": False, + "lazy_load": lazy_load, + "release_source": release_source, + "direct_backend": direct_backend, + "dim_name_from_role_name": True, + } + } + } + + ds = ds_ek.to_xarray(**kwargs) + assert ds is not None + + # dataset + ll = ds_ek_ref[0].to_latlon(flatten=True) + lats = ll["lat"] + lons = ll["lon"] + data_vars = ["r", "t", "u", "v", "z"] + + coords_ref_full = { + "date": np.array([20240603, 20240604]), + "time": np.array([0, 1200]), + "step": np.array([0, 6]), + "level": np.array([300, 400, 500, 700, 850, 1000]), + "latitude": lats, + "longitude": lons, + } + + dims_ref_full = { + "date": 2, + "time": 2, + "step": 2, + "level": 6, + "values": 684, + } + + assert len(ds.dims) == len(dims_ref_full) + compare_coords(ds, coords_ref_full) + assert [v for v in ds.data_vars] == data_vars + + # data variable + assert ds["u"].shape == (2, 2, 2, 6, 684) + assert ds["u"].values.shape == (2, 2, 2, 6, 684) + assert ds["u"].as_numpy().shape == (2, 2, 2, 6, 684) + assert ds["u"].to_numpy().shape == (2, 2, 2, 6, 684) + r = ds["u"] + compare_coords(r, coords_ref_full) + + # sel() on dataset + r = ds.sel(date=20240603, time=[0, 1200]) + coords_ref = dict(coords_ref_full) + coords_ref["date"] = np.array([20240603]) + compare_coords(r, coords_ref) + assert [v for v in r.data_vars] == data_vars + + # sel() on data variable of filtered dataset + assert r["u"].shape == (2, 2, 6, 684) + r1 = r["u"].sel(step=6, level=[1000, 300]) + assert r1.shape == (2, 2, 684) + coords_ref["step"] = np.array([6]) + coords_ref["level"] = np.array([1000, 300]) + compare_coords(r1, coords_ref) + + # isel() on dataset + r = ds.isel(date=0, time=[0, 1]) + coords_ref = dict(coords_ref_full) + coords_ref["date"] = np.array([20240603]) + compare_coords(r, coords_ref) + assert [v for v in r.data_vars] == data_vars + + # isel() on data variable of filtered dataset + assert r["u"].shape == (2, 2, 6, 684) + r1 = r["u"].isel(step=1, level=[0, -1]) + assert r1.shape == (2, 2, 684) + coords_ref["step"] = np.array([6]) + coords_ref["level"] = np.array([300, 1000]) + compare_coords(r1, coords_ref) + + # slicing of data variable + da = ds["u"] + + r = da[:, 0] + assert r.shape == (2, 2, 6, 684) + assert r.values.shape == (2, 2, 6, 684) + assert r.to_numpy().shape == (2, 2, 6, 684) + dims_ref = dict(dims_ref_full) + dims_ref.pop("time") + assert len(r.dims) == len(dims_ref) + coords_ref = dict(coords_ref_full) + coords_ref["time"] = np.array([0]) + compare_coords(r, coords_ref) + + r = da[:, 0, :, 3:5] + assert r.shape == (2, 2, 2, 684) + assert r.values.shape == (2, 2, 2, 684) + assert r.to_numpy().shape == (2, 2, 2, 684) + dims_ref = dict(dims_ref_full) + dims_ref.pop("time") + assert len(r.dims) == len(dims_ref) + coords_ref = dict(coords_ref_full) + coords_ref["time"] = np.array([0]) + coords_ref["level"] = np.array([700, 850]) + compare_coords(r, coords_ref) + + r = da.loc[:, 0, :, [700, 850]] + assert r.shape == (2, 2, 2, 684) + assert r.values.shape == (2, 2, 2, 684) + assert r.to_numpy().shape == (2, 2, 2, 684) + dims_ref = dict(dims_ref_full) + dims_ref.pop("time") + assert len(r.dims) == len(dims_ref) + coords_ref = dict(coords_ref_full) + coords_ref["time"] = np.array([0]) + coords_ref["level"] = np.array([700, 850]) + compare_coords(r, coords_ref) + + # level=500, lat=0, lon=0 + da = ds["t"] + + r = da[:, 0, :, 2, 9 * 36 + 0] + assert r.shape == (2, 2) + vals_ref = np.array([[269.00918579, 268.78610229], [268.57771301, 268.08932495]]) + assert np.allclose(r.values, vals_ref) + + r = da[:, 0, :, 2, [9 * 36, 10 * 36, 11 * 36]] + assert r.shape == (2, 2, 3) + vals_ref = np.array( + [ + [ + [269.00918579, 269.70254517, 267.50527954], + [268.78610229, 269.52731323, 266.61813354], + ], + [ + [268.57771301, 269.33357239, 264.75154114], + [268.08932495, 269.01803589, 264.29733276], + ], + ] + ) + + v_ek = ds_ek_ref.sel(param="t", time=0, levelist=500).to_numpy(flatten=True) + assert np.allclose(r.values.flatten(), v_ek[:, [9 * 36, 10 * 36, 11 * 36]].flatten()) + assert np.allclose(r.values, vals_ref) + + r = da.loc[:, 0, :, 500, 9 * 36 + 0] + assert r.shape == (2, 2) + vals_ref = np.array([[269.00918579, 268.78610229], [268.57771301, 268.08932495]]) + assert np.allclose(r.values, vals_ref) + + @pytest.mark.cache @pytest.mark.parametrize( "kwargs", diff --git a/tests/xr_engine/test_xr_ens.py b/tests/xr_engine/test_xr_ens.py new file mode 100644 index 000000000..319155bf9 --- /dev/null +++ b/tests/xr_engine/test_xr_ens.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import os +import sys + +import pytest + +from earthkit.data import from_source +from earthkit.data.testing import earthkit_remote_test_data_file + +here = os.path.dirname(__file__) +sys.path.insert(0, here) +from xr_engine_fixtures import compare_dims # noqa: E402 + + +@pytest.mark.cache +@pytest.mark.parametrize( + "kwargs,dims", + [ + ( + {}, + { + "number": [0, 1, 2], + }, + ), + ( + { + "dim_roles": {"number": "perturbationNumber"}, + "dim_name_from_role_name": True, + }, + { + "number": [0, 1, 2], + }, + ), + ( + { + "dim_roles": {"ens": "perturbationNumber"}, + "dim_name_from_role_name": True, + }, + { + "number": [0, 1, 2], + }, + ), + ( + { + "dim_roles": {"number": "perturbationNumber"}, + "dim_name_from_role_name": False, + }, + { + "perturbationNumber": [0, 1, 2], + }, + ), + ], +) +def test_xr_number_dim(kwargs, dims): + ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/ens/ens_cf_pf.grib")) + + ds = ds_ek.to_xarray(**kwargs) + compare_dims(ds, dims, order_ref_var="t") diff --git a/tests/xr_engine/test_xr_level.py b/tests/xr_engine/test_xr_level.py index 2f3f4b91e..6d4260acb 100644 --- a/tests/xr_engine/test_xr_level.py +++ b/tests/xr_engine/test_xr_level.py @@ -28,11 +28,11 @@ "kwargs,dims", [ ( - {"profile": "mars", "level_dim_mode": "level"}, + {"profile": "mars", "level_dim_mode": "level", "dim_name_from_role_name": False}, {"levelist": [300, 400, 500, 700, 850, 1000]}, ), ( - {"profile": "mars", "level_dim_mode": "level_and_type"}, + {"profile": "mars", "level_dim_mode": "level_and_type", "dim_name_from_role_name": False}, {"level_and_type": ["1000pl", "300pl", "400pl", "500pl", "700pl", "850pl"]}, ), ], @@ -50,67 +50,117 @@ def test_xr_level_dim(kwargs, dims): [ ( "pl.grib", - {"profile": "grib", "level_dim_mode": "level"}, + {"profile": "grib", "level_dim_mode": "level", "dim_name_from_role_name": False}, {"level": [300, 400, 500, 700, 850, 1000]}, "isobaricInhPa", ), ( "pl_80_Pa.grib2", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [80]}, "isobaricInPa", ), ( "hpa_and_pa.grib", - {"profile": "mars", "level_dim_mode": "level", "ensure_dims": "levelist"}, + { + "profile": "mars", + "level_dim_mode": "level", + "ensure_dims": "levelist", + "dim_name_from_role_name": False, + }, {"levelist": [0.01, 0.1, 1]}, "pl", ), ( "hl_1000_m_asl.grib2", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [100, 1000, 2000, 3000]}, "heightAboveSea", ), ( "hl_1000_m_agr.grib2", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [500, 1000, 2500, 10000]}, "heightAboveGround", ), ( "pt_320_K.grib1", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [320]}, "theta", ), ( "pv_1500.grib1", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [1500]}, "potentialVorticity", ), ( "soil_7.grib1", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [7]}, "depthBelowLand", ), ( "sol_3.grib2", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [3]}, "snowLayer", ), ( "ml_77.grib2", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [77]}, "hybrid", ), ( "sfc.grib1", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [0]}, "surface", ), @@ -122,49 +172,84 @@ def test_xr_level_dim(kwargs, dims): # ), ( "mean_sea_level_reduced_ll.grib1", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [0]}, "meanSea", ), ( "gen_vert_layer.grib", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "level"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "level", + "dim_name_from_role_name": False, + }, {"level": [1]}, "generalVerticalLayer", ), ( "pl.grib", - {"profile": "mars", "level_dim_mode": "level"}, + {"profile": "mars", "level_dim_mode": "level", "dim_name_from_role_name": False}, {"levelist": [300, 400, 500, 700, 850, 1000]}, "pl", ), ( "pl_80_Pa.grib2", - {"profile": "mars", "level_dim_mode": "level", "ensure_dims": "levelist"}, + { + "profile": "mars", + "level_dim_mode": "level", + "ensure_dims": "levelist", + "dim_name_from_role_name": False, + }, {"levelist": [0.8]}, "pl", ), ( "pt_320_K.grib1", - {"profile": "mars", "level_dim_mode": "level", "ensure_dims": "levelist"}, + { + "profile": "mars", + "level_dim_mode": "level", + "ensure_dims": "levelist", + "dim_name_from_role_name": False, + }, {"levelist": [320]}, "pt", ), ( "pv_1500.grib1", - {"profile": "mars", "level_dim_mode": "level", "ensure_dims": "levelist"}, + { + "profile": "mars", + "level_dim_mode": "level", + "ensure_dims": "levelist", + "dim_name_from_role_name": False, + }, {"levelist": [1500]}, "pv", ), ( "sol_3.grib2", - {"profile": "grib", "level_dim_mode": "level", "ensure_dims": "levelist"}, + { + "profile": "grib", + "level_dim_mode": "level", + "ensure_dims": "levelist", + "dim_name_from_role_name": False, + }, {"levelist": [3]}, "sol", ), ( "hpa_and_pa.grib", - {"profile": "mars", "level_dim_mode": "level", "ensure_dims": "levelist"}, + { + "profile": "mars", + "level_dim_mode": "level", + "ensure_dims": "levelist", + "dim_name_from_role_name": False, + }, {"levelist": [0.01, 0.1, 1]}, "pl", ), diff --git a/tests/xr_engine/test_xr_remapping.py b/tests/xr_engine/test_xr_remapping.py index 859c51c6c..3011c0df9 100644 --- a/tests/xr_engine/test_xr_remapping.py +++ b/tests/xr_engine/test_xr_remapping.py @@ -38,18 +38,52 @@ def test_xr_remapping_1(): @pytest.mark.cache -def test_xr_remapping_2(): +@pytest.mark.parametrize( + "kwargs,coords,dims", + [ + ( + dict( + dim_roles={"level": "_k"}, + level_dim_mode="level", + remapping={"_k": "{levelist}_{levtype}"}, + dim_name_from_role_name=False, + ), + {"_k": ["500_pl", "700_pl"]}, + {"forecast_reference_time": 4, "step_timedelta": 2, "_k": 2, "latitude": 19, "longitude": 36}, + ), + ( + dict( + dim_roles={"level": "_k"}, + level_dim_mode="level", + remapping={"_k": "{levelist}_{levtype}"}, + dim_name_from_role_name=True, + ), + {"level": ["500_pl", "700_pl"]}, + {"forecast_reference_time": 4, "step": 2, "level": 2, "latitude": 19, "longitude": 36}, + ), + ( + dict( + dim_roles={"level": "_k"}, + level_dim_mode="level", + remapping={"_k": "{levelist}_{levtype}"}, + rename_dims={"level": "_k"}, + dim_name_from_role_name=True, + ), + {"_k": ["500_pl", "700_pl"]}, + {"forecast_reference_time": 4, "step": 2, "_k": 2, "latitude": 19, "longitude": 36}, + ), + ], +) +def test_xr_remapping_2(kwargs, coords, dims): ds0 = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl_small.grib")) - ds = ds0.to_xarray( - dim_roles={"level": "_k"}, level_dim_mode="level", remapping={"_k": "{levelist}_{levtype}"} - ) + ds = ds0.to_xarray(**kwargs) data_vars = ["r", "t"] assert [v for v in ds.data_vars] == data_vars - coords = {"_k": ["500_pl", "700_pl"]} + # coords = {"_k": ["500_pl", "700_pl"]} compare_coords(ds, coords) - dims = {"forecast_reference_time": 4, "step": 2, "_k": 2, "latitude": 19, "longitude": 36} + # dims = {"forecast_reference_time": 4, "step_timedelta": 2, "_k": 2, "latitude": 19, "longitude": 36} compare_dims(ds, dims, sizes=True) diff --git a/tests/xr_engine/test_xr_split.py b/tests/xr_engine/test_xr_split.py index abd42a4c0..af537cb9a 100644 --- a/tests/xr_engine/test_xr_split.py +++ b/tests/xr_engine/test_xr_split.py @@ -22,7 +22,7 @@ [ ( ["level", "pl.grib"], - {"time_dim_mode": "raw", "split_dims": ["step"]}, + {"time_dim_mode": "raw", "split_dims": ["step"], "dim_name_from_role_name": False}, 2, ["2t", "msl", "r", "t"], ["date", "time", "levelist"], @@ -30,7 +30,12 @@ ), ( ["level", "pl.grib"], - {"time_dim_mode": "raw", "split_dims": ["step"], "ensure_dims": "step"}, + { + "time_dim_mode": "raw", + "split_dims": ["step"], + "ensure_dims": "step", + "dim_name_from_role_name": False, + }, 2, ["2t", "msl", "r", "t"], ["date", "time", "step", "levelist"], @@ -38,7 +43,11 @@ ), ( ["cds-reanalysis-era5-single-levels-20230101-low-resol.grib"], - {"time_dim_mode": "valid_time", "split_dims": ["stream", "dataType", "edition", "Ni"]}, + { + "time_dim_mode": "valid_time", + "split_dims": ["stream", "dataType", "edition", "Ni"], + "dim_name_from_role_name": False, + }, 11, None, ["valid_time"], @@ -56,6 +65,27 @@ {"stream": "wave", "dataType": "an", "edition": 1, "Ni": 18}, ], ), + ( + ["level", "pl.grib"], + {"time_dim_mode": "raw", "split_dims": ["step"], "dim_name_from_role_name": True}, + 2, + ["2t", "msl", "r", "t"], + ["date", "time", "level"], + [{"step": 0}, {"step": 6}], + ), + ( + ["level", "pl.grib"], + { + "time_dim_mode": "raw", + "split_dims": ["step"], + "ensure_dims": "step", + "dim_name_from_role_name": True, + }, + 2, + ["2t", "msl", "r", "t"], + ["date", "time", "step", "level"], + [{"step": 0}, {"step": 6}], + ), # ({"base_datetime_dim": True}, "param", ["r", "t"], ["levelist"]), # ({"squeeze": False}, "param", ["r", "t"], ["time", "step", "levelist"]), ], diff --git a/tests/xr_engine/test_xr_time.py b/tests/xr_engine/test_xr_time.py index f81dc583e..ff8ebd5b4 100644 --- a/tests/xr_engine/test_xr_time.py +++ b/tests/xr_engine/test_xr_time.py @@ -26,22 +26,37 @@ @pytest.mark.cache @pytest.mark.parametrize( - "kwargs,dims", + "kwargs,dims,step_units", [ ( - {"time_dim_mode": "raw", "decode_times": False, "decode_timedelta": False}, + { + "time_dim_mode": "raw", + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, {"date": [20240603, 20240604], "time": [0, 1200], "step": [0, 6]}, + ("step", "hours"), ), ( - {"time_dim_mode": "raw"}, + { + "time_dim_mode": "raw", + "dim_name_from_role_name": True, + }, { "date": [np.datetime64("2024-06-03", "ns"), np.datetime64("2024-06-04", "ns")], "time": [np.timedelta64(0, "s"), np.timedelta64(43200, "s")], "step": [np.timedelta64(0, "h"), np.timedelta64(6, "h")], }, + None, ), ( - {"time_dim_mode": "forecast", "decode_times": False, "decode_timedelta": False}, + { + "time_dim_mode": "forecast", + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, { "forecast_reference_time": [ np.datetime64("2024-06-03T00", "ns"), @@ -51,9 +66,13 @@ ], "step": [0, 6], }, + ("step", "hours"), ), ( - {"time_dim_mode": "forecast"}, + { + "time_dim_mode": "forecast", + "dim_name_from_role_name": True, + }, { "forecast_reference_time": [ np.datetime64("2024-06-03T00", "ns"), @@ -63,9 +82,15 @@ ], "step": [np.timedelta64(0, "h"), np.timedelta64(6, "h")], }, + None, ), ( - {"time_dim_mode": "valid_time", "decode_times": False, "decode_timedelta": False}, + { + "time_dim_mode": "valid_time", + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, { "valid_time": [ np.datetime64("2024-06-03T00", "ns"), @@ -78,9 +103,15 @@ np.datetime64("2024-06-04T18", "ns"), ], }, + None, ), ( - {"time_dim_mode": "valid_time", "decode_times": True, "decode_timedelta": True}, + { + "time_dim_mode": "valid_time", + "decode_times": True, + "decode_timedelta": True, + "dim_name_from_role_name": True, + }, { "valid_time": [ np.datetime64("2024-06-03T00", "ns"), @@ -93,25 +124,54 @@ np.datetime64("2024-06-04T18", "ns"), ], }, + None, + ), + ( + { + "time_dim_mode": "raw", + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": False, + }, + {"date": [20240603, 20240604], "time": [0, 1200], "step_timedelta": [0, 6]}, + ("step_timedelta", "hours"), + ), + ( + { + "time_dim_mode": "raw", + "dim_name_from_role_name": False, + }, + { + "date": [np.datetime64("2024-06-03", "ns"), np.datetime64("2024-06-04", "ns")], + "time": [np.timedelta64(0, "s"), np.timedelta64(43200, "s")], + "step_timedelta": [np.timedelta64(0, "h"), np.timedelta64(6, "h")], + }, + None, ), ], ) -def test_xr_time_basic(kwargs, dims): +def test_xr_time_basic(kwargs, dims, step_units): ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl.grib")) ds = ds_ek.to_xarray(**kwargs) compare_dims(ds, dims, order_ref_var="t") + if step_units is not None: + assert ( + ds[step_units[0]].attrs["units"] == step_units[1] + ), f"step units mismatch {ds[step_units[0]].attrs['units']} != {step_units[1]}" + @pytest.mark.cache @pytest.mark.parametrize( - "kwargs,dims", + "kwargs,dims,step_units", [ ( { "dim_roles": {"date": "indexingDate", "time": "indexingTime", "step": "forecastMonth"}, "decode_times": False, "decode_timedelta": False, + "dim_name_from_role_name": False, }, { "indexing_time": [ @@ -120,12 +180,14 @@ def test_xr_time_basic(kwargs, dims): ], "forecastMonth": [1, 2, 3], }, + ("forecastMonth", "months"), ), ( { "dim_roles": {"forecast_reference_time": "indexing_time", "step": "forecastMonth"}, "decode_times": False, "decode_timedelta": False, + "dim_name_from_role_name": False, }, { "indexing_time": [ @@ -134,10 +196,43 @@ def test_xr_time_basic(kwargs, dims): ], "forecastMonth": [1, 2, 3], }, + ("forecastMonth", "months"), + ), + ( + { + "dim_roles": {"date": "indexingDate", "time": "indexingTime", "step": "forecastMonth"}, + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, + { + "forecast_reference_time": [ + np.datetime64("2014-09-01", "ns"), + np.datetime64("2014-10-01", "ns"), + ], + "step": [1, 2, 3], + }, + ("step", "months"), + ), + ( + { + "dim_roles": {"forecast_reference_time": "indexing_time", "step": "forecastMonth"}, + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, + { + "forecast_reference_time": [ + np.datetime64("2014-09-01", "ns"), + np.datetime64("2014-10-01", "ns"), + ], + "step": [1, 2, 3], + }, + ("step", "months"), ), ], ) -def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): +def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims, step_units): ds_ek = from_source( "url", earthkit_remote_test_data_file("test-data/xr_engine/date/jma_seasonal_fc_ref_time_per_member.grib"), @@ -146,10 +241,15 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): ds = ds_ek.to_xarray(**kwargs) compare_dims(ds, dims, order_ref_var="2t") + if step_units is not None: + assert ( + ds[step_units[0]].attrs["units"] == step_units[1] + ), f"step units mismatch {ds[step_units[0]].attrs['units']} != {step_units[1]}" + @pytest.mark.cache @pytest.mark.parametrize( - "kwargs,dims", + "kwargs,dims,step_units", [ ( { @@ -157,6 +257,7 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): "dim_roles": {"step": "forecastMonth"}, "decode_times": False, "decode_timedelta": False, + "dim_name_from_role_name": False, }, { "number": [0, 1, 2], @@ -168,6 +269,7 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): ], "forecastMonth": [1, 2, 3, 4, 5, 6], }, + ("forecastMonth", "months"), ), ( { @@ -175,6 +277,7 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): "dim_roles": {"step": "fcmonth"}, "decode_times": False, "decode_timedelta": False, + "dim_name_from_role_name": False, }, { "number": [0, 1, 2], @@ -186,6 +289,7 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): ], "fcmonth": [1, 2, 3, 4, 5, 6], }, + ("fcmonth", "months"), ), ( { @@ -194,6 +298,7 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): "decode_times": False, "decode_timedelta": False, "ensure_dims": ["number", "date", "time", "forecastMonth"], + "dim_name_from_role_name": False, }, { "number": [0, 1, 2], @@ -206,10 +311,73 @@ def test_xr_time_seasonal_monthly_indexing_date(kwargs, dims): "time": [np.timedelta64(0, "s")], "forecastMonth": [1, 2, 3, 4, 5, 6], }, + ("forecastMonth", "months"), + ), + ( + { + "time_dim_mode": "forecast", + "dim_roles": {"step": "forecastMonth"}, + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, + { + "number": [0, 1, 2], + "forecast_reference_time": [ + np.datetime64("1993-10-01", "ns"), + np.datetime64("1994-10-01", "ns"), + np.datetime64("1995-10-01", "ns"), + np.datetime64("1996-10-01", "ns"), + ], + "step": [1, 2, 3, 4, 5, 6], + }, + ("step", "months"), + ), + ( + { + "time_dim_mode": "forecast", + "dim_roles": {"step": "fcmonth"}, + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, + { + "number": [0, 1, 2], + "forecast_reference_time": [ + np.datetime64("1993-10-01", "ns"), + np.datetime64("1994-10-01", "ns"), + np.datetime64("1995-10-01", "ns"), + np.datetime64("1996-10-01", "ns"), + ], + "step": [1, 2, 3, 4, 5, 6], + }, + ("step", "months"), + ), + ( + { + "time_dim_mode": "raw", + "dim_roles": {"step": "forecastMonth"}, + "decode_times": False, + "decode_timedelta": False, + "ensure_dims": ["number", "date", "time", "step"], + "dim_name_from_role_name": True, + }, + { + "number": [0, 1, 2], + "date": [ + np.datetime64("1993-10-01", "ns"), + np.datetime64("1994-10-01", "ns"), + np.datetime64("1995-10-01", "ns"), + np.datetime64("1996-10-01", "ns"), + ], + "time": [np.timedelta64(0, "s")], + "step": [1, 2, 3, 4, 5, 6], + }, + ("step", "months"), ), ], ) -def test_xr_time_seasonal_monthly_simple(kwargs, dims): +def test_xr_time_seasonal_monthly_simple(kwargs, dims, step_units): ds_ek = from_source( "url", earthkit_remote_test_data_file("test-data/xr_engine/date/seasonal_monthly.grib"), @@ -218,32 +386,195 @@ def test_xr_time_seasonal_monthly_simple(kwargs, dims): ds = ds_ek.to_xarray(**kwargs) compare_dims(ds, dims, order_ref_var="2t") + if step_units is not None: + assert ( + ds[step_units[0]].attrs["units"] == step_units[1] + ), f"step units mismatch {ds[step_units[0]].attrs['units']} != {step_units[1]}" + @pytest.mark.cache -def test_xr_valid_time_coord(): +@pytest.mark.parametrize( + "kwargs,dims,step_units,coords", + [ + ( + { + "time_dim_mode": "forecast", + "add_valid_time_coord": True, + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": True, + }, + { + "forecast_reference_time": [ + np.datetime64("2024-06-03T00", "ns"), + np.datetime64("2024-06-03T12", "ns"), + ], + "step": [0, 6], + }, + ("step", "hours"), + { + "valid_time": [ + [np.datetime64("2024-06-03T00", "ns"), np.datetime64("2024-06-03T06", "ns")], + [np.datetime64("2024-06-03T12", "ns"), np.datetime64("2024-06-03T18", "ns")], + ] + }, + ), + ( + { + "fixed_dims": ["level", "forecast_reference_time", "step"], + "add_valid_time_coord": True, + "decode_times": False, + "decode_timedelta": False, + }, + { + "forecast_reference_time": [ + np.datetime64("2024-06-03T00", "ns"), + np.datetime64("2024-06-03T12", "ns"), + ], + "step": [0, 6], + }, + ("step", "hours"), + { + "valid_time": [ + [np.datetime64("2024-06-03T00", "ns"), np.datetime64("2024-06-03T06", "ns")], + [np.datetime64("2024-06-03T12", "ns"), np.datetime64("2024-06-03T18", "ns")], + ] + }, + ), + ( + { + "fixed_dims": ["level", "step", "forecast_reference_time"], + "add_valid_time_coord": True, + "decode_times": False, + "decode_timedelta": False, + }, + { + "step": [0, 6], + "forecast_reference_time": [ + np.datetime64("2024-06-03T00", "ns"), + np.datetime64("2024-06-03T12", "ns"), + ], + }, + ("step", "hours"), + { + "valid_time": [ + [np.datetime64("2024-06-03T00", "ns"), np.datetime64("2024-06-03T12", "ns")], + [np.datetime64("2024-06-03T06", "ns"), np.datetime64("2024-06-03T18", "ns")], + ] + }, + ), + ( + { + "time_dim_mode": "forecast", + "add_valid_time_coord": True, + "decode_times": False, + "decode_timedelta": False, + "dim_name_from_role_name": False, + }, + { + "forecast_reference_time": [ + np.datetime64("2024-06-03T00", "ns"), + np.datetime64("2024-06-03T12", "ns"), + ], + "step_timedelta": [0, 6], + }, + ("step_timedelta", "hours"), + { + "valid_time": [ + [np.datetime64("2024-06-03T00", "ns"), np.datetime64("2024-06-03T06", "ns")], + [np.datetime64("2024-06-03T12", "ns"), np.datetime64("2024-06-03T18", "ns")], + ] + }, + ), + ], +) +def test_xr_valid_time_coord(kwargs, dims, step_units, coords): ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl_small.grib")).sel( date=20240603, time=[0, 1200] ) - ds = ds_ek.to_xarray( - time_dim_mode="forecast", add_valid_time_coord=True, decode_times=False, decode_timedelta=False - ) + ds = ds_ek.to_xarray(**kwargs) - dims = { - "forecast_reference_time": [ - np.datetime64("2024-06-03T00", "ns"), - np.datetime64("2024-06-03T12", "ns"), - ], - "step": [0, 6], - } compare_dims(ds, dims, order_ref_var="t") vt = ds.coords["valid_time"] - assert vt.dims == ("forecast_reference_time", "step") + assert vt.dims == tuple(dims.keys()) + + compare_coords(ds, coords) - ref = [ - [np.datetime64("2024-06-03T00", "ns"), np.datetime64("2024-06-03T06", "ns")], - [np.datetime64("2024-06-03T12", "ns"), np.datetime64("2024-06-03T18", "ns")], - ] + if step_units is not None: + assert ( + ds[step_units[0]].attrs["units"] == step_units[1] + ), f"step units mismatch {ds[step_units[0]].attrs['units']} != {step_units[1]}" + + +@pytest.mark.cache +@pytest.mark.parametrize( + "kwargs,dims,step_units", + [ + ( + { + "time_dim_mode": "raw", + "dim_name_from_role_name": True, + "ensure_dims": ["date", "time", "step"], + }, + { + "date": [np.datetime64("2011-12-15", "ns")], + "time": [np.timedelta64(12, "h")], + "step": [ + np.timedelta64(12, "h"), + np.timedelta64(18, "h"), + np.timedelta64(24, "h"), + np.timedelta64(30, "h"), + np.timedelta64(36, "h"), + ], + }, + None, + ), + ], +) +def test_xr_time_step_range_1(kwargs, dims, step_units): + ds_ek = from_source( + "url", earthkit_remote_test_data_file("test-data/xr_engine/date/wgust_step_range.grib1") + ) + + ds = ds_ek.to_xarray(**kwargs) + compare_dims(ds, dims, order_ref_var="10fg6") + + if step_units is not None: + assert ( + ds[step_units[0]].attrs["units"] == step_units[1] + ), f"step units mismatch {ds[step_units[0]].attrs['units']} != {step_units[1]}" + + +@pytest.mark.cache +@pytest.mark.parametrize( + "kwargs,dims,step_units", + [ + ( + { + "time_dim_mode": "raw", + "dim_name_from_role_name": True, + "ensure_dims": ["date", "time", "step"], + }, + { + "date": [np.datetime64("2025-05-27", "ns")], + "time": [np.timedelta64(0, "ns")], + "step": [np.timedelta64(72, "h"), np.timedelta64(73, "h")], + }, + None, + ), + ], +) +def test_xr_time_step_range_2(kwargs, dims, step_units): + ds_ek = from_source( + "url", earthkit_remote_test_data_file("test-data/xr_engine/date/lsp_step_range.grib2") + ) + + ds = ds_ek.to_xarray(**kwargs) + compare_dims(ds, dims, order_ref_var="lsp") - compare_coords(ds, {"valid_time": ref}) + if step_units is not None: + assert ( + ds[step_units[0]].attrs["units"] == step_units[1] + ), f"step units mismatch {ds[step_units[0]].attrs['units']} != {step_units[1]}" diff --git a/tests/xr_engine/test_xr_write.py b/tests/xr_engine/test_xr_write.py index 118bb8c5e..0154690dc 100644 --- a/tests/xr_engine/test_xr_write.py +++ b/tests/xr_engine/test_xr_write.py @@ -211,6 +211,7 @@ def test_xr_write_seasonal(): ds = ds_ek.to_xarray( time_dim_mode="forecast", dim_roles={"date": "indexingDate", "time": "indexingTime", "step": "forecastMonth"}, + dim_name_from_role_name=False, ) import xarray as xr diff --git a/tests/xr_engine/xr_engine_fixtures.py b/tests/xr_engine/xr_engine_fixtures.py index 713f95ef4..208819f43 100644 --- a/tests/xr_engine/xr_engine_fixtures.py +++ b/tests/xr_engine/xr_engine_fixtures.py @@ -90,7 +90,7 @@ def compare_coord(ds, name, ref_vals, mode="coord"): assert np.allclose(ds.coords[name].values, vals), f"{name=} {ds.coords[name].values} != {vals}" -def compare_dim_order(ds, dims, order_ref_var): +def compare_dim_order(ds, dims, order_ref_var, check_coord=True): if order_ref_var is None: return @@ -98,6 +98,8 @@ def compare_dim_order(ds, dims, order_ref_var): for d in ds[order_ref_var].dims: if d in dims: dim_order.append(d) + if check_coord: + assert d in ds.coords, f"{d} not in {ds.coords}" if isinstance(dims, dict): assert dim_order == list(dims.keys()), f"{dim_order=} != {list(dims.keys())}" From 17ed541eac752d54639fd227b915bf8e24efdc05 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Tue, 10 Jun 2025 18:12:23 +0100 Subject: [PATCH 09/17] Add zarr target (#716) * Add zarr target --- docs/examples/grib_to_zarr_target.ipynb | 822 ++++++++++++++++++++++++ docs/examples/index.rst | 1 + docs/guide/targets/index.rst | 1 + docs/guide/targets/to_target.rst | 23 + src/earthkit/data/encoders/zarr.py | 80 +++ src/earthkit/data/targets/zarr.py | 51 ++ src/earthkit/data/testing.py | 10 + tests/sources/test_zarr.py | 17 +- tests/targets/test_target_zarr.py | 65 ++ 9 files changed, 1064 insertions(+), 6 deletions(-) create mode 100644 docs/examples/grib_to_zarr_target.ipynb create mode 100644 src/earthkit/data/encoders/zarr.py create mode 100644 src/earthkit/data/targets/zarr.py create mode 100644 tests/targets/test_target_zarr.py diff --git a/docs/examples/grib_to_zarr_target.ipynb b/docs/examples/grib_to_zarr_target.ipynb new file mode 100644 index 000000000..1df45a6fc --- /dev/null +++ b/docs/examples/grib_to_zarr_target.ipynb @@ -0,0 +1,822 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8e308cd3-7f5a-4b62-bd2d-027850282c00", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "## Writing GRIB data to Zarr" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "62b00621-67cd-46b0-81ef-16278a6eee18", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8d75e667605a4e4eb22967f6a3d6e9c6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "pl.grib: 0%| | 0.00/48.8k [00:00\n", + "#T_ca541 th {\n", + " text-align: left;\n", + "}\n", + "#T_ca541_row0_col0, #T_ca541_row0_col1, #T_ca541_row0_col2, #T_ca541_row0_col3, #T_ca541_row0_col4, #T_ca541_row0_col5, #T_ca541_row0_col6, #T_ca541_row0_col7, #T_ca541_row0_col8, #T_ca541_row1_col0, #T_ca541_row1_col1, #T_ca541_row1_col2, #T_ca541_row1_col3, #T_ca541_row1_col4, #T_ca541_row1_col5, #T_ca541_row1_col6, #T_ca541_row1_col7, #T_ca541_row1_col8 {\n", + " text-align: left;\n", + "}\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
      leveldatetimestepparamIdclassstreamtypeexperimentVersionNumber
    shortNametypeOfLevel         
    risobaricInhPa700,50020240603,202406040,12000,6157odoperfc0001
    tisobaricInhPa700,50020240603,202406040,12000,6130odoperfc0001
    \n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds.describe()" + ] + }, + { + "cell_type": "markdown", + "id": "2ab4d979-7c02-42e4-9b52-8ec12e76853b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "#### Using to_target() on the data object" + ] + }, + { + "cell_type": "raw", + "id": "60ee891e-f0cd-426c-8f26-8155e9c25381", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We use :func:`to_target` to write the GRIB fieldlist/field into a zarr store. First, the data is converted to Xarray then :py:func:`xarray.Dataset.to_zarr` is called to generate the zarr store. We need to set the kwargs accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e3ff25d0-bce4-4cfc-bdd0-93ca0864d08d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/homebrew/Caskroom/miniforge/base/envs/dev/lib/python3.11/site-packages/zarr/api/asynchronous.py:205: UserWarning: Consolidated metadata is currently not part in the Zarr format 3 specification. It may not be supported by other zarr implementations and may change in the future.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# with these options each field will be a separate chunk\n", + "ds.to_target(\"zarr\", \n", + " earthkit_to_xarray_kwargs={\"chunks\": {\"forecast_reference_time\": 1, \n", + " \"step\": 1, \n", + " \"level\": 1}},\n", + " xarray_to_zarr_kwargs={\"store\": \"_pl.zarr\", \"mode\": \"w\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3ffafb60-c412-4560-9bea-089143bcf85d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/html": [ + "
    /\n",
    +       "├── forecast_reference_time (4,) int64\n",
    +       "├── latitude (19,) float64\n",
    +       "├── level (2,) int64\n",
    +       "├── longitude (36,) float64\n",
    +       "├── r (4, 2, 2, 19, 36) float64\n",
    +       "├── step (2,) int64\n",
    +       "└── t (4, 2, 2, 19, 36) float64\n",
    +       "
    \n" + ], + "text/plain": [ + "\u001b[1m/\u001b[0m\n", + "├── \u001b[1mforecast_reference_time\u001b[0m (4,) int64\n", + "├── \u001b[1mlatitude\u001b[0m (19,) float64\n", + "├── \u001b[1mlevel\u001b[0m (2,) int64\n", + "├── \u001b[1mlongitude\u001b[0m (36,) float64\n", + "├── \u001b[1mr\u001b[0m (4, 2, 2, 19, 36) float64\n", + "├── \u001b[1mstep\u001b[0m (2,) int64\n", + "└── \u001b[1mt\u001b[0m (4, 2, 2, 19, 36) float64\n" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import zarr \n", + "root = zarr.group(\"_pl.zarr\")\n", + "root.tree()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "515bc071-d45f-48aa-abea-0cc688f4eebc", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Type : Array\n", + "Zarr format : 3\n", + "Data type : DataType.float64\n", + "Shape : (4, 2, 2, 19, 36)\n", + "Chunk shape : (1, 1, 1, 19, 36)\n", + "Order : C\n", + "Read-only : False\n", + "Store type : LocalStore\n", + "Filters : ()\n", + "Serializer : BytesCodec(endian=)\n", + "Compressors : (ZstdCodec(level=0, checksum=False),)\n", + "No. bytes : 87552 (85.5K)" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "root[\"t\"].info" + ] + }, + { + "cell_type": "markdown", + "id": "ef19bc33-fcc7-4b5a-83b0-ee59da4179f0", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "The zarr store can be loaded to Xarray to check its content." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "706d4467-64b8-46bf-894d-346088208fa2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/93/w0p869rx17q98wxk83gn9ys40000gn/T/ipykernel_45349/754541422.py:2: FutureWarning: In a future version of xarray decode_timedelta will default to False rather than None. To silence this warning, set decode_timedelta to True, False, or a 'CFTimedeltaCoder' instance.\n", + " xarray.open_dataset(\"_pl.zarr\")\n" + ] + }, + { + "data": { + "text/html": [ + "
    \n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
    <xarray.Dataset> Size: 176kB\n",
    +       "Dimensions:                  (step: 2, longitude: 36,\n",
    +       "                              forecast_reference_time: 4, latitude: 19, level: 2)\n",
    +       "Coordinates:\n",
    +       "  * step                     (step) timedelta64[ns] 16B 00:00:00 06:00:00\n",
    +       "  * longitude                (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n",
    +       "  * forecast_reference_time  (forecast_reference_time) datetime64[ns] 32B 202...\n",
    +       "  * latitude                 (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n",
    +       "  * level                    (level) int64 16B 500 700\n",
    +       "Data variables:\n",
    +       "    r                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "    t                        (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n",
    +       "Attributes:\n",
    +       "    class:        od\n",
    +       "    stream:       oper\n",
    +       "    levtype:      pl\n",
    +       "    type:         fc\n",
    +       "    expver:       0001\n",
    +       "    date:         20240603\n",
    +       "    time:         0\n",
    +       "    domain:       g\n",
    +       "    number:       0\n",
    +       "    Conventions:  CF-1.8\n",
    +       "    institution:  ECMWF
    " + ], + "text/plain": [ + " Size: 176kB\n", + "Dimensions: (step: 2, longitude: 36,\n", + " forecast_reference_time: 4, latitude: 19, level: 2)\n", + "Coordinates:\n", + " * step (step) timedelta64[ns] 16B 00:00:00 06:00:00\n", + " * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0\n", + " * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202...\n", + " * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0\n", + " * level (level) int64 16B 500 700\n", + "Data variables:\n", + " r (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + " t (forecast_reference_time, step, level, latitude, longitude) float64 88kB ...\n", + "Attributes:\n", + " class: od\n", + " stream: oper\n", + " levtype: pl\n", + " type: fc\n", + " expver: 0001\n", + " date: 20240603\n", + " time: 0\n", + " domain: g\n", + " number: 0\n", + " Conventions: CF-1.8\n", + " institution: ECMWF" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import xarray\n", + "xarray.open_dataset(\"_pl.zarr\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8479a12e-e907-43de-a3a6-aefb8cbfa754", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dev", + "language": "python", + "name": "dev" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/index.rst b/docs/examples/index.rst index c945dddd4..727becb00 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -181,6 +181,7 @@ Targets and encoders grib_to_file_pattern_target.ipynb grib_to_fdb_target.ipynb grib_to_geotiff.ipynb + grib_to_zarr_target.ipynb grib_encoder.ipynb Miscellaneous diff --git a/docs/guide/targets/index.rst b/docs/guide/targets/index.rst index 2611967ff..2efcb80f2 100644 --- a/docs/guide/targets/index.rst +++ b/docs/guide/targets/index.rst @@ -23,6 +23,7 @@ Examples - :ref:`/examples/grib_to_file_pattern_target.ipynb` - :ref:`/examples/grib_to_fdb_target.ipynb` - :ref:`/examples/grib_to_geotiff.ipynb` + - :ref:`/examples/grib_to_zarr_target.ipynb` Overview diff --git a/docs/guide/targets/to_target.rst b/docs/guide/targets/to_target.rst index f39d4d33d..8c943527f 100644 --- a/docs/guide/targets/to_target.rst +++ b/docs/guide/targets/to_target.rst @@ -39,6 +39,10 @@ Built in targets * - :ref:`targets-fdb` - add data to a `Fields DataBase `_ (FDB) - :py:class:`~data.targets.FDBTarget` + * - :ref:`targets-zarr` + - add data to a `zarr `_ store + - :py:class:`~data.targets.ZarrTarget` + .. _targets-file: @@ -170,6 +174,25 @@ fdb - :ref:`/examples/grib_to_fdb_target.ipynb` +.. _targets-zarr: + +zarr +---- + +.. py:function:: to_target("zarr", earthkit_to_xarray_kwargs=None, xarray_to_zarr_kwargs=None, data=None) + :noindex: + + The ``zarr`` target writes to a `zarr `_ store. + + :param dict earthkit_to_xarray_kwargs: the keyword arguments passed to the :func:`to_xarray` function. If not provided, the default values are used. + :param dict xarray_to_zarr_kwargs: the keyword arguments passed to the :py:func:`xarray.Dataset.to_zarr` function. As a bare minimum, the ``store`` keyword argument must be provided. + :param data: specify the data to write. Cannot be set when :func:`to_target` is called on a data object. + + This target converts the data to an :ref:`xarray.Dataset ` and then writes it to a zarr store using the :py:func:`xarray.Dataset.to_zarr` function. The conversion to an xarray dataset is done by the :func:`to_xarray` function. + + Notebook examples: + + - :ref:`/examples/grib_to_zarr_target.ipynb` .. .. _data-targets-multio: diff --git a/src/earthkit/data/encoders/zarr.py b/src/earthkit/data/encoders/zarr.py new file mode 100644 index 000000000..2e9b05085 --- /dev/null +++ b/src/earthkit/data/encoders/zarr.py @@ -0,0 +1,80 @@ +# (C) Copyright 2023 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import logging + +from . import EncodedData +from . import Encoder + +LOG = logging.getLogger(__name__) + + +class ZarrEncodedData(EncodedData): + def __init__(self, ds): + self.ds = ds + + def to_bytes(self): + return None + + def to_file(self, f): + return None + + def to_xarray(self): + return self.ds + + def metadata(self, key): + raise NotImplementedError + + +class ZarrEncoder(Encoder): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def encode( + self, + data=None, + **kwargs, + ): + if data is not None: + from earthkit.data.wrappers import get_wrapper + + data = get_wrapper(data) + return data._encode(self, **kwargs) + else: + raise ValueError("No data to encode") + + def _encode( + self, + data=None, + values=None, + min=None, + max=None, + check_nans=False, + metadata={}, + template=None, + # return_bytes=False, + missing_value=9999, + **kwargs, + ): + return ZarrEncodedData(data.to_xarray(add_earthkit_attrs=False)) + + def _encode_field(self, field, **kwargs): + raise NotImplementedError("ZarrEncoder does not support encoding individual fields.") + + def _encode_fieldlist(self, data, **kwargs): + earthkit_to_xarray_kwargs = kwargs.pop("earthkit_to_xarray_kwargs", {}) + # earthkit_to_xarray_kwargs.update(kwargs) + earthkit_to_xarray_kwargs["add_earthkit_attrs"] = False + kwargs = earthkit_to_xarray_kwargs + + ds = data.to_xarray(**kwargs) + return ZarrEncodedData(ds) + + +encoder = ZarrEncoder diff --git a/src/earthkit/data/targets/zarr.py b/src/earthkit/data/targets/zarr.py new file mode 100644 index 000000000..9bb4fdd6f --- /dev/null +++ b/src/earthkit/data/targets/zarr.py @@ -0,0 +1,51 @@ +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + +import logging + +from . import SimpleTarget + +LOG = logging.getLogger(__name__) + + +class ZarrTarget(SimpleTarget): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._zarr_kwargs = kwargs + self._ekd_kwargs = kwargs.pop("earthkit_to_xarray_kwargs", {}) + self._xr_kwargs = kwargs.pop("xarray_to_zarr_kwargs", {}) + self._encoder = "zarr" + + def close(self): + """Close the target and flush the fdb. + + The target will not be able to write anymore. + + Raises: + ------- + ValueError: If the target is already closed. + """ + pass + + def flush(self): + """Flush the fdb. + + Raises: + ------- + ValueError: If the target is already closed. + """ + pass + + def _write(self, data, **kwargs): + r = self._encode(data, earthkit_to_xarray_kwargs=self._ekd_kwargs) + ds = r.to_xarray() + ds.to_zarr(**self._xr_kwargs) + + +target = ZarrTarget diff --git a/src/earthkit/data/testing.py b/src/earthkit/data/testing.py index e9b08d28b..115a87ced 100644 --- a/src/earthkit/data/testing.py +++ b/src/earthkit/data/testing.py @@ -133,6 +133,16 @@ def modules_installed(*modules): NO_ECFS = True +NO_ZARR = True +try: + import zarr # noqa + + if int(zarr.__version__.split(".")[0]) >= 3: + NO_ZARR = False +except Exception: + pass + + def MISSING(*modules): return not modules_installed(*modules) diff --git a/tests/sources/test_zarr.py b/tests/sources/test_zarr.py index f84a58693..109cf2d82 100644 --- a/tests/sources/test_zarr.py +++ b/tests/sources/test_zarr.py @@ -1,16 +1,21 @@ -import importlib.util +#!/usr/bin/env python3 + +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# import pytest from earthkit.data import from_source from earthkit.data.readers.netcdf.field import XArrayField +from earthkit.data.testing import NO_ZARR from earthkit.data.testing import earthkit_test_data_file -if importlib.util.find_spec("zarr") is not None: - NO_ZARR = False -else: - NO_ZARR = True - @pytest.mark.skipif(NO_ZARR, reason="Zarr not installed") def test_zarr_source(): diff --git a/tests/targets/test_target_zarr.py b/tests/targets/test_target_zarr.py new file mode 100644 index 000000000..fed4cb6b2 --- /dev/null +++ b/tests/targets/test_target_zarr.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +# (C) Copyright 2020 ECMWF. +# +# This software is licensed under the terms of the Apache Licence Version 2.0 +# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0. +# In applying this licence, ECMWF does not waive the privileges and immunities +# granted to it by virtue of its status as an intergovernmental organisation +# nor does it submit to any jurisdiction. +# + + +import os + +import pytest + +from earthkit.data import from_source +from earthkit.data.core.temporary import temp_directory +from earthkit.data.targets import to_target +from earthkit.data.testing import NO_ZARR + + +@pytest.mark.skipif(NO_ZARR, reason="Zarr not installed") +@pytest.mark.cache +@pytest.mark.parametrize("direct_call", [True, False]) +def test_target_zarr_from_grib(direct_call): + ds = from_source("sample", "pl.grib") + + with temp_directory() as tmp: + path = os.path.join(tmp, "_res.zarr") + + if direct_call: + to_target( + "zarr", + earthkit_to_xarray_kwargs={"chunks": {"forecast_reference_time": 1, "step": 1, "level": 1}}, + xarray_to_zarr_kwargs={"store": path, "mode": "w"}, + data=ds, + ) + else: + ds.to_target( + "zarr", + earthkit_to_xarray_kwargs={"chunks": {"forecast_reference_time": 1, "step": 1, "level": 1}}, + xarray_to_zarr_kwargs={"store": path, "mode": "w"}, + ) + + import zarr + + root = zarr.group(path) + assert root + + shapes = { + "t": (4, 2, 2, 19, 36), + "r": (4, 2, 2, 19, 36), + "forecast_reference_time": (4,), + "step": (2,), + "level": (2,), + "latitude": (19,), + "longitude": (36,), + } + + for k in ["t", "r", "forecast_reference_time", "step", "level", "latitude", "longitude"]: + k in root, f"Key {k} not found in Zarr root" + assert ( + root[k].shape == shapes[k] + ), f"Shape mismatch for {k}: expected {shapes[k]}, got {root[k].shape}" From 1126d80dc1eb3eb10fc185a76e758a83e96b6136 Mon Sep 17 00:00:00 2001 From: Peter Tsrunchev Date: Fri, 13 Jun 2025 09:57:25 +0100 Subject: [PATCH 10/17] build: update polytope-client (#717) Enables using ecmwfapirc for authentication --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f48e966d9..adda5f583 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ optional-dependencies.geopandas = [ "geopandas" ] optional-dependencies.geotiff = [ "pyproj", "rasterio", "rioxarray" ] optional-dependencies.mars = [ "ecmwf-api-client>=1.6.1" ] optional-dependencies.odb = [ "pyodc" ] -optional-dependencies.polytope = [ "polytope-client>=0.7.4" ] +optional-dependencies.polytope = [ "polytope-client>=0.7.6" ] optional-dependencies.projection = [ "cartopy" ] optional-dependencies.s3 = [ "aws-requests-auth", "botocore" ] optional-dependencies.test = [ From 2520474044cad995e871a16573a197a5d295f912 Mon Sep 17 00:00:00 2001 From: Kai Kratz Date: Mon, 16 Jun 2025 15:25:00 +0200 Subject: [PATCH 11/17] Pin libaec to v1.1.3 --- .github/ci-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ci-config.yml b/.github/ci-config.yml index 6138e6362..a115e249a 100644 --- a/.github/ci-config.yml +++ b/.github/ci-config.yml @@ -1,6 +1,6 @@ dependencies: | ecmwf/ecbuild - MathisRosenhauer/libaec@master + MathisRosenhauer/libaec@refs/tags/v1.1.3 ecmwf/eccodes ecmwf/eckit ecmwf/odc From 50232e91b64fa0373c0b0ca14fb9e18cccf52a52 Mon Sep 17 00:00:00 2001 From: Kai Kratz Date: Mon, 16 Jun 2025 16:08:52 +0200 Subject: [PATCH 12/17] Pin libaec to v1.1.3 for hpc ci --- .github/ci-hpc-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ci-hpc-config.yml b/.github/ci-hpc-config.yml index 8a5eb009e..c7a7073ee 100644 --- a/.github/ci-hpc-config.yml +++ b/.github/ci-hpc-config.yml @@ -3,7 +3,7 @@ build: - ninja dependencies: - ecmwf/ecbuild@develop - - MathisRosenhauer/libaec@master + - MathisRosenhauer/libaec@refs/tags/v1.1.3 - ecmwf/eccodes@develop - ecmwf/eckit@develop - ecmwf/odc@develop From 8666f3ae9e8ab4b940c59adb9513c1423b3858ef Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Tue, 17 Jun 2025 11:23:45 +0100 Subject: [PATCH 13/17] Rename xarray-zarr source and add docs (#723) * Rename xarray-zarr source and add docs --------- Co-authored-by: oisin-m --- docs/guide/sources.rst | 18 ++++++++++++++++++ docs/guide/targets/to_target.rst | 4 ++-- docs/release_notes/version_0.15_updates.rst | 8 +++++--- src/earthkit/data/readers/directory.py | 2 +- .../data/sources/{xarray_zarr.py => zarr.py} | 0 tests/sources/test_zarr.py | 2 +- 6 files changed, 27 insertions(+), 7 deletions(-) rename src/earthkit/data/sources/{xarray_zarr.py => zarr.py} (100%) diff --git a/docs/guide/sources.rst b/docs/guide/sources.rst index 6599d892d..d9d4f10b2 100644 --- a/docs/guide/sources.rst +++ b/docs/guide/sources.rst @@ -66,6 +66,8 @@ We can get data from a given source by using :func:`from_source`: - retrieve data from `WEkEO`_ using the WEkEO grammar * - :ref:`data-sources-wekeocds` - retrieve `CDS `_ data stored on `WEkEO`_ using the `cdsapi`_ grammar + * - :ref:`data-sources-zarr` + - load data from a `Zarr `_ store ---------------------------------- @@ -1229,6 +1231,22 @@ wekeocds - :ref:`/examples/wekeo.ipynb` + +.. _data-sources-zarr: + +zarr +-------- + +.. py:function:: from_source("zarr", path) + :noindex: + + The ``zarr`` source accesses data from a `Zarr `_ store. Internally the data is loaded via the :py:meth:`xarray.open_zarr` method. So only Zarr data supported by Xarray can be accessed. + + :param str path: path or URL to the Zarr store + + + + .. _MARS catalog: https://apps.ecmwf.int/archive-catalogue/ .. _MARS user documentation: https://confluence.ecmwf.int/display/UDOC/MARS+user+documentation .. _web API: https://www.ecmwf.int/en/forecasts/access-forecasts/ecmwf-web-api diff --git a/docs/guide/targets/to_target.rst b/docs/guide/targets/to_target.rst index 8c943527f..1a4de625f 100644 --- a/docs/guide/targets/to_target.rst +++ b/docs/guide/targets/to_target.rst @@ -182,13 +182,13 @@ zarr .. py:function:: to_target("zarr", earthkit_to_xarray_kwargs=None, xarray_to_zarr_kwargs=None, data=None) :noindex: - The ``zarr`` target writes to a `zarr `_ store. + The ``zarr`` target writes to a `Zarr `_ store. :param dict earthkit_to_xarray_kwargs: the keyword arguments passed to the :func:`to_xarray` function. If not provided, the default values are used. :param dict xarray_to_zarr_kwargs: the keyword arguments passed to the :py:func:`xarray.Dataset.to_zarr` function. As a bare minimum, the ``store`` keyword argument must be provided. :param data: specify the data to write. Cannot be set when :func:`to_target` is called on a data object. - This target converts the data to an :ref:`xarray.Dataset ` and then writes it to a zarr store using the :py:func:`xarray.Dataset.to_zarr` function. The conversion to an xarray dataset is done by the :func:`to_xarray` function. + This target converts the data to an :ref:`xarray.Dataset ` and then writes it to a Zarr store using the :py:func:`xarray.Dataset.to_zarr` function. The conversion to an Xarray dataset is done by the :func:`to_xarray` function. Notebook examples: diff --git a/docs/release_notes/version_0.15_updates.rst b/docs/release_notes/version_0.15_updates.rst index f53d1e020..bb09ffd41 100644 --- a/docs/release_notes/version_0.15_updates.rst +++ b/docs/release_notes/version_0.15_updates.rst @@ -44,9 +44,9 @@ Other changes New Xarray engine notebooks ------------------------------ -- :ref:`/examples/xr_engine_step_range.ipynb` -- :ref:`/examples/xr_engine_ensemble.ipynb` -- :ref:`/examples/xr_engine_squeeze.ipynb` +- :ref:`/examples/xarray_engine_step_ranges.ipynb` +- :ref:`/examples/xarray_engine_ensemble.ipynb` +- :ref:`/examples/xarray_engine_squeeze.ipynb` - :ref:`/examples/xarray_engine_chunks.ipynb` - :ref:`/examples/list_of_dicts_to_xarray.ipynb` @@ -55,6 +55,8 @@ New Xarray engine notebooks New features +++++++++++++++++ +- Added :ref:`zarr ` source to read Zarr data (:pr:`675`). +- Added the :ref:`targets-zarr` target (:pr:`716`). See the :ref:`/examples/grib_to_zarr_target.ipynb` notebook example. - Added new config option ``grib-file-serialisation-policy`` to control how GRIB data on disk is pickled. The options are "path" and "memory". The default is "path". Previously, only "memory" was implemented (:pr:`700`). - Added serialisation to GRIB fields (both on disk and in-memory) (:pr:`700`) diff --git a/src/earthkit/data/readers/directory.py b/src/earthkit/data/readers/directory.py index a15af57be..c5d72c425 100644 --- a/src/earthkit/data/readers/directory.py +++ b/src/earthkit/data/readers/directory.py @@ -69,7 +69,7 @@ def mutate_source(self): ): if self.stream: raise ValueError("Cannot stream zarr directories") - return from_source("xarray-zarr", self.path) + return from_source("zarr", self.path) if len(self._content) == 1: return from_source( diff --git a/src/earthkit/data/sources/xarray_zarr.py b/src/earthkit/data/sources/zarr.py similarity index 100% rename from src/earthkit/data/sources/xarray_zarr.py rename to src/earthkit/data/sources/zarr.py diff --git a/tests/sources/test_zarr.py b/tests/sources/test_zarr.py index 109cf2d82..4348e972c 100644 --- a/tests/sources/test_zarr.py +++ b/tests/sources/test_zarr.py @@ -19,7 +19,7 @@ @pytest.mark.skipif(NO_ZARR, reason="Zarr not installed") def test_zarr_source(): - ds = from_source("xarray-zarr", earthkit_test_data_file("test_zarr/")) + ds = from_source("zarr", earthkit_test_data_file("test_zarr/")) assert len(ds) == 4 assert isinstance(ds[0], XArrayField) assert isinstance(ds[1], XArrayField) From cad23234e9649422017e411b437e589259e0e47b Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Tue, 17 Jun 2025 11:58:31 +0100 Subject: [PATCH 14/17] Update docs (#726) --- docs/guide/sources.rst | 4 +++- docs/install.rst | 28 ++++------------------------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/docs/guide/sources.rst b/docs/guide/sources.rst index d9d4f10b2..9819a9b04 100644 --- a/docs/guide/sources.rst +++ b/docs/guide/sources.rst @@ -1240,7 +1240,9 @@ zarr .. py:function:: from_source("zarr", path) :noindex: - The ``zarr`` source accesses data from a `Zarr `_ store. Internally the data is loaded via the :py:meth:`xarray.open_zarr` method. So only Zarr data supported by Xarray can be accessed. + *New in version 0.15.0* + + The ``zarr`` source accesses data from a `Zarr `_ store. Internally the data is loaded via the :py:meth:`xarray.open_zarr` method, so only Zarr data supported by Xarray can be accessed. Requires ``zarr >= 3`` version. :param str path: path or URL to the Zarr store diff --git a/docs/install.rst b/docs/install.rst index 260825fe3..e9be3ef16 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -9,7 +9,7 @@ Installing from PyPI Minimal installation +++++++++++++++++++++++++ -Install **earthkit-data** with python3 (>= 3.8) and ``pip`` as follows: +Install **earthkit-data** with python3 (>= 3.9) and ``pip`` as follows: .. code-block:: bash @@ -20,7 +20,7 @@ The package installed like this is **minimal** supporting only GRIB and NetCDF d Installing all the optional packages ++++++++++++++++++++++++++++++++++++++++ -You can install **earthkit-data** with all the optional packages (with the exception of "geotiff" dependencies, see below) in one go by using: +You can install **earthkit-data** with all the optional packages (with the exception of the "geotiff" and "zarr" dependencies, see below) in one go by using: .. code-block:: bash @@ -51,6 +51,7 @@ Alternatively, you can install the following components: - covjsonkit: provides access to CoverageJSON data served by the :ref:`data-sources-polytope` source - s3: provides access to non-public :ref:`s3 ` buckets (new in version *0.11.0*) - geotiff: adds GeoTIFF support (new in version *0.11.0*). Please note that this is not included in the ``[all]`` option and has to be invoked separately. + - zarr: provides access to the :ref:`data-sources-zarr` source (new in version *0.15.0*). Please note that this is not included in the ``[all]`` option and has to be invoked separately. E.g. to add :ref:`data-sources-mars` support you can use: @@ -76,31 +77,10 @@ Install **earthkit-data** via ``conda`` with: This will bring in some necessary binary dependencies for you. + Installing the binary dependencies -------------------------------------- -ecCodes -+++++++++++ - -**earthkit-data** depends on the ECMWF :xref:`eccodes` library -that must be installed on the system and accessible as a shared library. - -When earthkit-data is installed from ``conda`` ecCodes will **also be installed** for you. Otherwise, you need to install it using one of the following methods: - - - The easiest way to install it is to use ``conda``: - - .. code-block:: bash - - conda install eccodes -c conda-forge - - - On a MacOS it is also available from ``HomeBrew``: - - .. code-block:: bash - - brew install eccodes - - - As an alternative you may install the official source distribution by following the instructions `here `_. - FDB +++++ From 51e11ac29f8db07389dad19a174732a7dd5ec78d Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Mon, 23 Jun 2025 16:02:53 +0100 Subject: [PATCH 15/17] Update release notes 0.15 (#728) * Update release notes 0.15 --- docs/examples/grib_to_netcdf.ipynb | 141 +++++++++++++++++--- docs/examples/xarray_engine_level.ipynb | 2 +- docs/guide/xarray/overview.rst | 19 ++- docs/release_notes/version_0.15_updates.rst | 39 +++++- 4 files changed, 179 insertions(+), 22 deletions(-) diff --git a/docs/examples/grib_to_netcdf.ipynb b/docs/examples/grib_to_netcdf.ipynb index 991a9117b..681bf814f 100644 --- a/docs/examples/grib_to_netcdf.ipynb +++ b/docs/examples/grib_to_netcdf.ipynb @@ -26,6 +26,14 @@ "ds = ekd.from_source(\"file\", \"tuv_pl.grib\")" ] }, + { + "cell_type": "markdown", + "id": "fac6cee2-845e-4d34-94f0-cdf53a757e5a", + "metadata": {}, + "source": [ + "#### Using the earthkit accessor" + ] + }, { "cell_type": "raw", "id": "d125684b-20c9-49d9-a87c-7880814b60fc", @@ -38,7 +46,7 @@ "tags": [] }, "source": [ - "To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` then generate NetCDF from it with :py:meth:`xarray.Dataset.to_netcdf`. Earthkit-data attaches some special attributes to the generated Xarray dataset that we do not want to write to NetCDF. In order to achieve this we need to call :py:meth:`xarray.Dataset.to_netcdf` on the \"earthkit\" accessor and not directly on the Xarray dataset." + "To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` then generate NetCDF from it with :py:meth:`xarray.Dataset.to_netcdf`. Earthkit-data attaches some special attributes to the generated Xarray dataset that cannot be written to NetCDF. In order to make ``to_netcdf`` work we need to call it on the \"earthkit\" accessor and not directly on the Xarray dataset." ] }, { @@ -75,37 +83,37 @@ "text": [ "netcdf _tuv_pl {\n", "dimensions:\n", - "\tlevelist = 6 ;\n", + "\tlevel = 6 ;\n", "\tlatitude = 7 ;\n", "\tlongitude = 12 ;\n", "variables:\n", - "\tdouble t(levelist, latitude, longitude) ;\n", + "\tdouble t(level, latitude, longitude) ;\n", "\t\tt:_FillValue = NaN ;\n", "\t\tt:param = \"t\" ;\n", "\t\tt:standard_name = \"air_temperature\" ;\n", "\t\tt:long_name = \"Temperature\" ;\n", "\t\tt:paramId = 130LL ;\n", "\t\tt:units = \"K\" ;\n", - "\tdouble u(levelist, latitude, longitude) ;\n", + "\tdouble u(level, latitude, longitude) ;\n", "\t\tu:_FillValue = NaN ;\n", "\t\tu:param = \"u\" ;\n", "\t\tu:standard_name = \"eastward_wind\" ;\n", "\t\tu:long_name = \"U component of wind\" ;\n", "\t\tu:paramId = 131LL ;\n", "\t\tu:units = \"m s**-1\" ;\n", - "\tdouble v(levelist, latitude, longitude) ;\n", + "\tdouble v(level, latitude, longitude) ;\n", "\t\tv:_FillValue = NaN ;\n", "\t\tv:param = \"v\" ;\n", "\t\tv:standard_name = \"northward_wind\" ;\n", "\t\tv:long_name = \"V component of wind\" ;\n", "\t\tv:paramId = 132LL ;\n", "\t\tv:units = \"m s**-1\" ;\n", - "\tint64 levelist(levelist) ;\n", - "\t\tlevelist:units = \"hPa\" ;\n", - "\t\tlevelist:positive = \"down\" ;\n", - "\t\tlevelist:stored_direction = \"decreasing\" ;\n", - "\t\tlevelist:standard_name = \"air_pressure\" ;\n", - "\t\tlevelist:long_name = \"pressure\" ;\n", + "\tint64 level(level) ;\n", + "\t\tlevel:units = \"hPa\" ;\n", + "\t\tlevel:positive = \"down\" ;\n", + "\t\tlevel:stored_direction = \"decreasing\" ;\n", + "\t\tlevel:standard_name = \"air_pressure\" ;\n", + "\t\tlevel:long_name = \"pressure\" ;\n", "\tdouble latitude(latitude) ;\n", "\t\tlatitude:_FillValue = NaN ;\n", "\t\tlatitude:units = \"degrees_north\" ;\n", @@ -139,10 +147,39 @@ "ncdump(\"_tuv_pl.nc\")" ] }, + { + "cell_type": "markdown", + "id": "c56a9200-8695-419c-bfc6-30008faa479c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "#### Using add_earthkit_attrs=False" + ] + }, + { + "cell_type": "raw", + "id": "8b71b1c4-7c43-46cf-b6a0-1b38af1a3f74", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. In this case we can call :py:meth:`xarray.Dataset.to_netcdf` directly on the Xarray dataset without using the ``earthkit`` accessor." + ] + }, { "cell_type": "code", - "execution_count": null, - "id": "8bd59c3f-5a6b-4930-8d49-9d948f640da7", + "execution_count": 4, + "id": "156e839c-2b03-41d7-a1d0-3c049bc81888", "metadata": { "editable": true, "slideshow": { @@ -150,15 +187,83 @@ }, "tags": [] }, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "netcdf _tuv_pl_1 {\n", + "dimensions:\n", + "\tlevel = 6 ;\n", + "\tlatitude = 7 ;\n", + "\tlongitude = 12 ;\n", + "variables:\n", + "\tdouble t(level, latitude, longitude) ;\n", + "\t\tt:_FillValue = NaN ;\n", + "\t\tt:param = \"t\" ;\n", + "\t\tt:standard_name = \"air_temperature\" ;\n", + "\t\tt:long_name = \"Temperature\" ;\n", + "\t\tt:paramId = 130LL ;\n", + "\t\tt:units = \"K\" ;\n", + "\tdouble u(level, latitude, longitude) ;\n", + "\t\tu:_FillValue = NaN ;\n", + "\t\tu:param = \"u\" ;\n", + "\t\tu:standard_name = \"eastward_wind\" ;\n", + "\t\tu:long_name = \"U component of wind\" ;\n", + "\t\tu:paramId = 131LL ;\n", + "\t\tu:units = \"m s**-1\" ;\n", + "\tdouble v(level, latitude, longitude) ;\n", + "\t\tv:_FillValue = NaN ;\n", + "\t\tv:param = \"v\" ;\n", + "\t\tv:standard_name = \"northward_wind\" ;\n", + "\t\tv:long_name = \"V component of wind\" ;\n", + "\t\tv:paramId = 132LL ;\n", + "\t\tv:units = \"m s**-1\" ;\n", + "\tint64 level(level) ;\n", + "\t\tlevel:units = \"hPa\" ;\n", + "\t\tlevel:positive = \"down\" ;\n", + "\t\tlevel:stored_direction = \"decreasing\" ;\n", + "\t\tlevel:standard_name = \"air_pressure\" ;\n", + "\t\tlevel:long_name = \"pressure\" ;\n", + "\tdouble latitude(latitude) ;\n", + "\t\tlatitude:_FillValue = NaN ;\n", + "\t\tlatitude:units = \"degrees_north\" ;\n", + "\t\tlatitude:standard_name = \"latitude\" ;\n", + "\t\tlatitude:long_name = \"latitude\" ;\n", + "\tdouble longitude(longitude) ;\n", + "\t\tlongitude:_FillValue = NaN ;\n", + "\t\tlongitude:units = \"degrees_east\" ;\n", + "\t\tlongitude:standard_name = \"longitude\" ;\n", + "\t\tlongitude:long_name = \"longitude\" ;\n", + "\n", + "// global attributes:\n", + "\t\t:class = \"od\" ;\n", + "\t\t:stream = \"oper\" ;\n", + "\t\t:levtype = \"pl\" ;\n", + "\t\t:type = \"an\" ;\n", + "\t\t:expver = \"0001\" ;\n", + "\t\t:date = 20180801LL ;\n", + "\t\t:time = 1200LL ;\n", + "\t\t:domain = \"g\" ;\n", + "\t\t:number = 0LL ;\n", + "\t\t:Conventions = \"CF-1.8\" ;\n", + "\t\t:institution = \"ECMWF\" ;\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "ds.to_xarray(add_earthkit_attrs=False).to_netcdf(\"_tuv_pl_1.nc\")\n", + "ncdump(\"_tuv_pl_1.nc\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "dev_ecc", + "display_name": "dev", "language": "python", - "name": "dev_ecc" + "name": "dev" }, "language_info": { "codemirror_mode": { @@ -170,7 +275,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.11.12" } }, "nbformat": 4, diff --git a/docs/examples/xarray_engine_level.ipynb b/docs/examples/xarray_engine_level.ipynb index eb469d346..327b28abd 100644 --- a/docs/examples/xarray_engine_level.ipynb +++ b/docs/examples/xarray_engine_level.ipynb @@ -58,7 +58,7 @@ "id": "a477dd3d-e98f-43f2-99f6-628c2bd8cd6f", "metadata": { "editable": true, - "raw_mimetype": "", + "raw_mimetype": "text/restructuredtext", "slideshow": { "slide_type": "" }, diff --git a/docs/guide/xarray/overview.rst b/docs/guide/xarray/overview.rst index 57af5d94e..1be2d050c 100644 --- a/docs/guide/xarray/overview.rst +++ b/docs/guide/xarray/overview.rst @@ -107,15 +107,32 @@ For further details see the following notebook: - :ref:`/examples/xarray_engine_to_grib.ipynb` +.. _xr_grib_to_netcdf: + + Converting GRIB to NetCDF ---------------------------- -To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` then generate NetCDF from it with :py:meth:`xarray.Dataset.to_netcdf`. Earthkit-data attaches some special attributes to the generated Xarray dataset that we do not want to write to NetCDF. In order to achieve this we need to call :py:meth:`xarray.Dataset.to_netcdf` on the ``earthkit`` accessor and not directly on the Xarray dataset. +To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` then generate NetCDF from it with :py:meth:`xarray.Dataset.to_netcdf`. Earthkit-data attaches some special attributes to the generated Xarray dataset that cannot be written to NetCDF. In order to make ``to_netcdf()`` work we need to invoke it on the ``earthkit`` accessor and not directly on the Xarray dataset. .. code-block:: python + import earthkit.data as ekd + + ds_fl = ekd.from_source("sample", "pl.grib") + ds_xr = ds_fl.to_xarray() ds_xr.earthkit.to_netcdf("_from_grib.nc") +Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. In this case we can call :py:meth:`xarray.Dataset.to_netcdf` directly on the Xarray dataset without using the ``earthkit`` accessor. + +.. code-block:: python + + import earthkit.data as ekd + + ds_fl = ekd.from_source("sample", "pl.grib") + ds_xr = ds_fl.to_xarray(add_earthkit_attrs=False) + ds_xr.to_netcdf("_from_grib.nc") + For further details see the following notebook: - :ref:`/examples/grib_to_netcdf.ipynb` diff --git a/docs/release_notes/version_0.15_updates.rst b/docs/release_notes/version_0.15_updates.rst index bb09ffd41..957a3678c 100644 --- a/docs/release_notes/version_0.15_updates.rst +++ b/docs/release_notes/version_0.15_updates.rst @@ -16,10 +16,45 @@ Xarray engine Breaking changes ------------------- -- Separated the dimension names from the metadata keys used to generate the dimensions. Dimensions associated with the dimension roles are now taking the name of the :ref:`dimension role <_xr_dim_roles>`, irrespective of the metadata key the dimension role is mapped to. E.g.: the "level_type" dimension role now generates a dimension called "level_type". Previously, the dimension name was the name of the associated metadata key: e.g. it was "levtype" in the :ref:`default ` profile. The old behaviour can still be invoked by using the newly added ``dim_name_from_role_name=False`` option. See: :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. +- Separated the dimension names from the metadata keys used to generate the dimensions. Dimensions associated with the dimension roles are now taking the name of the :ref:`dimension role <_xr_dim_roles>`, irrespective of the metadata key the dimension role is mapped to. The example belows shows what this means for e.g. the "level" dimension role when the :ref:`mars profile ` (the default) is used. In this case the "level" role is mapped to the "levelist" ecCodes GRIB key. + .. code-block:: python -- The ``step`` dimension role is now mapped to the ``step_timedelta`` metadata key, which is the ``datetime.timedelta`` representation of the ``"endStep"`` GRIB/metadata key. Previously, this role was mapped to the ``"step"`` key. Please note that due to this change when ``dim_name_from_role_name=False`` is used the step dimension will be called "step_timedelta" instead of "step". + import earthkit.data as ekd + + ds_fl = ekd.from_source("sample", "pl.grib") + ds_fl.to_xarray().coords + + + With the new code the dimension/coordinate name will be "level". So the output is as follows:: + + Coordinates: + * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202... + * step (step) timedelta64[ns] 16B 00:00:00 06:00:00 + * level (level) int64 16B 500 700 + * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0 + * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0 + + + However, using the previous version the output would be as follows:: + + Coordinates: + * forecast_reference_time (forecast_reference_time) datetime64[ns] 32B 202... + * step (step) timedelta64[ns] 16B 00:00:00 06:00:00 + * levelist (levelist) int64 16B 500 700 + * latitude (latitude) float64 152B 90.0 80.0 ... -80.0 -90.0 + * longitude (longitude) float64 288B 0.0 10.0 ... 340.0 350.0 + + + The old behaviour can still be invoked by using the newly added ``dim_name_from_role_name=False`` option. See: :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. + + +- The ``step`` dimension role is now mapped to the ``step_timedelta`` metadata key, which is the ``datetime.timedelta`` representation of the ``"endStep"`` GRIB/metadata key. Previously, this role was mapped to the ``"step"`` key. This change was necessary for the following reasons: + + - allows handling cases when the ``step`` key contains ranges as a str in the form of e.g. "12-24" + - allows proper sorting of the "step" dimension values when building the dataset + + Please note that due to this change when ``dim_name_from_role_name=False`` is used the step dimension will be called "step_timedelta" instead of "step". You can still reproduce the old behaviour by using: ``dim_roles={"step": "step"}``. See the following notebook example: :ref:`/examples/xarray_engine_step_ranges.ipynb`. Other changes From 67cc92d20ecbf993e286d89a860d0ba6aeb08c12 Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Mon, 23 Jun 2025 22:43:24 +0100 Subject: [PATCH 16/17] Enable using xarray engine options in to_target (#729) --- docs/examples/grib_to_netcdf.ipynb | 204 ++++++++++++++++---- docs/guide/xarray/overview.rst | 37 +++- docs/release_notes/version_0.15_updates.rst | 8 + src/earthkit/data/encoders/netcdf.py | 12 +- tests/targets/test_target_file.py | 25 ++- 5 files changed, 247 insertions(+), 39 deletions(-) diff --git a/docs/examples/grib_to_netcdf.ipynb b/docs/examples/grib_to_netcdf.ipynb index 681bf814f..41c339948 100644 --- a/docs/examples/grib_to_netcdf.ipynb +++ b/docs/examples/grib_to_netcdf.ipynb @@ -23,7 +23,8 @@ "source": [ "import earthkit.data as ekd\n", "ekd.download_example_file(\"tuv_pl.grib\")\n", - "ds = ekd.from_source(\"file\", \"tuv_pl.grib\")" + "# we only select the temperature fields\n", + "ds = ekd.from_source(\"file\", \"tuv_pl.grib\").sel(param=\"t\")" ] }, { @@ -89,25 +90,9 @@ "variables:\n", "\tdouble t(level, latitude, longitude) ;\n", "\t\tt:_FillValue = NaN ;\n", - "\t\tt:param = \"t\" ;\n", "\t\tt:standard_name = \"air_temperature\" ;\n", "\t\tt:long_name = \"Temperature\" ;\n", - "\t\tt:paramId = 130LL ;\n", "\t\tt:units = \"K\" ;\n", - "\tdouble u(level, latitude, longitude) ;\n", - "\t\tu:_FillValue = NaN ;\n", - "\t\tu:param = \"u\" ;\n", - "\t\tu:standard_name = \"eastward_wind\" ;\n", - "\t\tu:long_name = \"U component of wind\" ;\n", - "\t\tu:paramId = 131LL ;\n", - "\t\tu:units = \"m s**-1\" ;\n", - "\tdouble v(level, latitude, longitude) ;\n", - "\t\tv:_FillValue = NaN ;\n", - "\t\tv:param = \"v\" ;\n", - "\t\tv:standard_name = \"northward_wind\" ;\n", - "\t\tv:long_name = \"V component of wind\" ;\n", - "\t\tv:paramId = 132LL ;\n", - "\t\tv:units = \"m s**-1\" ;\n", "\tint64 level(level) ;\n", "\t\tlevel:units = \"hPa\" ;\n", "\t\tlevel:positive = \"down\" ;\n", @@ -126,6 +111,8 @@ "\t\tlongitude:long_name = \"longitude\" ;\n", "\n", "// global attributes:\n", + "\t\t:param = \"t\" ;\n", + "\t\t:paramId = 130LL ;\n", "\t\t:class = \"od\" ;\n", "\t\t:stream = \"oper\" ;\n", "\t\t:levtype = \"pl\" ;\n", @@ -173,7 +160,7 @@ "tags": [] }, "source": [ - "Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. In this case we can call :py:meth:`xarray.Dataset.to_netcdf` directly on the Xarray dataset without using the ``earthkit`` accessor." + "Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. With this the earthkit attributes are not added to the generated dataset and it is safe to call :py:meth:`to_netcdf ` directly on it." ] }, { @@ -200,25 +187,9 @@ "variables:\n", "\tdouble t(level, latitude, longitude) ;\n", "\t\tt:_FillValue = NaN ;\n", - "\t\tt:param = \"t\" ;\n", "\t\tt:standard_name = \"air_temperature\" ;\n", "\t\tt:long_name = \"Temperature\" ;\n", - "\t\tt:paramId = 130LL ;\n", "\t\tt:units = \"K\" ;\n", - "\tdouble u(level, latitude, longitude) ;\n", - "\t\tu:_FillValue = NaN ;\n", - "\t\tu:param = \"u\" ;\n", - "\t\tu:standard_name = \"eastward_wind\" ;\n", - "\t\tu:long_name = \"U component of wind\" ;\n", - "\t\tu:paramId = 131LL ;\n", - "\t\tu:units = \"m s**-1\" ;\n", - "\tdouble v(level, latitude, longitude) ;\n", - "\t\tv:_FillValue = NaN ;\n", - "\t\tv:param = \"v\" ;\n", - "\t\tv:standard_name = \"northward_wind\" ;\n", - "\t\tv:long_name = \"V component of wind\" ;\n", - "\t\tv:paramId = 132LL ;\n", - "\t\tv:units = \"m s**-1\" ;\n", "\tint64 level(level) ;\n", "\t\tlevel:units = \"hPa\" ;\n", "\t\tlevel:positive = \"down\" ;\n", @@ -237,6 +208,8 @@ "\t\tlongitude:long_name = \"longitude\" ;\n", "\n", "// global attributes:\n", + "\t\t:param = \"t\" ;\n", + "\t\t:paramId = 130LL ;\n", "\t\t:class = \"od\" ;\n", "\t\t:stream = \"oper\" ;\n", "\t\t:levtype = \"pl\" ;\n", @@ -257,6 +230,169 @@ "ds.to_xarray(add_earthkit_attrs=False).to_netcdf(\"_tuv_pl_1.nc\")\n", "ncdump(\"_tuv_pl_1.nc\")" ] + }, + { + "cell_type": "markdown", + "id": "23a28542-078b-488c-8b0b-58c7c1add2ba", + "metadata": {}, + "source": [ + "#### Using to_target" + ] + }, + { + "cell_type": "raw", + "id": "7a1e9600-5b49-4fc8-aef8-ac0b51a42f72", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can call :func:`to_target` on the fieldlist and the Xarray conversion and writing to NetCDF will happen automatically under the hood using the default options. In this case ``add_earthkit_attrs=False`` is always enforced." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1bdebe02-5f4f-4e3f-b434-ebab25959577", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "netcdf _tuv_pl_2 {\n", + "dimensions:\n", + "\tlevel = 6 ;\n", + "\tlatitude = 7 ;\n", + "\tlongitude = 12 ;\n", + "variables:\n", + "\tdouble longitude(longitude) ;\n", + "\t\tlongitude:units = \"degrees_east\" ;\n", + "\t\tlongitude:standard_name = \"longitude\" ;\n", + "\t\tlongitude:long_name = \"longitude\" ;\n", + "\t\tlongitude:_FillValue = NaN ;\n", + "\tdouble latitude(latitude) ;\n", + "\t\tlatitude:units = \"degrees_north\" ;\n", + "\t\tlatitude:standard_name = \"latitude\" ;\n", + "\t\tlatitude:long_name = \"latitude\" ;\n", + "\t\tlatitude:_FillValue = NaN ;\n", + "\tdouble t(level, latitude, longitude) ;\n", + "\t\tt:standard_name = \"air_temperature\" ;\n", + "\t\tt:long_name = \"Temperature\" ;\n", + "\t\tt:units = \"K\" ;\n", + "\t\tt:_FillValue = NaN ;\n", + "\tint level(level) ;\n", + "\t\tlevel:units = \"hPa\" ;\n", + "\t\tlevel:positive = \"down\" ;\n", + "\t\tlevel:stored_direction = \"decreasing\" ;\n", + "\t\tlevel:standard_name = \"air_pressure\" ;\n", + "\t\tlevel:long_name = \"pressure\" ;\n", + "\n", + "// global attributes:\n", + "\t\t:param = \"t\" ;\n", + "\t\t:paramId = 130 ;\n", + "\t\t:class = \"od\" ;\n", + "\t\t:stream = \"oper\" ;\n", + "\t\t:levtype = \"pl\" ;\n", + "\t\t:type = \"an\" ;\n", + "\t\t:expver = \"0001\" ;\n", + "\t\t:date = 20180801 ;\n", + "\t\t:time = 1200 ;\n", + "\t\t:domain = \"g\" ;\n", + "\t\t:number = 0 ;\n", + "\t\t:Conventions = \"CF-1.8\" ;\n", + "\t\t:institution = \"ECMWF\" ;\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "ds.to_target(\"file\", \"_tuv_pl_2.nc\")\n", + "ncdump(\"_tuv_pl_2.nc\")" + ] + }, + { + "cell_type": "markdown", + "id": "0ebd29d0-cce4-4d3e-bbd7-2fcc1729c563", + "metadata": {}, + "source": [ + "To control the Xarray conversion we can pass options to the earthkit Xarray engine via ``earthkit_to_xarray_kwargs``." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "669cfc69-f64b-490b-83cc-2415ec42ab7a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "netcdf _tuv_pl_3 {\n", + "dimensions:\n", + "\tlevel = 6 ;\n", + "\tvalues = 84 ;\n", + "variables:\n", + "\tdouble latitude(values) ;\n", + "\t\tlatitude:units = \"degrees_north\" ;\n", + "\t\tlatitude:standard_name = \"latitude\" ;\n", + "\t\tlatitude:long_name = \"latitude\" ;\n", + "\t\tlatitude:_FillValue = NaN ;\n", + "\tdouble longitude(values) ;\n", + "\t\tlongitude:units = \"degrees_east\" ;\n", + "\t\tlongitude:standard_name = \"longitude\" ;\n", + "\t\tlongitude:long_name = \"longitude\" ;\n", + "\t\tlongitude:_FillValue = NaN ;\n", + "\tdouble t(level, values) ;\n", + "\t\tt:standard_name = \"air_temperature\" ;\n", + "\t\tt:long_name = \"Temperature\" ;\n", + "\t\tt:units = \"K\" ;\n", + "\t\tt:coordinates = \"latitude longitude\" ;\n", + "\t\tt:_FillValue = NaN ;\n", + "\tint level(level) ;\n", + "\t\tlevel:units = \"hPa\" ;\n", + "\t\tlevel:positive = \"down\" ;\n", + "\t\tlevel:stored_direction = \"decreasing\" ;\n", + "\t\tlevel:standard_name = \"air_pressure\" ;\n", + "\t\tlevel:long_name = \"pressure\" ;\n", + "\n", + "// global attributes:\n", + "\t\t:param = \"t\" ;\n", + "\t\t:paramId = 130 ;\n", + "\t\t:class = \"od\" ;\n", + "\t\t:stream = \"oper\" ;\n", + "\t\t:levtype = \"pl\" ;\n", + "\t\t:type = \"an\" ;\n", + "\t\t:expver = \"0001\" ;\n", + "\t\t:date = 20180801 ;\n", + "\t\t:time = 1200 ;\n", + "\t\t:domain = \"g\" ;\n", + "\t\t:number = 0 ;\n", + "\t\t:Conventions = \"CF-1.8\" ;\n", + "\t\t:institution = \"ECMWF\" ;\n", + "}\n", + "\n" + ] + } + ], + "source": [ + "ds.to_target(\"file\", \"_tuv_pl_3.nc\", earthkit_to_xarray_kwargs={\"flatten_values\": True})\n", + "ncdump(\"_tuv_pl_3.nc\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80f49214-43c6-43be-af56-9d55a16f0f8c", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/docs/guide/xarray/overview.rst b/docs/guide/xarray/overview.rst index 1be2d050c..28b9ab79d 100644 --- a/docs/guide/xarray/overview.rst +++ b/docs/guide/xarray/overview.rst @@ -113,7 +113,12 @@ For further details see the following notebook: Converting GRIB to NetCDF ---------------------------- -To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` then generate NetCDF from it with :py:meth:`xarray.Dataset.to_netcdf`. Earthkit-data attaches some special attributes to the generated Xarray dataset that cannot be written to NetCDF. In order to make ``to_netcdf()`` work we need to invoke it on the ``earthkit`` accessor and not directly on the Xarray dataset. +To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` then generate NetCDF from it with :py:meth:`xarray.Dataset.to_netcdf`. We have 3 options to do this: + +Using the earthkit accessor +++++++++++++++++++++++++++++ + +By default, the earthkit Xarray engine attaches some special attributes to the generated Xarray dataset that cannot be written to NetCDF. In order to make ``to_netcdf()`` work we need to invoke it on the ``earthkit`` accessor and not directly on the Xarray dataset. .. code-block:: python @@ -123,7 +128,10 @@ To convert GRIB data to NetCDF first we need to convert GRIB to Xarray with :py: ds_xr = ds_fl.to_xarray() ds_xr.earthkit.to_netcdf("_from_grib.nc") -Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. In this case we can call :py:meth:`xarray.Dataset.to_netcdf` directly on the Xarray dataset without using the ``earthkit`` accessor. +Using the ``add_earthkit_attrs=False`` option +++++++++++++++++++++++++++++++++++++++++++++++++++ + +Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray`. With this the earthkit attributes are not added to the generated dataset and it is safe to call :py:meth:`to_netcdf ` directly on it. .. code-block:: python @@ -133,6 +141,31 @@ Alternatively, we can use the ``add_earthkit_attrs=False`` option in :py:meth:`~ ds_xr = ds_fl.to_xarray(add_earthkit_attrs=False) ds_xr.to_netcdf("_from_grib.nc") +Using to_target +++++++++++++++++ + +The third option is to use the :func:`to_target` method to convert GRIB directly to NetCDF. This method will generate an Xarray dataset and write it to a NetCDF file in one step. + +.. code-block:: python + + import earthkit.data as ekd + + ds_fl = ekd.from_source("sample", "pl.grib") + ds.fl.to_target("file", "_from_grib.nc") + + +To control the Xarray conversion we can pass options to the earthkit Xarray engine with ``earthkit_to_xarray_kwargs``. In this case ``add_earthkit_attrs=False`` is always enforced. + +.. code-block:: python + + import earthkit.data as ekd + + ds_fl = ekd.from_source("sample", "pl.grib") + ds.fl.to_target( + "file", "_from_grib.nc", earthkit_to_xarray_kwargs={"flatten_values": True} + ) + + For further details see the following notebook: - :ref:`/examples/grib_to_netcdf.ipynb` diff --git a/docs/release_notes/version_0.15_updates.rst b/docs/release_notes/version_0.15_updates.rst index 957a3678c..2d650c393 100644 --- a/docs/release_notes/version_0.15_updates.rst +++ b/docs/release_notes/version_0.15_updates.rst @@ -94,6 +94,14 @@ New features - Added the :ref:`targets-zarr` target (:pr:`716`). See the :ref:`/examples/grib_to_zarr_target.ipynb` notebook example. - Added new config option ``grib-file-serialisation-policy`` to control how GRIB data on disk is pickled. The options are "path" and "memory". The default is "path". Previously, only "memory" was implemented (:pr:`700`). - Added serialisation to GRIB fields (both on disk and in-memory) (:pr:`700`) +- Enabled specifying earthkit Xarray engine options via the ``earthkit_to_xarray_kwargs`` kwarg in :func:`to_target` when converting GRIB to NetCDF. See the :ref:`/examples/grib_to_netcdf.ipynb` notebook example. E.g. + + .. code-block:: python + + ds.to_target( + "netcdf", "pl.nc", earthkit_to_xarray_kwargs={"flatten_values": True} + ) + Fixes diff --git a/src/earthkit/data/encoders/netcdf.py b/src/earthkit/data/encoders/netcdf.py index 5c064a98a..d503373b9 100644 --- a/src/earthkit/data/encoders/netcdf.py +++ b/src/earthkit/data/encoders/netcdf.py @@ -62,7 +62,17 @@ def _encode( missing_value=9999, **kwargs, ): - return NetCDFEncodedData(data.to_xarray()) + _kwargs = kwargs.copy() + if data is not None: + # TODO: find better way to check if the earthkit engine is used + if hasattr(data, "to_xarray_earthkit"): + earthkit_to_xarray_kwargs = _kwargs.pop("earthkit_to_xarray_kwargs", {}) + earthkit_to_xarray_kwargs["add_earthkit_attrs"] = False + _kwargs = earthkit_to_xarray_kwargs + else: + _kwargs = {} + + return NetCDFEncodedData(data.to_xarray(**_kwargs)) def _encode_field(self, field, **kwargs): return self._encode(field, **kwargs) diff --git a/tests/targets/test_target_file.py b/tests/targets/test_target_file.py index 55445d19d..b3fc8a554 100644 --- a/tests/targets/test_target_file.py +++ b/tests/targets/test_target_file.py @@ -230,7 +230,7 @@ def test_target_file_odb(): assert len(df) == 717 -def test_target_file_grib_to_netcdf(): +def test_target_file_grib_to_netcdf_1(): ds = from_source("file", earthkit_examples_file("test.grib")) # vals_ref = ds.values[:, :4] @@ -240,7 +240,28 @@ def test_target_file_grib_to_netcdf(): ds1 = from_source("file", path) assert len(ds1) == len(ds) assert ds1.metadata("param") == ["2t", "msl"] - # assert np.allclose(ds1.values[:, :4], vals_ref) + + ds2 = ds1.to_xarray() + assert "values" not in ds2.sizes + + +def test_target_file_grib_to_netcdf_2(): + ds = from_source("file", earthkit_examples_file("test.grib")) + # vals_ref = ds.values[:, :4] + + with temp_file() as path: + ds.to_target("file", path, encoder="netcdf", earthkit_to_xarray_kwargs={"flatten_values": True}) + + ds1 = from_source("file", path) + ds2 = ds1.to_xarray() + + for name in ["2t", "msl"]: + assert name in ds2.data_vars + + for name in ["latitude", "longitude"]: + assert name in ds2.coords + + assert "values" in ds2.sizes @pytest.mark.skipif(NO_RIOXARRAY, reason="rioxarray not available") From abc0ed86cb6650c2768fdf826e8c7ef83bf8bf8e Mon Sep 17 00:00:00 2001 From: Sandor Kertesz Date: Tue, 24 Jun 2025 14:39:32 +0100 Subject: [PATCH 17/17] Allow using to_target in xarry to GRIB conversion (#730) * Allow using to_target in xarry to GRIB conversion --- docs/examples/xarray_engine_to_grib.ipynb | 355 ++++++++++-------- docs/guide/xarray/dim.rst | 11 +- docs/guide/xarray/overview.rst | 43 ++- docs/release_notes/deprecations.rst | 21 ++ .../include/deprec_xarray_earthkit_to_grib.py | 7 + .../migrated_xarray_earthkit_to_grib.py | 11 + docs/release_notes/version_0.15_updates.rst | 14 +- src/earthkit/data/sources/stream.py | 3 + src/earthkit/data/targets/__init__.py | 5 + src/earthkit/data/utils/xarray/engine.py | 30 ++ tests/targets/test_target_file.py | 28 +- tests/xr_engine/test_xr_write.py | 99 +++++ 12 files changed, 444 insertions(+), 183 deletions(-) create mode 100644 docs/release_notes/include/deprec_xarray_earthkit_to_grib.py create mode 100644 docs/release_notes/include/migrated_xarray_earthkit_to_grib.py diff --git a/docs/examples/xarray_engine_to_grib.ipynb b/docs/examples/xarray_engine_to_grib.ipynb index 8f0efe4fd..cdb07cb7a 100644 --- a/docs/examples/xarray_engine_to_grib.ipynb +++ b/docs/examples/xarray_engine_to_grib.ipynb @@ -58,7 +58,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cf6d8ab8661b476ca0f076c67c8acfea", + "model_id": "7ddf1047ae7f43cb94d42c6c8bfa4d81", "version_major": 2, "version_minor": 0 }, @@ -465,20 +465,20 @@ " domain: g\n", " number: 0\n", " Conventions: CF-1.8\n", - " institution: ECMWF
  • class :
    od
    stream :
    oper
    levtype :
    pl
    type :
    fc
    expver :
    0001
    date :
    20240603
    time :
    0
    domain :
    g
    number :
    0
    Conventions :
    CF-1.8
    institution :
    ECMWF
  • " ], "text/plain": [ " Size: 176kB\n", @@ -532,7 +532,7 @@ "tags": [] }, "source": [ - "Xarray datasets created with the earthkit engine can be converted back to GRIB format by using :py:meth:`~data.utils.xarray.engine.XarrayEarthkit.to_fieldlist` on the ``earthkit`` accessor of the Xarray data array or dataset object. If the original Xarray was modified we must ensure the variable attributes are copied to the new Xarray dataset. By default, variable attributes are not kept in Xarray computations so we need to set the global Xarray ``keep_attrs`` option to enable it." + "By default, ``add_earthkit_attrs=True`` in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` and some special earthkit attributes are added to the dataset. This is needed for the Xarray to GRIB conversion. For this reason, if the Xarray is modified we must ensure the variable attributes are copied to the new Xarray dataset. By default, variable attributes are not kept in Xarray computations so we need to set the global Xarray ``keep_attrs`` option to enable it." ] }, { @@ -548,6 +548,7 @@ }, "outputs": [], "source": [ + "# ensure earthkit attributes are set\n", "import xarray as xr \n", "xr.set_options(keep_attrs=True)\n", "\n", @@ -557,15 +558,30 @@ }, { "cell_type": "markdown", - "id": "be7471ec-bb6c-4873-bd32-babb4b4fc2ba", + "id": "40c77039-21e0-48f4-a809-9530672da724", "metadata": {}, "source": [ - "#### Using to_fieldlist()" + "#### Using to_target()" + ] + }, + { + "cell_type": "raw", + "id": "d6c82521-859e-4bc9-ad3e-686071af88e8", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "It is possible to directly write the Xarray dataset created with the earthkit engine into a GRIB file with :func:`to_target`. This is a memory efficient way to write GRIB to disk since only one field is loaded into memory at a time. We can call :func:`to_target` either on the ``earthkit`` accessor or as a top level function." ] }, { "cell_type": "markdown", - "id": "86df6cdf-6446-418f-a4b2-c940eaa24bc6", + "id": "fcf81205-624e-4d4c-ad86-0595ff3e588f", "metadata": { "editable": true, "slideshow": { @@ -574,13 +590,62 @@ "tags": [] }, "source": [ - "First, we convert a single variable back into a GRIB fieldlist." + "First, we write a datarray into a GRIB file." ] }, { "cell_type": "code", "execution_count": 3, - "id": "3784d2d3-8254-48bf-8ee2-9aa2b5d69686", + "id": "68d4ec97-2429-44f8-8f80-5ffd58639222", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "GribField(t,500,20240603,0,0,0)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# option1: writing to GRIB file using the accessor\n", + "ds_xr[\"t\"].earthkit.to_target(\"file\", \"_from_xr_1.grib\")\n", + "\n", + "# option2: writing to GRIB file using the top level function\n", + "ekd.to_target(\"file\", \"_from_xr_1a.grib\", data=ds_xr[\"t\"])\n", + "\n", + "# check the results\n", + "ds_tmp1 = ekd.from_source(\"file\", \"_from_xr_1.grib\")\n", + "ds_tmp1[0]" + ] + }, + { + "cell_type": "markdown", + "id": "d31c0a87-ce0f-4365-a9d4-f9fb88e67eaa", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "Next, we write the whole dataset into a GRIB file." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ced38f9d-20c6-43d7-9e91-32b7bff93a6a", "metadata": { "editable": true, "slideshow": { @@ -626,7 +691,7 @@ " \n", " 0\n", " ecmf\n", - " t\n", + " r\n", " isobaricInhPa\n", " 500\n", " 20240603\n", @@ -639,7 +704,7 @@ " \n", " 1\n", " ecmf\n", - " t\n", + " r\n", " isobaricInhPa\n", " 700\n", " 20240603\n", @@ -652,7 +717,7 @@ " \n", " 2\n", " ecmf\n", - " t\n", + " r\n", " isobaricInhPa\n", " 500\n", " 20240603\n", @@ -665,7 +730,7 @@ " \n", " 3\n", " ecmf\n", - " t\n", + " r\n", " isobaricInhPa\n", " 700\n", " 20240603\n", @@ -678,7 +743,7 @@ " \n", " 4\n", " ecmf\n", - " t\n", + " r\n", " isobaricInhPa\n", " 500\n", " 20240603\n", @@ -694,11 +759,11 @@ ], "text/plain": [ " centre shortName typeOfLevel level dataDate dataTime stepRange \\\n", - "0 ecmf t isobaricInhPa 500 20240603 0 0 \n", - "1 ecmf t isobaricInhPa 700 20240603 0 0 \n", - "2 ecmf t isobaricInhPa 500 20240603 0 6 \n", - "3 ecmf t isobaricInhPa 700 20240603 0 6 \n", - "4 ecmf t isobaricInhPa 500 20240603 1200 0 \n", + "0 ecmf r isobaricInhPa 500 20240603 0 0 \n", + "1 ecmf r isobaricInhPa 700 20240603 0 0 \n", + "2 ecmf r isobaricInhPa 500 20240603 0 6 \n", + "3 ecmf r isobaricInhPa 700 20240603 0 6 \n", + "4 ecmf r isobaricInhPa 500 20240603 1200 0 \n", "\n", " dataType number gridType \n", "0 fc 0 regular_ll \n", @@ -708,19 +773,26 @@ "4 fc 0 regular_ll " ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "ds_fl1 = ds_xr[\"t\"].earthkit.to_fieldlist()\n", - "ds_fl1.head()" + "# option1: writing to GRIB file using the accessor\n", + "ds_xr.earthkit.to_target(\"file\", \"_from_xr_2.grib\")\n", + "\n", + "# option2: writing to GRIB file using the top level function\n", + "ekd.to_target(\"file\", \"_from_xr_2a.grib\", data=ds_xr)\n", + "\n", + "# check the results\n", + "ds_tmp2 = ekd.from_source(\"file\", \"_from_xr_2.grib\")\n", + "ds_tmp2.head()" ] }, { "cell_type": "markdown", - "id": "3a763835-768e-44c7-a547-3412b35defce", + "id": "c75cbad6-a383-4bf2-add9-98a6c313aada", "metadata": { "editable": true, "slideshow": { @@ -734,8 +806,8 @@ }, { "cell_type": "code", - "execution_count": 4, - "id": "968bbc95-8bd6-401e-ba5a-ba2618754a1f", + "execution_count": 5, + "id": "0c0bafa4-e3ca-4361-b28c-851b8447f323", "metadata": { "editable": true, "slideshow": { @@ -757,12 +829,12 @@ "# original GRIB data\n", "print(ds_fl.sel(param=\"t\", step=0, level=500)[0].values[0])\n", "# GRIB data converted from the modified xarray object\n", - "print(ds_fl1.sel(param=\"t\", step=0, level=500)[0].values[0])" + "print(ds_tmp1.sel(param=\"t\", step=0, level=500)[0].values[0])" ] }, { "cell_type": "markdown", - "id": "b81cc6de-0b91-442b-9df3-4f3c332aa09c", + "id": "be7471ec-bb6c-4873-bd32-babb4b4fc2ba", "metadata": { "editable": true, "slideshow": { @@ -771,13 +843,42 @@ "tags": [] }, "source": [ - "Next, we convert back the whole dataset into a GRIB fieldlist." + "#### Using to_fieldlist()" + ] + }, + { + "cell_type": "raw", + "id": "30ad2d99-db56-4302-99cf-277602202459", + "metadata": { + "editable": true, + "raw_mimetype": "text/restructuredtext", + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "We can also convert the Xarray dataset into a GRIB fieldlist by using :py:meth:`~data.utils.xarray.engine.XarrayEarthkit.to_fieldlist` on the ``earthkit`` accessor of the Xarray object. Please note that this will generate a fieldlist entirely stored in memory." + ] + }, + { + "cell_type": "markdown", + "id": "07ee1067-6476-450b-bc12-d90b1dc2fc05", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "source": [ + "First, we convert a dataarray to a GRIB fieldlist." ] }, { "cell_type": "code", - "execution_count": 5, - "id": "b39c4d26-dd9f-4a76-9559-6fb8b6676698", + "execution_count": 6, + "id": "3784d2d3-8254-48bf-8ee2-9aa2b5d69686", "metadata": { "editable": true, "slideshow": { @@ -823,7 +924,7 @@ " \n", " 0\n", " ecmf\n", - " r\n", + " t\n", " isobaricInhPa\n", " 500\n", " 20240603\n", @@ -836,7 +937,7 @@ " \n", " 1\n", " ecmf\n", - " r\n", + " t\n", " isobaricInhPa\n", " 700\n", " 20240603\n", @@ -849,7 +950,7 @@ " \n", " 2\n", " ecmf\n", - " r\n", + " t\n", " isobaricInhPa\n", " 500\n", " 20240603\n", @@ -862,7 +963,7 @@ " \n", " 3\n", " ecmf\n", - " r\n", + " t\n", " isobaricInhPa\n", " 700\n", " 20240603\n", @@ -875,7 +976,7 @@ " \n", " 4\n", " ecmf\n", - " r\n", + " t\n", " isobaricInhPa\n", " 500\n", " 20240603\n", @@ -891,11 +992,11 @@ ], "text/plain": [ " centre shortName typeOfLevel level dataDate dataTime stepRange \\\n", - "0 ecmf r isobaricInhPa 500 20240603 0 0 \n", - "1 ecmf r isobaricInhPa 700 20240603 0 0 \n", - "2 ecmf r isobaricInhPa 500 20240603 0 6 \n", - "3 ecmf r isobaricInhPa 700 20240603 0 6 \n", - "4 ecmf r isobaricInhPa 500 20240603 1200 0 \n", + "0 ecmf t isobaricInhPa 500 20240603 0 0 \n", + "1 ecmf t isobaricInhPa 700 20240603 0 0 \n", + "2 ecmf t isobaricInhPa 500 20240603 0 6 \n", + "3 ecmf t isobaricInhPa 700 20240603 0 6 \n", + "4 ecmf t isobaricInhPa 500 20240603 1200 0 \n", "\n", " dataType number gridType \n", "0 fc 0 regular_ll \n", @@ -905,119 +1006,34 @@ "4 fc 0 regular_ll " ] }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ds_fl1 = ds_xr.earthkit.to_fieldlist()\n", - "ds_fl1.head()" - ] - }, - { - "cell_type": "raw", - "id": "d67d36c7-e0e4-41d5-b0b6-e8b8743f3d10", - "metadata": { - "editable": true, - "raw_mimetype": "text/restructuredtext", - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "source": [ - "The generated GRIB fieldlist can be saved to disk using the :func:`to_target` method." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "36423326-cf54-494b-8932-72d986b908bf", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "GribField(r,500,20240603,0,0,0)" - ] - }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "out_name = \"_from_xr_1.grib\"\n", - "ds_fl1.to_target(\"file\", out_name)\n", - "# read back and check the save GRIB\n", - "ds_tmp = ekd.from_source(\"file\", out_name)\n", - "ds_tmp[0]" + "ds_fl1 = ds_xr[\"t\"].earthkit.to_fieldlist()\n", + "ds_fl1.head()" ] }, { "cell_type": "markdown", - "id": "9cbf8f1d-50dc-4a96-aab8-e0ce4410aa4e", - "metadata": {}, - "source": [ - "#### Using to_grib()" - ] - }, - { - "cell_type": "raw", - "id": "29d80b02-9032-4b27-b0a1-aaaf8d568784", + "id": "b81cc6de-0b91-442b-9df3-4f3c332aa09c", "metadata": { "editable": true, - "raw_mimetype": "text/restructuredtext", "slideshow": { "slide_type": "" }, "tags": [] }, "source": [ - "It is also possible to directly write the Xarray into a GRIB file when calling :py:meth:`~data.utils.xarray.engine.XarrayEarthkit.to_grib` on the ``earthkit`` accessor. This method does not require the creation of a fieldlist so will be a more memory efficient way to write the data to disk." + "Next, we convert back the whole dataset into a GRIB fieldlist." ] }, { "cell_type": "code", "execution_count": 7, - "id": "fd88bf7f-c9ed-4b11-95e0-048151081b74", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "GribField(r,500,20240603,0,0,0)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "out_name = \"_from_xr_2.grib\"\n", - "ds_xr.earthkit.to_grib(out_name)\n", - "ds_tmp = ekd.from_source(\"file\", out_name)\n", - "ds_tmp[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "140e38b4-27fb-46c9-8725-a0625fbd3bbd", + "id": "b39c4d26-dd9f-4a76-9559-6fb8b6676698", "metadata": { "editable": true, "slideshow": { @@ -1026,20 +1042,6 @@ "tags": [] }, "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "120cbbf74d0c4edeac0cbdeed7bb6e2f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "pl.grib: 0%| | 0.00/48.8k [00:00` +.. _xr_to_grib: + + Converting Xarray to GRIB ------------------------- @@ -78,7 +81,34 @@ Converting Xarray to GRIB This is an experimental feature and it is not yet fully supported. -Xarray datasets created with the earthkit engine can be converted back to GRIB format by using :py:meth:`~data.utils.xarray.engine.XarrayEarthkit.to_fieldlist` on the ``earthkit`` accessor of the Xarray object. If the original Xarray was modified we must ensure the variable attributes are copied to the new Xarray dataset. By default, variable attributes are not kept in Xarray computations so we need to set the global Xarray ``keep_attrs`` option to enable it. +By default, ``add_earthkit_attrs=True`` in :py:meth:`~data.readers.grib.index.GribFieldList.to_xarray` and some special earthkit attributes are added to the dataset. This is needed for the Xarray to GRIB conversion. For this reason, if the Xarray is modified we must ensure the variable attributes are copied to the new Xarray dataset. By default, variable attributes are not kept in Xarray computations so we need to set the global Xarray ``keep_attrs`` option to enable it.See the examples below for details. + +Using to_target +++++++++++++++++ + +It is possible to directly write the Xarray dataset created with the earthkit engine into a GRIB file with :func:`to_target`. This is a memory efficient way to write GRIB to disk since only one field is loaded into memory at a time. We can call :func:`to_target` either on the ``earthkit`` accessor or as a top level function. + +.. code-block:: python + + # ensure attributes are kept + import xarray as xr + + xr.set_options(keep_attrs=True) + + # ds_xr is an Xarray dataset created with the earthkit engine, we modify it + ds_xr += 1 + + # option1: writing to GRIB file using the accessor + ds_xr.earthkit.to_target("file", "_from_xr_1.grib") + + # option: 2writing to GRIB file using the top level function + to_target("file", "_from_xr_2.grib", data=ds_xr) + + +Using to_fieldlist() +++++++++++++++++++++ + +We can also convert the Xarray dataset into a GRIB fieldlist by using :py:meth:`~data.utils.xarray.engine.XarrayEarthkit.to_fieldlist` on the ``earthkit`` accessor of the Xarray object. Please note that this will generate a fieldlist entirely stored in memory. .. code-block:: python @@ -89,18 +119,11 @@ Xarray datasets created with the earthkit engine can be converted back to GRIB f >>> ds_fl1[0] ArrayField(r,500,20240603,0,0,0) -The generated GRIB fieldlist can be saved to disk using the :py:meth:`~data.readers.grib.index.GribFieldList.save` method. - -.. code-block:: python - - ds_fl1.save("_from_xr_1.grib") - - -It is also possible to directly write the Xarray into a GRIB file when calling :py:meth:`~data.utils.xarray.engine.XarrayEarthkit.to_grib` on the ``earthkit`` accessor. This will be a more memory efficient way to write GRIB to disk than generating a fieldlist first. +The generated GRIB fieldlist can be saved to disk using the :func:`to_target` method. .. code-block:: python - ds_xr.earthkit.to_grib("_from_xr_2.grib") + ds_fl1.to_target("file", "_from_xr_3.grib") For further details see the following notebook: diff --git a/docs/release_notes/deprecations.rst b/docs/release_notes/deprecations.rst index 110bab34e..a93cef866 100644 --- a/docs/release_notes/deprecations.rst +++ b/docs/release_notes/deprecations.rst @@ -28,6 +28,27 @@ The name of the ensemble member :ref:`dimension role <_xr_dim_roles>` changed to .. literalinclude:: include/migrated_ens_dim_role.py +.. _deprecated-xarray-accessor-to-grib: + +The "to_grib" method on the earthkit Xarray accessor is deprecated ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The :func:`to_grib` method on the ``earthkit`` Xarray accessor is deprecated. Use :func:`to_target` instead. The method is still available for backward compatibility but will be removed in a future release. See :ref:`/examples/xarray_engine_to_grib.ipynb` notebook for details on how to use the new API. + +.. list-table:: + :header-rows: 0 + + * - Deprecated code + * - + + .. literalinclude:: include/deprec_xarray_earthkit_to_grib.py + + * - New code + * - + + .. literalinclude:: include/migrated_xarray_earthkit_to_grib.py + + .. _deprecated-0.13.0: diff --git a/docs/release_notes/include/deprec_xarray_earthkit_to_grib.py b/docs/release_notes/include/deprec_xarray_earthkit_to_grib.py new file mode 100644 index 000000000..9a390bd31 --- /dev/null +++ b/docs/release_notes/include/deprec_xarray_earthkit_to_grib.py @@ -0,0 +1,7 @@ +import earthkit.data as ekd + +ds = ekd.from_source("sample", "test.grib") + +ds_xr = ds.to_xarray() + +ds_xr.earthkit.to_grib("_res_xarray_to_grib_1.grib") diff --git a/docs/release_notes/include/migrated_xarray_earthkit_to_grib.py b/docs/release_notes/include/migrated_xarray_earthkit_to_grib.py new file mode 100644 index 000000000..3f43b7261 --- /dev/null +++ b/docs/release_notes/include/migrated_xarray_earthkit_to_grib.py @@ -0,0 +1,11 @@ +import earthkit.data as ekd + +ds = ekd.from_source("sample", "test.grib") + +ds_xr = ds.to_xarray() + +# option1: writing to GRIB file using the accessor +ds_xr.earthkit.to_target("file", "_res_xarray_to_grib_2.grib") + +# option2: writing to GRIB file using the top level function +ekd.to_target("file", "_res_xarray_to_grib_2a.grib", data=ds_xr) diff --git a/docs/release_notes/version_0.15_updates.rst b/docs/release_notes/version_0.15_updates.rst index 2d650c393..8841c7106 100644 --- a/docs/release_notes/version_0.15_updates.rst +++ b/docs/release_notes/version_0.15_updates.rst @@ -9,14 +9,15 @@ Deprecations +++++++++++++++++++ - :ref:`deprecated-ens-dim-role` +- :ref:`deprecated-xarray-accessor-to-grib` Xarray engine ++++++++++++++++++++++++++++++ -Breaking changes -------------------- +Breaking xarray engine changes +------------------------------- -- Separated the dimension names from the metadata keys used to generate the dimensions. Dimensions associated with the dimension roles are now taking the name of the :ref:`dimension role <_xr_dim_roles>`, irrespective of the metadata key the dimension role is mapped to. The example belows shows what this means for e.g. the "level" dimension role when the :ref:`mars profile ` (the default) is used. In this case the "level" role is mapped to the "levelist" ecCodes GRIB key. +- Separated the dimension names from the metadata keys used to generate the dimensions. Dimensions associated with the dimension roles are now taking the name of the :ref:`dimension role `, irrespective of the metadata key the dimension role is mapped to. The example belows shows what this means for e.g. the "level" dimension role when the :ref:`mars profile ` (the default) is used. In this case the "level" role is mapped to the "levelist" ecCodes GRIB key. .. code-block:: python @@ -57,8 +58,8 @@ Breaking changes Please note that due to this change when ``dim_name_from_role_name=False`` is used the step dimension will be called "step_timedelta" instead of "step". You can still reproduce the old behaviour by using: ``dim_roles={"step": "step"}``. See the following notebook example: :ref:`/examples/xarray_engine_step_ranges.ipynb`. -Other changes -------------------- +Other Xarray engine changes +------------------------------ - Allowed using mappings in the ``extra_dims`` and ``fixed_dims`` options to define both the name of the dimensions and the metadata keys to generate their values. Previously, these options only took a single/multiple metadata keys. E.g. both the options below will generate the "expver", "mars_stream" and "mars_class" dimensions using the "expver", "stream" and "class" metadata keys. @@ -75,6 +76,7 @@ Other changes - Improved the serialisation of GRIB fieldlists to reduce memory usage when Xarray is generated with chunks (:pr:`700`). See the :ref:`/examples/xarray_engine_chunks.ipynb` notebook example. - TensorBackendArray, which implements the lazy loading of DataArrays in the Xarray engine, now uses a ``dask.utils.SerializableLock`` when accessing the data (:pr:`700`). - Enabled converting :ref:`data-sources-lod` fieldlists into Xarray (:pr:`701`). See the :ref:`/examples/list_of_dicts_to_xarray.ipynb` notebook example. +- Enabled converting Xarray generated with the earthkit fieldlist into GRIB using :func:`to_target` (:pr:`730`). See :ref:`xr_to_grib` and the related :ref:`/examples/xarray_engine_to_grib.ipynb` notebook example. New Xarray engine notebooks ------------------------------ @@ -94,7 +96,7 @@ New features - Added the :ref:`targets-zarr` target (:pr:`716`). See the :ref:`/examples/grib_to_zarr_target.ipynb` notebook example. - Added new config option ``grib-file-serialisation-policy`` to control how GRIB data on disk is pickled. The options are "path" and "memory". The default is "path". Previously, only "memory" was implemented (:pr:`700`). - Added serialisation to GRIB fields (both on disk and in-memory) (:pr:`700`) -- Enabled specifying earthkit Xarray engine options via the ``earthkit_to_xarray_kwargs`` kwarg in :func:`to_target` when converting GRIB to NetCDF. See the :ref:`/examples/grib_to_netcdf.ipynb` notebook example. E.g. +- Enabled specifying earthkit Xarray engine options via the ``earthkit_to_xarray_kwargs`` kwarg in :func:`to_target` when converting GRIB to NetCDF. See :ref:`xr_grib_to_netcdf` and related the :ref:`/examples/grib_to_netcdf.ipynb` notebook example. (:pr:`729`) E.g. .. code-block:: python diff --git a/src/earthkit/data/sources/stream.py b/src/earthkit/data/sources/stream.py index 862f5b6bb..20d8bf2ea 100644 --- a/src/earthkit/data/sources/stream.py +++ b/src/earthkit/data/sources/stream.py @@ -233,6 +233,9 @@ def merge(cls, sources): assert len(sources) > 1 return MultiStreamSource.merge(sources) + def default_encoder(self): + return None + class Stream: def __init__(self, stream=None, maker=None, **kwargs): diff --git a/src/earthkit/data/targets/__init__.py b/src/earthkit/data/targets/__init__.py index ae28d06eb..794e8d88b 100644 --- a/src/earthkit/data/targets/__init__.py +++ b/src/earthkit/data/targets/__init__.py @@ -144,6 +144,11 @@ def write( self.write(d, **kwargs) elif hasattr(data, "to_target"): self._write(data, **kwargs) + # Xarray generated with earthkit + elif hasattr(data, "earthkit"): + accessor = data.earthkit + if hasattr(accessor, "_to_fields") and hasattr(accessor, "_generator"): + self._write(accessor._generator(), **kwargs) # elif "values" not in kwargs: # # TODO: this should be reviewed # self._write(None, values=data, **kwargs) diff --git a/src/earthkit/data/utils/xarray/engine.py b/src/earthkit/data/utils/xarray/engine.py index 7a8789841..2b5975c90 100644 --- a/src/earthkit/data/utils/xarray/engine.py +++ b/src/earthkit/data/utils/xarray/engine.py @@ -380,13 +380,43 @@ def to_fieldlist(self): return FieldArray([f for f in self._to_fields()]) + def to_target(self, target, *args, **kwargs): + from earthkit.data.targets import to_target + + to_target(target, *args, data=self._generator(), **kwargs) + def to_grib(self, filename): + import warnings + + warnings.warn( + "The `to_grib` is deprecated in 0.15.0 and will be removed in a future version. " + "Use `to_target` instead.", + DeprecationWarning, + ) from earthkit.data.targets import create_target with create_target("file", filename) as target: for f in self._to_fields(): target.write(f) + def _generator(self): + from earthkit.data import FieldList + + class GeneratorFieldList(FieldList): + def __init__(self, data): + self._data = data + + def mutate(self): + return self + + def __iter__(self): + return self._data + + def default_encoder(self): + return "grib" + + return GeneratorFieldList(self._to_fields()) + @xarray.register_dataarray_accessor("earthkit") class XarrayEarthkitDataArray(XarrayEarthkit): diff --git a/tests/targets/test_target_file.py b/tests/targets/test_target_file.py index b3fc8a554..f6a078960 100644 --- a/tests/targets/test_target_file.py +++ b/tests/targets/test_target_file.py @@ -34,7 +34,7 @@ ], ) @pytest.mark.parametrize("direct_call", [True, False]) -def test_target_file_grib_core(kwargs, direct_call): +def test_target_file_grib_core_non_stream(kwargs, direct_call): ds = from_source("file", earthkit_examples_file("test.grib")) vals_ref = ds.values[:, :4] @@ -50,6 +50,32 @@ def test_target_file_grib_core(kwargs, direct_call): assert np.allclose(ds1.values[:, :4], vals_ref) +@pytest.mark.parametrize( + "kwargs", + [ + # {}, TODO: make it work + {"encoder": "grib"}, + {"encoder": GribEncoder()}, + ], +) +@pytest.mark.parametrize("direct_call", [True, False]) +def test_target_file_grib_core_stream(kwargs, direct_call): + ds = from_source("file", earthkit_examples_file("test.grib"), stream=True) + ds_ref = from_source("file", earthkit_examples_file("test.grib")) + vals_ref = ds_ref.values[:, :4] + + with temp_file() as path: + if direct_call: + to_target("file", path, data=ds, **kwargs) + else: + ds.to_target("file", path, **kwargs) + + ds1 = from_source("file", path) + assert len(ds_ref) == len(ds1) + assert ds1.metadata("shortName") == ["2t", "msl"] + assert np.allclose(ds1.values[:, :4], vals_ref) + + def test_target_file_grib_append(): ds = from_source("file", earthkit_examples_file("test.grib")) vals_ref = ds.values[:, :4] diff --git a/tests/xr_engine/test_xr_write.py b/tests/xr_engine/test_xr_write.py index 0154690dc..f446b0b13 100644 --- a/tests/xr_engine/test_xr_write.py +++ b/tests/xr_engine/test_xr_write.py @@ -13,6 +13,8 @@ import pytest from earthkit.data import from_source +from earthkit.data import to_target +from earthkit.data.core.temporary import temp_file from earthkit.data.testing import earthkit_remote_test_data_file @@ -252,3 +254,100 @@ def test_xr_write_bits_per_value(): assert len(r) == 16 assert r.index("shortName") == ["t"] assert r[0].metadata("bitsPerValue") == 8 + + +@pytest.mark.cache +@pytest.mark.parametrize("method", ["to_grib", "to_target_on_obj", "to_target_func"]) +@pytest.mark.parametrize( + "kwargs", + [ + {"profile": "mars", "time_dim_mode": "raw"}, + ], +) +def test_xr_write_to_file_1(method, kwargs): + ds_ek = from_source("url", earthkit_remote_test_data_file("test-data/xr_engine/level/pl.grib")) + ds_ek = ds_ek.sel(param=["t", "r"], level=[500, 850]) + + ref_t_vals = ds_ek.sel(param="t", step=6, level=500).to_numpy() + ref_r_vals = ds_ek.sel(param="r", step=6, level=500).to_numpy() + + import xarray as xr + + xr.set_options(keep_attrs=True) + + ds = ds_ek.to_xarray(**kwargs) + ds += 1 + + # data-array + with temp_file() as path: + if method == "to_grib": + ds["t"].earthkit.to_grib(path) + elif method == "to_target_on_obj": + ds["t"].earthkit.to_target("file", path) + elif method == "to_target_func": + to_target("file", path, data=ds["t"]) + + r = from_source("file", path) + + assert len(r) == 16 + assert r.index("shortName") == ["t"] + assert np.allclose(ref_t_vals + 1.0, r.sel(param="t", step=6, level=500).to_numpy()) + + ref_base = [ + "2024-06-03T00:00:00", + "2024-06-03T00:00:00", + "2024-06-03T00:00:00", + "2024-06-03T00:00:00", + "2024-06-03T12:00:00", + "2024-06-03T12:00:00", + "2024-06-03T12:00:00", + "2024-06-03T12:00:00", + "2024-06-04T00:00:00", + "2024-06-04T00:00:00", + "2024-06-04T00:00:00", + "2024-06-04T00:00:00", + "2024-06-04T12:00:00", + "2024-06-04T12:00:00", + "2024-06-04T12:00:00", + "2024-06-04T12:00:00", + ] + + ref_valid = [ + "2024-06-03T00:00:00", + "2024-06-03T00:00:00", + "2024-06-03T06:00:00", + "2024-06-03T06:00:00", + "2024-06-03T12:00:00", + "2024-06-03T12:00:00", + "2024-06-03T18:00:00", + "2024-06-03T18:00:00", + "2024-06-04T00:00:00", + "2024-06-04T00:00:00", + "2024-06-04T06:00:00", + "2024-06-04T06:00:00", + "2024-06-04T12:00:00", + "2024-06-04T12:00:00", + "2024-06-04T18:00:00", + "2024-06-04T18:00:00", + ] + + assert r.metadata("base_datetime") == ref_base + assert r.metadata("valid_datetime") == ref_valid + + # dataset + with temp_file() as path: + if method == "to_grib": + ds.earthkit.to_grib(path) + elif method == "to_target_on_obj": + ds.earthkit.to_target("file", path) + elif method == "to_target_func": + to_target("file", path, data=ds) + + r = from_source("file", path) + assert len(r) == 16 * 2 + assert set(r.index("shortName")) == set(["t", "r"]) + assert np.allclose(ref_t_vals + 1.0, r.sel(param="t", step=6, level=500).to_numpy()) + assert np.allclose(ref_r_vals + 1.0, r.sel(param="r", step=6, level=500).to_numpy()) + + assert sorted(r.metadata("base_datetime")) == sorted(ds_ek.metadata("base_datetime")) + assert sorted(r.metadata("valid_datetime")) == sorted(ds_ek.metadata("valid_datetime"))