Source code for esmvalcore.io.xarray

"""Access data using :meth:`xarray.open_dataset`.

To use this module, copy one of the following
:ref:`configuration <config-data-sources>` files to your configuration
directory:

1. ``data-cds-era5.yml``

    Enable access to ERA5 data stored in the
    `Copernicus Climate Data Store (CDS) <https://cds.climate.copernicus.eu/>`__:

    .. code-block:: console

        esmvaltool config copy data-cds-era5.yml

    The file content is:

    .. literalinclude:: ../configurations/data-cds-era5.yml
        :language: yaml
        :caption: Contents of ``data-cds-era5.yml``

    Then edit the copied file and replace the text ``<INSERT YOUR CDS API KEY HERE>``
    with your personal `CDS API key <https://cds.climate.copernicus.eu/how-to-api>`__.

2. ``data-gcs-era5.yml``

    Enable access to `ERA5 data stored in Google Cloud Storage
    <https://github.com/google-research/arco-era5>`__:

    .. code-block:: console

        esmvaltool config copy data-gcs-era5.yml

    Install `gcsfs <https://pypi.org/project/gcsfs/>`_ alongside ESMValCore to
    use this data source.

"""

from __future__ import annotations

import copy
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

import xarray as xr

from esmvalcore.dataset import _ismatch
from esmvalcore.io.protocol import DataElement, DataSource
from esmvalcore.iris_helpers import dataset_to_iris

if TYPE_CHECKING:
    import iris.cube

    from esmvalcore.typing import Facets, FacetValue


[docs] @dataclass class XarrayDataset(DataElement): """A dataset that can load data using :meth:`xarray <xarray.open_dataset>`.""" name: str """A unique name identifying the dataset.""" facets: Facets = field(repr=False) """Facets are key-value pairs that were used to find this data.""" store: str = field(repr=False) """The data store containing the data.""" options: dict[str, Any] = field(repr=False, default_factory=dict) """Options to use when opening the data.""" _attributes: dict[str, Any] | None = field( init=False, repr=False, default=None, ) def __hash__(self) -> int: """Return a number uniquely representing the data element.""" return hash((self.name, self.facets.get("version")))
[docs] def prepare(self) -> None: """Prepare the data for access."""
@property def attributes(self) -> dict[str, Any]: """Attributes are key-value pairs describing the data.""" if self._attributes is None: msg = ( "Attributes have not been read yet. Call the `to_iris` method " "first to read the attributes from the file." ) raise ValueError(msg) return self._attributes @attributes.setter def attributes(self, value: dict[str, Any]) -> None: self._attributes = value
[docs] def to_iris(self) -> iris.cube.CubeList: """Load the data as Iris cubes. Returns ------- : The loaded data. """ options = self.options | { # Do not decode the time coordininate because Iris needs the # encoded values. "decode_times": False, } dataset = xr.open_dataset(self.store, **options) dataset.attrs["source_file"] = str(self.store) # Cache the attributes. self.attributes = copy.deepcopy(dataset.attrs) return dataset_to_iris(dataset)
[docs] @dataclass class XarrayDataSource(DataSource): """Data source that can find data using :meth:`xarray <xarray.open_dataset>`.""" name: str """A name identifying the data source.""" project: str """The project that the data source provides data for.""" priority: int """The priority of the data source. Lower values have priority.""" variables: dict[str, str] """Mapping between the dataset variable name and ESMValCore ``short_name``.""" facets: Facets """Facets that will be added to the data elements found using this data source.""" debug_info: str = field(init=False, repr=False, default="") """A string containing debug information when no data is found.""" store: str """The store used to load the data.""" options: dict[str, Any] = field(default_factory=dict) """Options to use when opening the data store."""
[docs] def find_data(self, **facets: FacetValue) -> list[XarrayDataset]: """Find data. Parameters ---------- **facets : Find data matching these facets. Returns ------- : A list of data elements that have been found. """ normalized_facets = { facet: [str(values)] if isinstance(values, str | int | float) else values for facet, values in facets.items() if facet in ["short_name", *self.facets] } if not all( any(_ismatch(self.facets[k], v) for v in values) for k, values in normalized_facets.items() if k != "short_name" ): return [] # Would it be useful to use `xr.open_datatree` instead? # https://tutorial.xarray.dev/intermediate/storage_formats.html#how-does-zarr-relate-to-xarray search_options = self.options | { # Avoid loading the coordinate data. "create_default_indexes": False, # Computing chunk sizes is expensive for datasets with many variables. "chunks": None, } search_dataset = xr.open_dataset(self.store, **search_options) available_variables = { self.variables.get(v, v): v for v in search_dataset.data_vars } short_names = normalized_facets.get("short_name", ["*"]) matching_variables = [ v for v in available_variables for s in short_names if _ismatch(v, s) ] if not matching_variables: self.debug_info = ( f"No variables matching {', '.join(sorted(short_names))} found " f"in {self.store}, available variables are: " f"{', '.join(sorted(available_variables))}." ) result = [] for short_name in matching_variables: dataset_facets = copy.deepcopy(self.facets) dataset_facets["short_name"] = short_name data_var = available_variables[short_name] dataset = XarrayDataset( name=f"{self.store}/{short_name}", store=self.store, options={ "drop_variables": [ v for v in available_variables.values() if v != data_var ], **self.options, }, facets=dataset_facets, ) result.append(dataset) return result