Repository navigation
Add PangeoCMIP6 demo notebook
#288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ahmedmohiduet
wants to merge
5
commits into
ACCESS-Community-Hub:develop
Choose a base branch
from
ahmedmohiduet:285-demo-notebook
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
4a01ef3
Add Pangeo CMIP6 ArchiveIndex accessor
ahmedmohiduet 5ae0b96
Add PangeoCMIP6.catalog() static method
ahmedmohiduet 8eff752
Add `PangeoCMIP6` demo notebook
ahmedmohiduet cdbc2ac
Use ConvertUnits transform and pipeline in Pangeo CMIP6 demo
ahmedmohiduet 982d9ac
Raise on missing times and ambiguous runs in PangeoCMIP6
ahmedmohiduet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # PyEarthTools archive: Pangeo CMIP6 | ||
|
|
||
| Accessor for the Pangeo analysis-ready, cloud-optimised (ARCO) CMIP6 data, hosted as Zarr stores on Google Cloud. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| [build-system] | ||
| requires = ["setuptools>=61", "setuptools-scm>=8.0"] | ||
|
|
||
| [project] | ||
| name = "PyEarthTools-archive-Pangeo" | ||
| description = "PyEarthTools archive for the Pangeo CMIP6 analysis-ready, cloud-optimised (ARCO) data" | ||
| requires-python = ">=3.11, <3.14" | ||
| keywords = ["pyearthtools", "cmip6", "pangeo", "zarr", "climate"] | ||
| classifiers = [ | ||
| "License :: OSI Approved :: Apache Software License", | ||
| "Operating System :: OS Independent", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Programming Language :: Python :: 3.13", | ||
| ] | ||
| dependencies = [ | ||
| "pyearthtools-data>=0.6.3", | ||
| "pandas", | ||
| "xarray", | ||
| "zarr", | ||
| "gcsfs", | ||
| ] | ||
| dynamic = ["version", "readme"] | ||
|
|
||
| [project.optional-dependencies] | ||
| dev = ["pytest>=7.0", "pytest-cov", "black==26.3.1", "ruff"] | ||
|
|
||
| [tool.setuptools.packages.find] | ||
| where = ["src"] | ||
|
|
||
| [tool.setuptools.dynamic] | ||
| readme = {file = ["README.md"], content-type = "text/markdown"} | ||
|
|
||
| [project.urls] | ||
| homepage = "https://pyearthtools.readthedocs.io/en/latest/" | ||
| documentation = "https://pyearthtools.readthedocs.io/en/latest/" | ||
| repository = "https://github.com/ACCESS-Community-Hub/PyEarthTools" | ||
|
|
||
| [tool.isort] | ||
| profile = "black" | ||
|
|
||
| [tool.black] | ||
| line-length = 120 |
21 changes: 21 additions & 0 deletions
21
packages/third_party/pangeo_site_archive/src/site_archive_pangeo/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| """ | ||
| Pangeo CMIP6 archive for PyEarthTools. | ||
|
|
||
| | Name | Description | | ||
| | :--- | ----: | | ||
| | [PangeoCMIP6][site_archive_pangeo.PangeoCMIP6] | CMIP6 analysis-ready, cloud-optimised (ARCO) data | | ||
| """ | ||
|
|
||
| from pyearthtools.data.archive import register_archive | ||
|
|
||
| import site_archive_pangeo | ||
| from site_archive_pangeo.pangeo_cmip6 import PangeoCMIP6 | ||
|
|
||
| register_archive("pangeo")(site_archive_pangeo) | ||
|
|
||
| try: | ||
| from ._version import __version__ # type: ignore | ||
| except ImportError: | ||
| __version__ = "999" | ||
|
|
||
| __all__ = ["PangeoCMIP6"] |
213 changes: 213 additions & 0 deletions
213
packages/third_party/pangeo_site_archive/src/site_archive_pangeo/pangeo_cmip6.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| """ | ||
| Pangeo CMIP6 accessor. | ||
|
|
||
| Reads the Pangeo analysis-ready, cloud-optimised (ARCO) CMIP6 data, which is published as one Zarr store | ||
| per variable and described by a single catalog CSV. | ||
| """ | ||
|
|
||
| import os | ||
| import warnings | ||
| from typing import Any, ClassVar | ||
|
|
||
| import pandas | ||
| import pyearthtools.data | ||
| import xarray | ||
| from pyearthtools.data import Petdt | ||
| from pyearthtools.data.archive import register_archive | ||
| from pyearthtools.data.indexes import ArchiveIndex | ||
| from pyearthtools.data.transforms import Transform, TransformCollection | ||
|
|
||
| DEFAULT_CATALOG = "https://storage.googleapis.com/cmip6/pangeo-cmip6.csv" | ||
|
|
||
| _CATALOG_COLUMNS = [ | ||
| "source_id", | ||
| "experiment_id", | ||
| "member_id", | ||
| "table_id", | ||
| "variable_id", | ||
| "grid_label", | ||
| "zstore", | ||
| "version", | ||
| ] | ||
|
|
||
|
|
||
| @register_archive( | ||
| "PangeoCMIP6", | ||
| sample_kwargs={"variables": ["tas"], "source_id": "CanESM5", "experiment_id": "historical"}, | ||
| ) | ||
| class PangeoCMIP6(ArchiveIndex): | ||
| """ | ||
| Data accessor for the Pangeo CMIP6 analysis-ready, cloud-optimised (ARCO) data. | ||
|
|
||
| A catalog CSV lists the Zarr store of every variable of every model run. This accessor selects one | ||
| model run (``source_id``, ``experiment_id``, ``member_id``, ``table_id``) and opens the store of each | ||
| requested variable lazily, so only the parts of the data that are used are downloaded. | ||
|
|
||
| A store holds all times, so ``filesystem`` ignores the requested time and the time is selected after | ||
| loading. | ||
|
|
||
| Examples: | ||
| >>> index = PangeoCMIP6(["tas"], source_id="CanESM5", experiment_id="historical") | ||
| >>> index.retrieve("2000-01") | ||
| """ | ||
|
|
||
| _OPEN_ARGS: ClassVar[dict[str, Any]] = {"consolidated": True, "storage_options": {"token": "anon"}} | ||
|
|
||
| def __init__( | ||
| self, | ||
| variables: list[str] | str, | ||
| *, | ||
| source_id: str, | ||
| experiment_id: str, | ||
| member_id: str = "r1i1p1f1", | ||
| table_id: str = "Amon", | ||
| grid_label: str | None = None, | ||
| catalog: str | os.PathLike = DEFAULT_CATALOG, | ||
| transforms: Transform | TransformCollection | None = None, | ||
| ): | ||
| """ | ||
| Set up the Pangeo CMIP6 accessor. | ||
|
|
||
| The catalog is not read here, only when data is first requested. | ||
|
|
||
| Args: | ||
| variables: Variables to retrieve, e.g. ``["tas", "pr"]``. | ||
| source_id: Model name, e.g. ``"CanESM5"``. | ||
| experiment_id: Experiment, e.g. ``"historical"`` or ``"ssp585"``. | ||
| member_id: Ensemble member. Defaults to ``"r1i1p1f1"``. | ||
| table_id: Frequency and realm table, e.g. ``"Amon"`` for monthly atmosphere. Defaults to ``"Amon"``. | ||
| grid_label: Grid, e.g. ``"gn"`` (native) or ``"gr"`` (regridded). Needed when a variable exists | ||
| on more than one grid. Defaults to None. | ||
| catalog: Location of the Pangeo catalog CSV, a URL or a local path. | ||
| transforms: Extra transforms to apply after loading. | ||
|
|
||
| Raises: | ||
| ValueError: If no variables are given. | ||
| """ | ||
| self._variables = [variables] if isinstance(variables, str) else list(variables) | ||
| if not self._variables: | ||
| raise ValueError("At least one variable is required.") | ||
|
|
||
| self._query = { | ||
| "source_id": source_id, | ||
| "experiment_id": experiment_id, | ||
| "member_id": member_id, | ||
| "table_id": table_id, | ||
| } | ||
| self._grid_label = grid_label | ||
| self._catalog_src = catalog | ||
| self._catalog = None # read lazily, see _load_catalog | ||
|
|
||
| extra_transforms = TransformCollection() if transforms is None else TransformCollection(transforms) | ||
| super().__init__( | ||
| transforms=TransformCollection([pyearthtools.data.transforms.variables.Trim(self._variables)]) | ||
| + extra_transforms, | ||
| ) | ||
| self.record_initialisation() | ||
|
|
||
| def _load_catalog(self) -> pandas.DataFrame: | ||
| """Read the catalog once, keeping only the rows of the requested variables and model run.""" | ||
| if self._catalog is None: | ||
| catalog = self.catalog(self._catalog_src) | ||
| catalog = catalog[catalog["variable_id"].isin(self._variables)] | ||
| for column, value in self._query.items(): | ||
| catalog = catalog[catalog[column] == value] | ||
| self._catalog = catalog.reset_index(drop=True) | ||
| return self._catalog | ||
|
|
||
| def filesystem(self, querytime: str | Petdt) -> dict[str, str]: | ||
| """ | ||
| Find the Zarr store of each variable. | ||
|
|
||
| Args: | ||
| querytime: Ignored, as a store holds all times. | ||
|
|
||
|
|
||
| Raises: | ||
| ValueError: If a variable has no store, exists on several grids and no ``grid_label`` was given, | ||
| or has several stores at its latest version. | ||
|
|
||
| Returns: | ||
| Store URL of each variable, using the latest version in the catalog. | ||
| """ | ||
| catalog = self._load_catalog() | ||
| stores = {} | ||
| for variable in self._variables: | ||
| rows = catalog[catalog["variable_id"] == variable] | ||
| if self._grid_label is not None: | ||
| rows = rows[rows["grid_label"] == self._grid_label] | ||
| if rows.empty: | ||
| raise ValueError(f"No Pangeo CMIP6 store for variable {variable!r} matching {self._query}") | ||
| grids = sorted(rows["grid_label"].unique()) | ||
| if len(grids) > 1: | ||
| raise ValueError( | ||
| f"Variable {variable!r} exists on several grids {grids}; pass grid_label to choose one." | ||
| ) | ||
| latest = rows[rows["version"] == rows["version"].max()] | ||
| urls = sorted(latest["zstore"].unique()) | ||
| if len(urls) > 1: | ||
| raise ValueError( | ||
| f"Variable {variable!r} has several stores at the latest version {urls}; the run is ambiguous." | ||
| ) | ||
| stores[variable] = str(urls[0]) | ||
| return stores | ||
|
|
||
| def load(self, files: dict[str, str], **kwargs) -> xarray.Dataset: | ||
| """ | ||
| Open the store of each variable lazily, keep only that variable, and merge them. | ||
|
|
||
| Args: | ||
| files: Store URL of each variable, as found by ``filesystem``. | ||
| **kwargs: Passed to ``xarray.open_zarr``. | ||
|
|
||
| Returns: | ||
| Dataset holding all times of all the variables. Scalar coordinates that differ between the variables | ||
| are dropped, e.g. ``height``, which is 2 m for ``tas`` but 10 m for ``uas``. | ||
| """ | ||
| open_args = {**self._OPEN_ARGS, **kwargs} | ||
| datasets = [xarray.open_zarr(url, **open_args)[[variable]] for variable, url in files.items()] | ||
| return xarray.merge(datasets, join="exact", compat="minimal") | ||
|
|
||
| def retrieve(self, querytime: str | Petdt, *args, **kwargs) -> xarray.Dataset: | ||
| """ | ||
| Retrieve the data at ``querytime``. | ||
|
|
||
| Raises: | ||
| DataNotFoundError: If the store holds no data at ``querytime``. Without this, the parent class | ||
| only warns and returns every time of the store. | ||
| """ | ||
| with warnings.catch_warnings(record=True) as caught: | ||
| warnings.simplefilter("always") | ||
| data = super().retrieve(querytime, *args, **kwargs) | ||
| for warning in caught: | ||
| if "Could not find time in dataset" in str(warning.message): | ||
| raise pyearthtools.data.DataNotFoundError(f"No data found at {querytime}.") | ||
| warnings.warn_explicit(warning.message, warning.category, warning.filename, warning.lineno) | ||
| return data | ||
|
|
||
| @staticmethod | ||
| def catalog(catalog: str | os.PathLike = DEFAULT_CATALOG) -> pandas.DataFrame: | ||
| """Return the Pangeo CMIP6 catalogue, so the available data can be discovered. | ||
|
|
||
| Args: | ||
| catalog: CSV catalogue to read. Defaults to the public Pangeo CMIP6 catalogue. | ||
|
|
||
| Returns: | ||
| DataFrame with one row per variable of one model run, with the columns `source_id`, | ||
| `experiment_id`, `member_id`, `table_id`, `variable_id`, `grid_label`, `zstore` and `version`. | ||
| """ | ||
| return pandas.read_csv(catalog, usecols=_CATALOG_COLUMNS) | ||
|
|
||
| def exists(self, *args, **kwargs) -> bool: | ||
| """Whether a store is found for every variable. Stores are URLs, so they are not checked on disk.""" | ||
| try: | ||
| self.search(*args) | ||
| except ValueError: | ||
| return False | ||
| return True | ||
|
|
||
| @property | ||
| def _desc_(self): | ||
| return { | ||
| "singleline": "Pangeo CMIP6 analysis-ready, cloud-optimised data accessor", | ||
| "Documentation": "https://pangeo-data.github.io/pangeo-cmip6-cloud/accessing_data.html", | ||
| } | ||
10 changes: 10 additions & 0 deletions
10
packages/third_party/pangeo_site_archive/tests/data/mini_catalog.csv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| activity_id,institution_id,source_id,experiment_id,member_id,table_id,variable_id,grid_label,zstore,dcpp_init_year,version | ||
| CMIP,TEST-INST,TEST-MODEL,historical,r1i1p1f1,Amon,tas,gn,gs://test-bucket/tas/gn/v20190101/,,20190101 | ||
| CMIP,TEST-INST,TEST-MODEL,historical,r1i1p1f1,Amon,tas,gn,gs://test-bucket/tas/gn/v20200101/,,20200101 | ||
| CMIP,TEST-INST,TEST-MODEL,historical,r1i1p1f1,Amon,pr,gn,gs://test-bucket/pr/gn/v20190101/,,20190101 | ||
| CMIP,TEST-INST,TEST-MODEL,historical,r1i1p1f1,Amon,psl,gn,gs://test-bucket/psl/gn/v20190101/,,20190101 | ||
| CMIP,TEST-INST,TEST-MODEL,historical,r1i1p1f1,Amon,psl,gr,gs://test-bucket/psl/gr/v20190101/,,20190101 | ||
| ScenarioMIP,TEST-INST,TEST-MODEL,ssp585,r1i1p1f1,Amon,tas,gn,gs://test-bucket/ssp/tas/gn/v20190101/,,20190101 | ||
| CMIP,TEST-INST,TEST-MODEL,historical,r1i1p1f1,Amon,uas,gn,gs://test-bucket/uas/gn/v20190101/,,20190101 | ||
| DCPP,TEST-INST,AMBIG-MODEL,dcppA-hindcast,r1i1p1f1,Amon,tas,gn,gs://test-bucket/ambig/tas/gn/init1960/,1960,20190101 | ||
| DCPP,TEST-INST,AMBIG-MODEL,dcppA-hindcast,r1i1p1f1,Amon,tas,gn,gs://test-bucket/ambig/tas/gn/init1961/,1961,20190101 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @ahmedmohiduet this looks really good. One change that would be good would be to do some of the operations in the notebooks using a pyearthtools pipeline. Like in python there is a "pythonic" way to do things, so there is a way of doing things we're trying to promote in pyearthtools,l especially in the tutorial/demo notebooks. So for example, doing the unit conversion could be done in a operation in a pipeline. Here is some example code I have create for this purpose. The transform class could just be in the notebook, or you could contribute it to the pipeline or data module. Here's something I put together:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could then use either in a pipeline:
or you could supply the transform to the index so you get the data object with the right units
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated. Unit conversion now runs through
ConvertUnitsinPipeline(index, ConvertUnits(...)). Also addedtransforms=route onPangeoCMIP6, and time series uses same transform plusDrop.ConvertUnitsstays in notebook for now. Your call if it belongs inpyearthtools.data.Applied Copilot suggestions in separate commit:
retrieveraisesDataNotFoundErrorfor missing times,filesystemraisesValueErrorfor ambiguous runs at latest version, and typo fixed inGallery.ipynb. Tests added, all offline.