cf-xarray 0.10.7__tar.gz → 0.10.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/workflows/ci.yaml +4 -4
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/workflows/pypi.yaml +5 -5
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/workflows/testpypi-release.yaml +2 -2
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/workflows/upstream-dev-ci.yaml +1 -1
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/PKG-INFO +1 -1
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/__init__.py +1 -1
- cf_xarray-0.10.8/cf_xarray/_version.py +1 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/accessor.py +345 -22
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/datasets.py +38 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/helpers.py +16 -3
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/__init__.py +1 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_accessor.py +201 -3
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_helpers.py +35 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray.egg-info/PKG-INFO +1 -1
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/ci/doc.yml +2 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/ci/environment-all-min-deps.yml +1 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/ci/environment.yml +1 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/ci/upstream-dev-env.yml +1 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/api.rst +3 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/grid_mappings.md +83 -18
- cf_xarray-0.10.7/cf_xarray/_version.py +0 -1
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.binder/environment.yml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.deepsource.toml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/dependabot.yml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/release.yml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.github/workflows/parse_logs.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.gitignore +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.pre-commit-config.yaml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.readthedocs.yml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/.tributors +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/CITATION.cff +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/LICENSE +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/README.rst +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/coding.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/criteria.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/formatting.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/geometry.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/groupers.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/options.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/parametric.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/py.typed +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/scripts/make_doc.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/scripts/print_versions.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/sgrid.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/conftest.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_coding.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_geometry.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_groupers.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_options.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_parametric.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_scripts.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/tests/test_units.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/units.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray/utils.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray.egg-info/SOURCES.txt +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray.egg-info/dependency_links.txt +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray.egg-info/requires.txt +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/cf_xarray.egg-info/top_level.txt +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/ci/environment-no-optional-deps.yml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/codecov.yml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/2D_bounds_averaged.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/2D_bounds_error.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/2D_bounds_nonunique.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/Makefile +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/_static/dataset-diagram-logo.tex +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/_static/full-logo.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/_static/logo.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/_static/logo.svg +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/_static/rich-repr-example.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/_static/style.css +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/bounds.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/cartopy_rotated_pole.png +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/coding.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/conf.py +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/contributing.rst +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/coord_axes.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/custom-criteria.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/dsg.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/examples/introduction.ipynb +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/faq.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/flags.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/geometry.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/howtouse.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/index.rst +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/make.bat +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/parametricz.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/plotting.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/provenance.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/quickstart.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/roadmap.rst +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/selecting.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/sgrid_ugrid.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/units.md +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/doc/whats-new.rst +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/pyproject.toml +0 -0
- {cf_xarray-0.10.7 → cf_xarray-0.10.8}/setup.cfg +0 -0
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
python-version: "3.13"
|
|
38
38
|
os: ubuntu-latest
|
|
39
39
|
steps:
|
|
40
|
-
- uses: actions/checkout@
|
|
40
|
+
- uses: actions/checkout@v5
|
|
41
41
|
with:
|
|
42
42
|
fetch-depth: 0 # Fetch all history for all branches and tags.
|
|
43
43
|
- name: Set environment variables
|
|
@@ -70,7 +70,7 @@ jobs:
|
|
|
70
70
|
run: |
|
|
71
71
|
pytest -n auto --cov=./ --cov-report=xml
|
|
72
72
|
- name: Upload code coverage to Codecov
|
|
73
|
-
uses: codecov/codecov-action@v5.
|
|
73
|
+
uses: codecov/codecov-action@v5.5.0
|
|
74
74
|
with:
|
|
75
75
|
file: ./coverage.xml
|
|
76
76
|
flags: unittests
|
|
@@ -88,7 +88,7 @@ jobs:
|
|
|
88
88
|
matrix:
|
|
89
89
|
python-version: ["3.11", "3.13"]
|
|
90
90
|
steps:
|
|
91
|
-
- uses: actions/checkout@
|
|
91
|
+
- uses: actions/checkout@v5
|
|
92
92
|
with:
|
|
93
93
|
fetch-depth: 0 # Fetch all history for all branches and tags.
|
|
94
94
|
- name: Set up conda environment
|
|
@@ -109,7 +109,7 @@ jobs:
|
|
|
109
109
|
run: |
|
|
110
110
|
python -m mypy --install-types --non-interactive --cobertura-xml-report mypy_report cf_xarray/
|
|
111
111
|
- name: Upload mypy coverage to Codecov
|
|
112
|
-
uses: codecov/codecov-action@v5.
|
|
112
|
+
uses: codecov/codecov-action@v5.5.0
|
|
113
113
|
with:
|
|
114
114
|
file: mypy_report/cobertura.xml
|
|
115
115
|
flags: mypy
|
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
if: github.repository == 'xarray-contrib/cf-xarray'
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
16
|
with:
|
|
17
17
|
fetch-depth: 0
|
|
18
18
|
- uses: actions/setup-python@v5
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
name: Install Python
|
|
55
55
|
with:
|
|
56
56
|
python-version: "3.10"
|
|
57
|
-
- uses: actions/download-artifact@
|
|
57
|
+
- uses: actions/download-artifact@v5
|
|
58
58
|
with:
|
|
59
59
|
name: releases
|
|
60
60
|
path: dist
|
|
@@ -72,7 +72,7 @@ jobs:
|
|
|
72
72
|
|
|
73
73
|
- name: Publish package to TestPyPI
|
|
74
74
|
if: github.event_name == 'push'
|
|
75
|
-
uses: pypa/gh-action-pypi-publish@v1.
|
|
75
|
+
uses: pypa/gh-action-pypi-publish@v1.13.0
|
|
76
76
|
with:
|
|
77
77
|
password: ${{ secrets.TESTPYPI_TOKEN }}
|
|
78
78
|
repository_url: https://test.pypi.org/legacy/
|
|
@@ -91,11 +91,11 @@ jobs:
|
|
|
91
91
|
id-token: write
|
|
92
92
|
|
|
93
93
|
steps:
|
|
94
|
-
- uses: actions/download-artifact@
|
|
94
|
+
- uses: actions/download-artifact@v5
|
|
95
95
|
with:
|
|
96
96
|
name: releases
|
|
97
97
|
path: dist
|
|
98
98
|
- name: Publish package to PyPI
|
|
99
|
-
uses: pypa/gh-action-pypi-publish@v1.
|
|
99
|
+
uses: pypa/gh-action-pypi-publish@v1.13.0
|
|
100
100
|
with:
|
|
101
101
|
verbose: true
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
if: ${{ contains( github.event.pull_request.labels.*.name, 'test-build') && github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }}
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v5
|
|
21
21
|
with:
|
|
22
22
|
fetch-depth: 0
|
|
23
23
|
|
|
@@ -66,7 +66,7 @@ jobs:
|
|
|
66
66
|
name: Install Python
|
|
67
67
|
with:
|
|
68
68
|
python-version: "3.10"
|
|
69
|
-
- uses: actions/download-artifact@
|
|
69
|
+
- uses: actions/download-artifact@v5
|
|
70
70
|
with:
|
|
71
71
|
name: releases
|
|
72
72
|
path: dist
|
|
@@ -3,7 +3,7 @@ from packaging.version import Version
|
|
|
3
3
|
|
|
4
4
|
from . import geometry as geometry
|
|
5
5
|
from . import sgrid # noqa
|
|
6
|
-
from .accessor import CFAccessor # noqa
|
|
6
|
+
from .accessor import CFAccessor, GridMapping # noqa
|
|
7
7
|
from .coding import ( # noqa
|
|
8
8
|
decode_compress_to_multi_index,
|
|
9
9
|
encode_multi_index_as_compress,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.10.8"
|
|
@@ -4,7 +4,7 @@ import functools
|
|
|
4
4
|
import inspect
|
|
5
5
|
import itertools
|
|
6
6
|
import re
|
|
7
|
-
from collections import ChainMap, namedtuple
|
|
7
|
+
from collections import ChainMap, defaultdict, namedtuple
|
|
8
8
|
from collections.abc import (
|
|
9
9
|
Callable,
|
|
10
10
|
Hashable,
|
|
@@ -13,6 +13,7 @@ from collections.abc import (
|
|
|
13
13
|
MutableMapping,
|
|
14
14
|
Sequence,
|
|
15
15
|
)
|
|
16
|
+
from dataclasses import dataclass
|
|
16
17
|
from datetime import datetime
|
|
17
18
|
from typing import (
|
|
18
19
|
Any,
|
|
@@ -82,6 +83,61 @@ from .utils import (
|
|
|
82
83
|
|
|
83
84
|
FlagParam = namedtuple("FlagParam", ["flag_mask", "flag_value"])
|
|
84
85
|
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True, kw_only=True)
|
|
88
|
+
class GridMapping:
|
|
89
|
+
"""
|
|
90
|
+
Represents a CF grid mapping with its properties and associated coordinate variables.
|
|
91
|
+
|
|
92
|
+
Attributes
|
|
93
|
+
----------
|
|
94
|
+
name : str
|
|
95
|
+
The CF grid mapping name (e.g., ``'latitude_longitude'``, ``'transverse_mercator'``)
|
|
96
|
+
crs : pyproj.CRS
|
|
97
|
+
The coordinate reference system object
|
|
98
|
+
array : xarray.DataArray
|
|
99
|
+
The grid mapping variable as a DataArray containing the CRS parameters
|
|
100
|
+
coordinates : tuple[Hashable, ...]
|
|
101
|
+
Names of coordinate variables associated with this grid mapping. For grid mappings
|
|
102
|
+
that are explicitly listed with coordinates in the grid_mapping attribute
|
|
103
|
+
(e.g., ``'spatial_ref: crs_4326: latitude longitude'``), this contains those coordinates.
|
|
104
|
+
For grid mappings (e.g. ``spatial_ref``) that don't explicitly specify coordinates,
|
|
105
|
+
this falls back to the dimension names of the data variable that references
|
|
106
|
+
this grid mapping.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
name: str
|
|
110
|
+
crs: Any # really pyproj.CRS
|
|
111
|
+
array: xr.DataArray
|
|
112
|
+
coordinates: tuple[Hashable, ...]
|
|
113
|
+
|
|
114
|
+
def __repr__(self) -> str:
|
|
115
|
+
# Try to get EPSG code first, fallback to shorter description
|
|
116
|
+
try:
|
|
117
|
+
if hasattr(self.crs, "to_epsg") and self.crs.to_epsg():
|
|
118
|
+
crs_repr = f"<CRS: EPSG:{self.crs.to_epsg()}>"
|
|
119
|
+
else:
|
|
120
|
+
# Use the name if available, otherwise authority:code
|
|
121
|
+
crs_name = getattr(self.crs, "name", str(self.crs)[:50] + "...")
|
|
122
|
+
crs_repr = f"<CRS: {crs_name}>"
|
|
123
|
+
except Exception:
|
|
124
|
+
# Fallback to generic representation
|
|
125
|
+
crs_repr = "<CRS>"
|
|
126
|
+
|
|
127
|
+
# Short array representation - name and shape
|
|
128
|
+
array_repr = f"<DataArray '{self.array.name}' {self.array.shape}>"
|
|
129
|
+
|
|
130
|
+
# Format coordinates nicely
|
|
131
|
+
coords_repr = f"({', '.join(repr(c) for c in self.coordinates)})"
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
f"GridMapping(name={self.name!r}, "
|
|
135
|
+
f"crs={crs_repr}, "
|
|
136
|
+
f"array={array_repr}, "
|
|
137
|
+
f"coordinates={coords_repr})"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
85
141
|
#: Classes wrapped by cf_xarray.
|
|
86
142
|
_WRAPPED_CLASSES = (Resample, GroupBy, Rolling, Coarsen, Weighted)
|
|
87
143
|
|
|
@@ -343,7 +399,7 @@ def _get_axis_coord(obj: DataArray | Dataset, key: str) -> list[str]:
|
|
|
343
399
|
results.update((coord,))
|
|
344
400
|
if criterion == "units":
|
|
345
401
|
# deal with pint-backed objects
|
|
346
|
-
units = getattr(var.
|
|
402
|
+
units = getattr(var.variable._data, "units", None)
|
|
347
403
|
if units in expected:
|
|
348
404
|
results.update((coord,))
|
|
349
405
|
|
|
@@ -440,6 +496,124 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
|
|
|
440
496
|
return list(results)
|
|
441
497
|
|
|
442
498
|
|
|
499
|
+
def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hashable]]:
|
|
500
|
+
"""
|
|
501
|
+
Parse a grid_mapping attribute that may contain multiple grid mappings.
|
|
502
|
+
|
|
503
|
+
The attribute has the format: "grid_mapping_variable_name: optional_coordinate_names_space_separated"
|
|
504
|
+
Multiple sections are separated by colons.
|
|
505
|
+
|
|
506
|
+
Examples:
|
|
507
|
+
- Single: "spatial_ref" -> {"spatial_ref": []}
|
|
508
|
+
- Multiple: "spatial_ref: crs_4326: latitude longitude crs_27700: x27700 y27700"
|
|
509
|
+
-> {"spatial_ref": [], "crs_4326": ["latitude", "longitude"], "crs_27700": ["x27700", "y27700"]}
|
|
510
|
+
|
|
511
|
+
Returns a dictionary mapping grid mapping variable names to their associated coordinate variables.
|
|
512
|
+
"""
|
|
513
|
+
# Check if there are colons indicating multiple mappings
|
|
514
|
+
if ":" not in grid_mapping_attr:
|
|
515
|
+
return {grid_mapping_attr.strip(): []}
|
|
516
|
+
|
|
517
|
+
# Use regex to parse the format
|
|
518
|
+
# First, find all grid mapping variables (words before colons)
|
|
519
|
+
grid_pattern = r"(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*)(?=\s*:)"
|
|
520
|
+
grid_mappings = re.findall(grid_pattern, grid_mapping_attr)
|
|
521
|
+
|
|
522
|
+
if not grid_mappings:
|
|
523
|
+
return {grid_mapping_attr.strip(): []}
|
|
524
|
+
|
|
525
|
+
result: dict[str, list[Hashable]] = {}
|
|
526
|
+
|
|
527
|
+
# Now extract coordinates for each grid mapping
|
|
528
|
+
# Split the string to find what comes after each grid mapping variable
|
|
529
|
+
for i, gm in enumerate(grid_mappings):
|
|
530
|
+
# Pattern to capture everything after this grid mapping until the next one or end
|
|
531
|
+
if i < len(grid_mappings) - 1:
|
|
532
|
+
next_gm = grid_mappings[i + 1]
|
|
533
|
+
# Capture everything between current grid mapping and next one
|
|
534
|
+
coord_pattern = (
|
|
535
|
+
rf"{re.escape(gm)}\s*:\s*([^:]*?)(?=\s+{re.escape(next_gm)}\s*:)"
|
|
536
|
+
)
|
|
537
|
+
else:
|
|
538
|
+
# Last grid mapping - capture everything after it
|
|
539
|
+
coord_pattern = rf"{re.escape(gm)}\s*:\s*(.*)$"
|
|
540
|
+
|
|
541
|
+
coord_match = re.search(coord_pattern, grid_mapping_attr)
|
|
542
|
+
if coord_match:
|
|
543
|
+
coord_text = coord_match.group(1).strip()
|
|
544
|
+
# Split coordinates and filter out any grid mapping names that might have been captured
|
|
545
|
+
coords = coord_text.split() if coord_text else []
|
|
546
|
+
# Filter out the next grid mapping variable if it got captured
|
|
547
|
+
coords = [c for c in coords if c not in grid_mappings]
|
|
548
|
+
result[gm] = coords # type: ignore[assignment]
|
|
549
|
+
else:
|
|
550
|
+
result[gm] = []
|
|
551
|
+
|
|
552
|
+
return result
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _create_grid_mapping(
|
|
556
|
+
var_name: str,
|
|
557
|
+
ds: Dataset,
|
|
558
|
+
grid_mapping_dict: dict[str, list[Hashable]],
|
|
559
|
+
) -> GridMapping:
|
|
560
|
+
"""
|
|
561
|
+
Create a GridMapping dataclass instance from a grid mapping variable.
|
|
562
|
+
|
|
563
|
+
Parameters
|
|
564
|
+
----------
|
|
565
|
+
var_name : str
|
|
566
|
+
Name of the grid mapping variable
|
|
567
|
+
obj_dataset : Dataset
|
|
568
|
+
Dataset containing the grid mapping variable
|
|
569
|
+
grid_mapping_dict : dict[str, list[Hashable]]
|
|
570
|
+
Dictionary mapping grid mapping variable names to their coordinate variables
|
|
571
|
+
|
|
572
|
+
Returns
|
|
573
|
+
-------
|
|
574
|
+
GridMapping
|
|
575
|
+
GridMapping dataclass instance
|
|
576
|
+
|
|
577
|
+
Notes
|
|
578
|
+
-----
|
|
579
|
+
Assumes pyproj is available.
|
|
580
|
+
"""
|
|
581
|
+
import pyproj
|
|
582
|
+
|
|
583
|
+
var = ds._variables[var_name]
|
|
584
|
+
|
|
585
|
+
# Create DataArray from Variable, preserving the name
|
|
586
|
+
da = xr.DataArray(ds._variables[var_name], name=var_name)
|
|
587
|
+
|
|
588
|
+
# Get the CF grid mapping name from the variable's attributes
|
|
589
|
+
cf_name = var.attrs.get("grid_mapping_name", var_name)
|
|
590
|
+
|
|
591
|
+
# Create CRS from the grid mapping variable
|
|
592
|
+
crs = pyproj.CRS.from_cf(var.attrs)
|
|
593
|
+
|
|
594
|
+
# Get associated coordinate variables, fallback to dimension names
|
|
595
|
+
coordinates: list[Hashable] = grid_mapping_dict.get(var_name, [])
|
|
596
|
+
# """
|
|
597
|
+
# In order to make use of a grid mapping to directly calculate latitude and longitude values
|
|
598
|
+
# it is necessary to associate the coordinate variables with the independent variables of the mapping.
|
|
599
|
+
# This is done by assigning a standard_name to the coordinate variable.
|
|
600
|
+
# The appropriate values of the standard_name depend on the grid mapping and are given in Appendix F, Grid Mappings.
|
|
601
|
+
# """
|
|
602
|
+
if not coordinates and len(grid_mapping_dict) == 1:
|
|
603
|
+
if crs.to_cf().get("grid_mapping_name") == "rotated_latitude_longitude":
|
|
604
|
+
xname, yname = "grid_longitude", "grid_latitude"
|
|
605
|
+
elif crs.is_geographic:
|
|
606
|
+
xname, yname = "longitude", "latitude"
|
|
607
|
+
elif crs.is_projected:
|
|
608
|
+
xname, yname = "projection_x_coordinate", "projection_y_coordinate"
|
|
609
|
+
|
|
610
|
+
x = apply_mapper(_get_with_standard_name, ds, xname, error=False, default=[[]])
|
|
611
|
+
y = apply_mapper(_get_with_standard_name, ds, yname, error=False, default=[[]])
|
|
612
|
+
coordinates = list(itertools.chain(x, y))
|
|
613
|
+
|
|
614
|
+
return GridMapping(name=cf_name, crs=crs, array=da, coordinates=tuple(coordinates))
|
|
615
|
+
|
|
616
|
+
|
|
443
617
|
def _get_grid_mapping_name(obj: DataArray | Dataset, key: str) -> list[str]:
|
|
444
618
|
"""
|
|
445
619
|
Translate from grid mapping name attribute to appropriate variable name.
|
|
@@ -466,14 +640,16 @@ def _get_grid_mapping_name(obj: DataArray | Dataset, key: str) -> list[str]:
|
|
|
466
640
|
results = set()
|
|
467
641
|
for var in variables.values():
|
|
468
642
|
attrs_or_encoding = ChainMap(var.attrs, var.encoding)
|
|
469
|
-
if "grid_mapping"
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
643
|
+
if grid_mapping_attr := attrs_or_encoding.get("grid_mapping"):
|
|
644
|
+
# Parse potentially multiple grid mappings
|
|
645
|
+
grid_mapping_dict = _parse_grid_mapping_attribute(grid_mapping_attr)
|
|
646
|
+
for grid_mapping_var_name in grid_mapping_dict.keys():
|
|
647
|
+
if grid_mapping_var_name not in variables:
|
|
648
|
+
raise ValueError(
|
|
649
|
+
f"{var} defines non-existing grid_mapping variable {grid_mapping_var_name}."
|
|
650
|
+
)
|
|
651
|
+
if key == variables[grid_mapping_var_name].attrs["grid_mapping_name"]:
|
|
652
|
+
results.update([grid_mapping_var_name])
|
|
477
653
|
return list(results)
|
|
478
654
|
|
|
479
655
|
|
|
@@ -1943,9 +2119,20 @@ class CFAccessor:
|
|
|
1943
2119
|
if dbounds := self._obj[dim].attrs.get("bounds", None):
|
|
1944
2120
|
coords["bounds"].append(dbounds)
|
|
1945
2121
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2122
|
+
if grid := attrs_or_encoding.get("grid", None):
|
|
2123
|
+
coords["grid"] = [grid]
|
|
2124
|
+
|
|
2125
|
+
if grid_mapping_attr := attrs_or_encoding.get("grid_mapping", None):
|
|
2126
|
+
# Parse grid mapping variables and their coordinates
|
|
2127
|
+
grid_mapping_dict = _parse_grid_mapping_attribute(grid_mapping_attr)
|
|
2128
|
+
coords["grid_mapping"] = cast(
|
|
2129
|
+
list[Hashable], list(grid_mapping_dict.keys())
|
|
2130
|
+
)
|
|
2131
|
+
|
|
2132
|
+
# Add coordinate variables from the grid mapping
|
|
2133
|
+
for coord_vars in grid_mapping_dict.values():
|
|
2134
|
+
if coord_vars:
|
|
2135
|
+
coords["coordinates"].extend(coord_vars)
|
|
1949
2136
|
|
|
1950
2137
|
more: Sequence[Hashable] = ()
|
|
1951
2138
|
if geometry_var := attrs_or_encoding.get("geometry", None):
|
|
@@ -2337,6 +2524,95 @@ class CFAccessor:
|
|
|
2337
2524
|
|
|
2338
2525
|
return obj
|
|
2339
2526
|
|
|
2527
|
+
@property
|
|
2528
|
+
def grid_mappings(self) -> tuple[GridMapping, ...]:
|
|
2529
|
+
"""
|
|
2530
|
+
Return a tuple of GridMapping objects for all grid mappings in this object.
|
|
2531
|
+
|
|
2532
|
+
For DataArrays, the order in the tuple matches the order that grid mappings appear
|
|
2533
|
+
in the grid_mapping attribute string.
|
|
2534
|
+
|
|
2535
|
+
Parameters
|
|
2536
|
+
----------
|
|
2537
|
+
None
|
|
2538
|
+
|
|
2539
|
+
Returns
|
|
2540
|
+
-------
|
|
2541
|
+
tuple[GridMapping, ...]
|
|
2542
|
+
Tuple of GridMapping dataclass instances, each containing:
|
|
2543
|
+
- name: CF grid mapping name
|
|
2544
|
+
- crs: pyproj.CRS object
|
|
2545
|
+
- array: xarray.DataArray containing the grid mapping variable
|
|
2546
|
+
- coordinates: tuple of coordinate variable names
|
|
2547
|
+
|
|
2548
|
+
Raises
|
|
2549
|
+
------
|
|
2550
|
+
ImportError
|
|
2551
|
+
If pyproj is not available. This property requires pyproj for CRS creation.
|
|
2552
|
+
|
|
2553
|
+
Examples
|
|
2554
|
+
--------
|
|
2555
|
+
>>> ds.cf.grid_mappings
|
|
2556
|
+
(GridMapping(name='latitude_longitude', crs=<CRS: EPSG:4326>, ...),)
|
|
2557
|
+
|
|
2558
|
+
Notes
|
|
2559
|
+
-----
|
|
2560
|
+
This property requires pyproj to be installed for creating CRS objects from
|
|
2561
|
+
CF grid mapping parameters. Install with: ``conda install pyproj`` or
|
|
2562
|
+
``pip install pyproj``.
|
|
2563
|
+
"""
|
|
2564
|
+
# Check pyproj availability upfront
|
|
2565
|
+
try:
|
|
2566
|
+
import pyproj # noqa: F401
|
|
2567
|
+
except ImportError:
|
|
2568
|
+
raise ImportError(
|
|
2569
|
+
"pyproj is required for .cf.grid_mappings property. "
|
|
2570
|
+
"Install with: conda install pyproj or pip install pyproj"
|
|
2571
|
+
) from None
|
|
2572
|
+
# For DataArrays, preserve order from grid_mapping attribute
|
|
2573
|
+
if isinstance(self._obj, DataArray) and "grid_mapping" in self._obj.attrs:
|
|
2574
|
+
grid_mapping_dict = _parse_grid_mapping_attribute(
|
|
2575
|
+
self._obj.attrs["grid_mapping"]
|
|
2576
|
+
)
|
|
2577
|
+
# Get grid mappings in the order they appear in the string
|
|
2578
|
+
ordered_var_names = list(grid_mapping_dict.keys())
|
|
2579
|
+
else:
|
|
2580
|
+
# For Datasets, look for grid_mapping attributes in data variables
|
|
2581
|
+
grid_mapping_dict = {}
|
|
2582
|
+
ordered_var_names = []
|
|
2583
|
+
|
|
2584
|
+
# Search all data variables for grid_mapping attributes
|
|
2585
|
+
for _var_name, var in self._obj.data_vars.items():
|
|
2586
|
+
if "grid_mapping" in var.attrs:
|
|
2587
|
+
parsed = _parse_grid_mapping_attribute(var.attrs["grid_mapping"])
|
|
2588
|
+
grid_mapping_dict.update(parsed)
|
|
2589
|
+
# Add variables in order they appear in this grid_mapping string
|
|
2590
|
+
for gm_var in parsed.keys():
|
|
2591
|
+
if gm_var not in ordered_var_names:
|
|
2592
|
+
ordered_var_names.append(gm_var)
|
|
2593
|
+
|
|
2594
|
+
# If no grid_mapping attributes found in data vars, try grid_mapping_names property
|
|
2595
|
+
if not ordered_var_names and hasattr(self, "grid_mapping_names"):
|
|
2596
|
+
grid_mapping_names = self.grid_mapping_names
|
|
2597
|
+
for var_names in grid_mapping_names.values():
|
|
2598
|
+
ordered_var_names.extend(var_names)
|
|
2599
|
+
|
|
2600
|
+
if not ordered_var_names:
|
|
2601
|
+
return ()
|
|
2602
|
+
|
|
2603
|
+
grid_mappings = []
|
|
2604
|
+
obj_dataset = self._maybe_to_dataset()
|
|
2605
|
+
|
|
2606
|
+
for var_name in ordered_var_names:
|
|
2607
|
+
if var_name not in obj_dataset._variables:
|
|
2608
|
+
continue
|
|
2609
|
+
|
|
2610
|
+
grid_mappings.append(
|
|
2611
|
+
_create_grid_mapping(var_name, obj_dataset, grid_mapping_dict)
|
|
2612
|
+
)
|
|
2613
|
+
|
|
2614
|
+
return tuple(grid_mappings)
|
|
2615
|
+
|
|
2340
2616
|
|
|
2341
2617
|
@xr.register_dataset_accessor("cf")
|
|
2342
2618
|
class CFDatasetAccessor(CFAccessor):
|
|
@@ -2899,6 +3175,49 @@ class CFDataArrayAccessor(CFAccessor):
|
|
|
2899
3175
|
terms[key] = value
|
|
2900
3176
|
return terms
|
|
2901
3177
|
|
|
3178
|
+
@property
|
|
3179
|
+
def grid_mapping_names(self) -> dict[str, list[str]]:
|
|
3180
|
+
"""
|
|
3181
|
+
Mapping the CF grid mapping name to the grid mapping variable name.
|
|
3182
|
+
|
|
3183
|
+
Returns
|
|
3184
|
+
-------
|
|
3185
|
+
dict
|
|
3186
|
+
Dictionary mapping the CF grid mapping name to the variable name containing
|
|
3187
|
+
the grid mapping attributes.
|
|
3188
|
+
|
|
3189
|
+
See Also
|
|
3190
|
+
--------
|
|
3191
|
+
DataArray.cf.grid_mapping_name
|
|
3192
|
+
Dataset.cf.grid_mapping_names
|
|
3193
|
+
|
|
3194
|
+
References
|
|
3195
|
+
----------
|
|
3196
|
+
https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#appendix-grid-mappings
|
|
3197
|
+
|
|
3198
|
+
Examples
|
|
3199
|
+
--------
|
|
3200
|
+
>>> from cf_xarray.datasets import hrrrds
|
|
3201
|
+
>>> hrrrds.foo.cf.grid_mapping_names
|
|
3202
|
+
{'latitude_longitude': ['crs_4326'], 'lambert_azimuthal_equal_area': ['spatial_ref']}
|
|
3203
|
+
"""
|
|
3204
|
+
da = self._obj
|
|
3205
|
+
attrs_or_encoding = ChainMap(da.attrs, da.encoding)
|
|
3206
|
+
grid_mapping_attr = attrs_or_encoding.get("grid_mapping", None)
|
|
3207
|
+
|
|
3208
|
+
if not grid_mapping_attr:
|
|
3209
|
+
return {}
|
|
3210
|
+
|
|
3211
|
+
# Parse potentially multiple grid mappings
|
|
3212
|
+
grid_mapping_dict = _parse_grid_mapping_attribute(grid_mapping_attr)
|
|
3213
|
+
|
|
3214
|
+
results = defaultdict(list)
|
|
3215
|
+
for grid_mapping_var_name in grid_mapping_dict.keys() & set(da.coords):
|
|
3216
|
+
grid_mapping_var = da.coords[grid_mapping_var_name]
|
|
3217
|
+
if gmn := grid_mapping_var.attrs.get("grid_mapping_name"):
|
|
3218
|
+
results[gmn].append(grid_mapping_var_name)
|
|
3219
|
+
return dict(results)
|
|
3220
|
+
|
|
2902
3221
|
@property
|
|
2903
3222
|
def grid_mapping_name(self) -> str:
|
|
2904
3223
|
"""
|
|
@@ -2911,6 +3230,7 @@ class CFDataArrayAccessor(CFAccessor):
|
|
|
2911
3230
|
|
|
2912
3231
|
See Also
|
|
2913
3232
|
--------
|
|
3233
|
+
DataArray.cf.grid_mapping_names
|
|
2914
3234
|
Dataset.cf.grid_mapping_names
|
|
2915
3235
|
|
|
2916
3236
|
Examples
|
|
@@ -2920,19 +3240,22 @@ class CFDataArrayAccessor(CFAccessor):
|
|
|
2920
3240
|
'rotated_latitude_longitude'
|
|
2921
3241
|
"""
|
|
2922
3242
|
|
|
2923
|
-
|
|
3243
|
+
# Use grid_mapping_names under the hood
|
|
3244
|
+
grid_mapping_names = self.grid_mapping_names
|
|
2924
3245
|
|
|
2925
|
-
|
|
2926
|
-
grid_mapping = attrs_or_encoding.get("grid_mapping", None)
|
|
2927
|
-
if not grid_mapping:
|
|
3246
|
+
if not grid_mapping_names:
|
|
2928
3247
|
raise ValueError("No 'grid_mapping' attribute present.")
|
|
2929
3248
|
|
|
2930
|
-
if
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3249
|
+
if len(grid_mapping_names) > 1:
|
|
3250
|
+
# Get the variable names for error message
|
|
3251
|
+
all_vars = list(itertools.chain.from_iterable(grid_mapping_names.values()))
|
|
3252
|
+
raise ValueError(
|
|
3253
|
+
f"Multiple grid mappings found: {all_vars}. "
|
|
3254
|
+
"Please use DataArray.cf.grid_mapping_names instead."
|
|
3255
|
+
)
|
|
2934
3256
|
|
|
2935
|
-
|
|
3257
|
+
# Return the single grid mapping name
|
|
3258
|
+
return next(iter(grid_mapping_names.keys()))
|
|
2936
3259
|
|
|
2937
3260
|
def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray:
|
|
2938
3261
|
"""
|
|
@@ -750,6 +750,44 @@ sgrid_delft3["grid"] = xr.DataArray(
|
|
|
750
750
|
)
|
|
751
751
|
|
|
752
752
|
|
|
753
|
+
try:
|
|
754
|
+
from pyproj import CRS
|
|
755
|
+
|
|
756
|
+
hrrrds = xr.Dataset()
|
|
757
|
+
hrrrds["foo"] = (
|
|
758
|
+
("x", "y"),
|
|
759
|
+
np.arange(200).reshape((10, 20)),
|
|
760
|
+
{
|
|
761
|
+
"grid_mapping": "spatial_ref: x y crs_4326: latitude longitude crs_27700: x27700 y27700"
|
|
762
|
+
},
|
|
763
|
+
)
|
|
764
|
+
hrrrds.coords["spatial_ref"] = ((), 0, CRS.from_epsg(3035).to_cf())
|
|
765
|
+
hrrrds.coords["crs_4326"] = ((), 0, CRS.from_epsg(4326).to_cf())
|
|
766
|
+
hrrrds.coords["crs_27700"] = ((), 0, CRS.from_epsg(27700).to_cf())
|
|
767
|
+
hrrrds.coords["latitude"] = (
|
|
768
|
+
("x", "y"),
|
|
769
|
+
np.ones((10, 20)),
|
|
770
|
+
{"standard_name": "latitude"},
|
|
771
|
+
)
|
|
772
|
+
hrrrds.coords["longitude"] = (
|
|
773
|
+
("x", "y"),
|
|
774
|
+
np.zeros((10, 20)),
|
|
775
|
+
{"standard_name": "longitude"},
|
|
776
|
+
)
|
|
777
|
+
hrrrds.coords["y27700"] = (
|
|
778
|
+
("x", "y"),
|
|
779
|
+
np.ones((10, 20)),
|
|
780
|
+
{"standard_name": "projected_x_coordinate"},
|
|
781
|
+
)
|
|
782
|
+
hrrrds.coords["x27700"] = (
|
|
783
|
+
("x", "y"),
|
|
784
|
+
np.zeros((10, 20)),
|
|
785
|
+
{"standard_name": "projected_y_coordinate"},
|
|
786
|
+
)
|
|
787
|
+
except ImportError:
|
|
788
|
+
pass
|
|
789
|
+
|
|
790
|
+
|
|
753
791
|
def point_dataset():
|
|
754
792
|
from shapely.geometry import MultiPoint, Point
|
|
755
793
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
from collections.abc import Hashable, Sequence
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
@@ -230,6 +231,10 @@ def _get_core_dim_orders(core_dim_coords: dict[str, np.ndarray]) -> dict[str, st
|
|
|
230
231
|
# Cast to float64 for safe comparison
|
|
231
232
|
diffs_float = diffs.astype("float64")
|
|
232
233
|
nonzero_diffs = diffs_float[diffs_float != 0]
|
|
234
|
+
elif isinstance(diffs[0], datetime.timedelta):
|
|
235
|
+
# For datetime timedelta, we use the total_seconds method
|
|
236
|
+
diffs_float = np.array([x.total_seconds() for x in diffs])
|
|
237
|
+
nonzero_diffs = diffs_float[diffs_float != 0]
|
|
233
238
|
else:
|
|
234
239
|
zero = 0
|
|
235
240
|
nonzero_diffs = diffs[diffs != zero]
|
|
@@ -360,9 +365,17 @@ def _is_bounds_monotonic(bounds: np.ndarray) -> bool:
|
|
|
360
365
|
# Cannot cast ufunc 'greater' input 0 from dtype('<m8[ns]') to dtype('<m8')
|
|
361
366
|
# with casting rule 'same_kind' To avoid this, always cast to float64 before
|
|
362
367
|
# np.diff.
|
|
363
|
-
|
|
364
|
-
diffs = np.diff(
|
|
365
|
-
|
|
368
|
+
|
|
369
|
+
diffs = np.diff(bounds.flatten())
|
|
370
|
+
|
|
371
|
+
if isinstance(diffs[0], datetime.timedelta):
|
|
372
|
+
# For datetime timedelta, we use the total_seconds method
|
|
373
|
+
diffs_float = np.array([x.total_seconds() for x in diffs])
|
|
374
|
+
|
|
375
|
+
else:
|
|
376
|
+
diffs_float = diffs.astype("float64")
|
|
377
|
+
|
|
378
|
+
nonzero_diffs = diffs_float[diffs_float != 0]
|
|
366
379
|
|
|
367
380
|
# All values are equal, treat as monotonic
|
|
368
381
|
if nonzero_diffs.size == 0:
|