cf-xarray 0.10.11__tar.gz → 0.11.1__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.11 → cf_xarray-0.11.1}/.github/workflows/ci.yaml +2 -2
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.github/workflows/pypi.yaml +5 -5
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.github/workflows/testpypi-release.yaml +2 -2
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.pre-commit-config.yaml +2 -2
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/PKG-INFO +1 -1
- cf_xarray-0.11.1/cf_xarray/_version.py +1 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/accessor.py +148 -82
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/helpers.py +16 -17
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_accessor.py +50 -4
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/utils.py +8 -5
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray.egg-info/PKG-INFO +1 -1
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/index.rst +1 -1
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/uv.lock +265 -235
- cf_xarray-0.10.11/cf_xarray/_version.py +0 -1
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.binder/environment.yml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.deepsource.toml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.github/dependabot.yml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.github/release.yml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.github/workflows/parse_logs.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.github/workflows/upstream-dev-ci.yaml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.gitignore +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.readthedocs.yml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/.tributors +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/CITATION.cff +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/LICENSE +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/README.rst +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/__init__.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/coding.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/criteria.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/datasets.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/formatting.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/geometry.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/groupers.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/options.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/parametric.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/py.typed +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/scripts/make_doc.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/scripts/print_versions.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/sgrid.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/__init__.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/conftest.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_coding.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_geometry.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_groupers.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_helpers.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_options.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_parametric.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_scripts.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/tests/test_units.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray/units.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray.egg-info/SOURCES.txt +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray.egg-info/dependency_links.txt +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray.egg-info/requires.txt +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/cf_xarray.egg-info/top_level.txt +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/codecov.yml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/2D_bounds_averaged.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/2D_bounds_error.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/2D_bounds_nonunique.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/Makefile +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/_static/dataset-diagram-logo.tex +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/_static/full-logo.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/_static/logo.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/_static/logo.svg +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/_static/rich-repr-example.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/_static/style.css +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/api.rst +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/bounds.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/cartopy_rotated_pole.png +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/coding.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/conf.py +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/contributing.rst +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/coord_axes.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/custom-criteria.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/dsg.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/examples/introduction.ipynb +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/faq.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/flags.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/geometry.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/grid_mappings.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/howtouse.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/make.bat +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/parametricz.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/plotting.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/provenance.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/quickstart.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/roadmap.rst +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/selecting.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/sgrid_ugrid.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/units.md +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/doc/whats-new.rst +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/pyproject.toml +0 -0
- {cf_xarray-0.10.11 → cf_xarray-0.11.1}/setup.cfg +0 -0
|
@@ -58,7 +58,7 @@ jobs:
|
|
|
58
58
|
pytest -n auto --cov=./ --cov-report=xml
|
|
59
59
|
|
|
60
60
|
- name: Upload code coverage to Codecov
|
|
61
|
-
uses: codecov/codecov-action@
|
|
61
|
+
uses: codecov/codecov-action@v6.0.0
|
|
62
62
|
with:
|
|
63
63
|
file: ./coverage.xml
|
|
64
64
|
flags: unittests
|
|
@@ -98,7 +98,7 @@ jobs:
|
|
|
98
98
|
python -m mypy --install-types --non-interactive --cobertura-xml-report mypy_report cf_xarray/
|
|
99
99
|
|
|
100
100
|
- name: Upload mypy coverage to Codecov
|
|
101
|
-
uses: codecov/codecov-action@
|
|
101
|
+
uses: codecov/codecov-action@v6.0.0
|
|
102
102
|
with:
|
|
103
103
|
file: mypy_report/cobertura.xml
|
|
104
104
|
flags: mypy
|
|
@@ -41,7 +41,7 @@ jobs:
|
|
|
41
41
|
else
|
|
42
42
|
echo "✅ Looks good"
|
|
43
43
|
fi
|
|
44
|
-
- uses: actions/upload-artifact@
|
|
44
|
+
- uses: actions/upload-artifact@v7
|
|
45
45
|
with:
|
|
46
46
|
name: releases
|
|
47
47
|
path: dist
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
name: Install Python
|
|
55
55
|
with:
|
|
56
56
|
python-version: "3.11"
|
|
57
|
-
- uses: actions/download-artifact@
|
|
57
|
+
- uses: actions/download-artifact@v8
|
|
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.14.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@v8
|
|
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.14.0
|
|
100
100
|
with:
|
|
101
101
|
verbose: true
|
|
@@ -53,7 +53,7 @@ jobs:
|
|
|
53
53
|
echo "✅ Looks good"
|
|
54
54
|
fi
|
|
55
55
|
|
|
56
|
-
- uses: actions/upload-artifact@
|
|
56
|
+
- uses: actions/upload-artifact@v7
|
|
57
57
|
with:
|
|
58
58
|
name: releases
|
|
59
59
|
path: dist
|
|
@@ -66,7 +66,7 @@ jobs:
|
|
|
66
66
|
name: Install Python
|
|
67
67
|
with:
|
|
68
68
|
python-version: "3.11"
|
|
69
|
-
- uses: actions/download-artifact@
|
|
69
|
+
- uses: actions/download-artifact@v8
|
|
70
70
|
with:
|
|
71
71
|
name: releases
|
|
72
72
|
path: dist
|
|
@@ -10,7 +10,7 @@ repos:
|
|
|
10
10
|
|
|
11
11
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
12
12
|
# Ruff version.
|
|
13
|
-
rev: 'v0.
|
|
13
|
+
rev: 'v0.15.9'
|
|
14
14
|
hooks:
|
|
15
15
|
- id: ruff
|
|
16
16
|
args: ["--fix", "--show-fixes"]
|
|
@@ -55,7 +55,7 @@ repos:
|
|
|
55
55
|
- id: validate-cff
|
|
56
56
|
|
|
57
57
|
- repo: https://github.com/abravalheri/validate-pyproject
|
|
58
|
-
rev: v0.
|
|
58
|
+
rev: v0.25
|
|
59
59
|
hooks:
|
|
60
60
|
- id: validate-pyproject
|
|
61
61
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.1"
|
|
@@ -27,6 +27,7 @@ import xarray as xr
|
|
|
27
27
|
from xarray import DataArray, Dataset
|
|
28
28
|
from xarray.core.groupby import GroupBy
|
|
29
29
|
from xarray.core.resample import Resample
|
|
30
|
+
from xarray.core.utils import Frozen
|
|
30
31
|
|
|
31
32
|
try:
|
|
32
33
|
from xarray.core.rolling import ( # type:ignore[import-not-found,no-redef,unused-ignore]
|
|
@@ -48,6 +49,11 @@ except ImportError:
|
|
|
48
49
|
Weighted,
|
|
49
50
|
)
|
|
50
51
|
|
|
52
|
+
try:
|
|
53
|
+
from regex import match as regex_match
|
|
54
|
+
except ImportError:
|
|
55
|
+
from re import match as regex_match # type: ignore[no-redef]
|
|
56
|
+
|
|
51
57
|
|
|
52
58
|
from . import parametric, sgrid
|
|
53
59
|
from .criteria import (
|
|
@@ -305,12 +311,6 @@ def _get_custom_criteria(
|
|
|
305
311
|
List[str]
|
|
306
312
|
Variable name(s) in parent xarray object that matches axis, coordinate, or custom ``key``
|
|
307
313
|
"""
|
|
308
|
-
|
|
309
|
-
try:
|
|
310
|
-
from regex import match as regex_match
|
|
311
|
-
except ImportError:
|
|
312
|
-
from re import match as regex_match # type: ignore[no-redef]
|
|
313
|
-
|
|
314
314
|
if criteria is None:
|
|
315
315
|
if not OPTIONS["custom_criteria"]:
|
|
316
316
|
return []
|
|
@@ -496,7 +496,10 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
|
|
|
496
496
|
return list(results)
|
|
497
497
|
|
|
498
498
|
|
|
499
|
-
|
|
499
|
+
@functools.lru_cache(maxsize=256)
|
|
500
|
+
def _parse_grid_mapping_attribute(
|
|
501
|
+
grid_mapping_attr: str,
|
|
502
|
+
) -> Mapping[str, list[Hashable]]:
|
|
500
503
|
"""
|
|
501
504
|
Parse a grid_mapping attribute that may contain multiple grid mappings.
|
|
502
505
|
|
|
@@ -508,11 +511,12 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
|
|
|
508
511
|
- Multiple: "spatial_ref: crs_4326: latitude longitude crs_27700: x27700 y27700"
|
|
509
512
|
-> {"spatial_ref": [], "crs_4326": ["latitude", "longitude"], "crs_27700": ["x27700", "y27700"]}
|
|
510
513
|
|
|
511
|
-
Returns a
|
|
514
|
+
Returns a read-only mapping from grid mapping variable name to its associated
|
|
515
|
+
coordinate variables. The result is memoized, so callers must not mutate it.
|
|
512
516
|
"""
|
|
513
517
|
# Check if there are colons indicating multiple mappings
|
|
514
518
|
if ":" not in grid_mapping_attr:
|
|
515
|
-
return {grid_mapping_attr.strip(): []}
|
|
519
|
+
return Frozen({grid_mapping_attr.strip(): []})
|
|
516
520
|
|
|
517
521
|
# Use regex to parse the format
|
|
518
522
|
# First, find all grid mapping variables (words before colons)
|
|
@@ -520,7 +524,7 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
|
|
|
520
524
|
grid_mappings = re.findall(grid_pattern, grid_mapping_attr)
|
|
521
525
|
|
|
522
526
|
if not grid_mappings:
|
|
523
|
-
return {grid_mapping_attr.strip(): []}
|
|
527
|
+
return Frozen({grid_mapping_attr.strip(): []})
|
|
524
528
|
|
|
525
529
|
result: dict[str, list[Hashable]] = {}
|
|
526
530
|
|
|
@@ -549,13 +553,13 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
|
|
|
549
553
|
else:
|
|
550
554
|
result[gm] = []
|
|
551
555
|
|
|
552
|
-
return result
|
|
556
|
+
return Frozen(result)
|
|
553
557
|
|
|
554
558
|
|
|
555
559
|
def _create_grid_mapping(
|
|
556
560
|
var_name: str,
|
|
557
561
|
ds: Dataset,
|
|
558
|
-
grid_mapping_dict:
|
|
562
|
+
grid_mapping_dict: Mapping[str, list[Hashable]],
|
|
559
563
|
) -> GridMapping:
|
|
560
564
|
"""
|
|
561
565
|
Create a GridMapping dataclass instance from a grid mapping variable.
|
|
@@ -1008,7 +1012,11 @@ def _getattr(
|
|
|
1008
1012
|
newmap.update(dict.fromkeys(inverted[key], value))
|
|
1009
1013
|
newmap.update({key: attribute[key] for key in unused_keys})
|
|
1010
1014
|
|
|
1011
|
-
skip: dict[
|
|
1015
|
+
skip: dict[
|
|
1016
|
+
str,
|
|
1017
|
+
list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1018
|
+
| None,
|
|
1019
|
+
] = {
|
|
1012
1020
|
"data_vars": ["coords"],
|
|
1013
1021
|
"coords": None,
|
|
1014
1022
|
}
|
|
@@ -1049,7 +1057,8 @@ def _getattr(
|
|
|
1049
1057
|
def _getitem(
|
|
1050
1058
|
accessor: CFAccessor,
|
|
1051
1059
|
key: Hashable,
|
|
1052
|
-
skip: list[Literal["coords", "measures"]]
|
|
1060
|
+
skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1061
|
+
| None = None,
|
|
1053
1062
|
) -> DataArray: ...
|
|
1054
1063
|
|
|
1055
1064
|
|
|
@@ -1057,14 +1066,16 @@ def _getitem(
|
|
|
1057
1066
|
def _getitem(
|
|
1058
1067
|
accessor: CFAccessor,
|
|
1059
1068
|
key: Iterable[Hashable],
|
|
1060
|
-
skip: list[Literal["coords", "measures"]]
|
|
1069
|
+
skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1070
|
+
| None = None,
|
|
1061
1071
|
) -> Dataset: ...
|
|
1062
1072
|
|
|
1063
1073
|
|
|
1064
1074
|
def _getitem(
|
|
1065
1075
|
accessor: CFAccessor,
|
|
1066
1076
|
key: Hashable | Iterable[Hashable],
|
|
1067
|
-
skip: list[Literal["coords", "measures"]]
|
|
1077
|
+
skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1078
|
+
| None = None,
|
|
1068
1079
|
):
|
|
1069
1080
|
"""
|
|
1070
1081
|
Index into obj using key. Attaches CF associated variables.
|
|
@@ -1078,9 +1089,14 @@ def _getitem(
|
|
|
1078
1089
|
"""
|
|
1079
1090
|
|
|
1080
1091
|
obj = accessor._obj
|
|
1081
|
-
all_bounds = obj.cf.bounds if isinstance(obj, Dataset) else {}
|
|
1082
1092
|
kind = str(type(obj).__name__)
|
|
1083
1093
|
scalar_key = isinstance(key, Hashable)
|
|
1094
|
+
# obj.cf.bounds is expensive; only compute it when scalar lookup on a
|
|
1095
|
+
# Dataset actually needs to drop bounds variables.
|
|
1096
|
+
if not isinstance(obj, DataArray) and scalar_key:
|
|
1097
|
+
all_bounds = obj.cf.bounds
|
|
1098
|
+
else:
|
|
1099
|
+
all_bounds = {}
|
|
1084
1100
|
|
|
1085
1101
|
key_iter: Iterable[Hashable]
|
|
1086
1102
|
if isinstance(key, Hashable): # using scalar_key breaks mypy type narrowing
|
|
@@ -1128,60 +1144,90 @@ def _getitem(
|
|
|
1128
1144
|
|
|
1129
1145
|
custom_criteria = ChainMap(*OPTIONS["custom_criteria"])
|
|
1130
1146
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1147
|
+
# Fast path: when every key is just a plain variable in the Dataset (no CF
|
|
1148
|
+
# special meaning), skip the per-key classification loop below. Test the
|
|
1149
|
+
# cheap predicates first and only build the reserved-name set / consult
|
|
1150
|
+
# accessor.standard_names (a full attrs scan) if those could pass — the
|
|
1151
|
+
# common ds.cf["X"] / ds.cf["longitude"] paths must not pay that cost.
|
|
1152
|
+
fast_path = (
|
|
1153
|
+
isinstance(obj, Dataset)
|
|
1154
|
+
and not skip
|
|
1155
|
+
and all(k in obj._variables for k in key_iter)
|
|
1156
|
+
)
|
|
1157
|
+
if fast_path:
|
|
1158
|
+
reserved: set[Hashable] = set(_AXIS_NAMES).union(
|
|
1159
|
+
_COORD_NAMES,
|
|
1160
|
+
_GEOMETRY_TYPES,
|
|
1161
|
+
("geometry",),
|
|
1162
|
+
measures,
|
|
1163
|
+
grid_mapping_names,
|
|
1164
|
+
custom_criteria,
|
|
1165
|
+
cf_role_criteria,
|
|
1166
|
+
)
|
|
1167
|
+
standard_names = accessor.standard_names
|
|
1168
|
+
fast_path = all(k not in reserved and k not in standard_names for k in key_iter)
|
|
1169
|
+
|
|
1170
|
+
varnames: list[Hashable]
|
|
1171
|
+
coords: list[Hashable]
|
|
1172
|
+
if fast_path:
|
|
1173
|
+
varnames = list(key_iter)
|
|
1174
|
+
coords = []
|
|
1175
|
+
successful = dict.fromkeys(key_iter, True)
|
|
1176
|
+
else:
|
|
1177
|
+
varnames = []
|
|
1178
|
+
coords = []
|
|
1179
|
+
successful = dict.fromkeys(key_iter, False)
|
|
1180
|
+
for k in key_iter:
|
|
1181
|
+
if "coords" not in skip and k in _AXIS_NAMES + _COORD_NAMES:
|
|
1182
|
+
names = _get_all(obj, k)
|
|
1183
|
+
names = drop_bounds(names)
|
|
1184
|
+
check_results(names, k)
|
|
1185
|
+
successful[k] = bool(names)
|
|
1186
|
+
coords.extend(names)
|
|
1187
|
+
elif "measures" not in skip and k in measures:
|
|
1188
|
+
measure = _get_all(obj, k)
|
|
1189
|
+
check_results(measure, k)
|
|
1190
|
+
successful[k] = bool(measure)
|
|
1191
|
+
if measure:
|
|
1192
|
+
varnames.extend(measure)
|
|
1193
|
+
elif "grid_mapping_names" not in skip and k in grid_mapping_names:
|
|
1194
|
+
grid_mapping = _get_all(obj, k)
|
|
1195
|
+
check_results(grid_mapping, k)
|
|
1196
|
+
successful[k] = bool(grid_mapping)
|
|
1197
|
+
if grid_mapping:
|
|
1198
|
+
varnames.extend(grid_mapping)
|
|
1199
|
+
elif "geometries" not in skip and (k == "geometry" or k in _GEOMETRY_TYPES):
|
|
1200
|
+
geometries = _get_all(obj, k)
|
|
1201
|
+
if geometries and k in _GEOMETRY_TYPES:
|
|
1202
|
+
new = itertools.chain(
|
|
1203
|
+
_parse_related_geometry_vars(
|
|
1204
|
+
ChainMap(obj[g].attrs, obj[g].encoding)
|
|
1205
|
+
)
|
|
1206
|
+
for g in geometries
|
|
1159
1207
|
)
|
|
1160
|
-
|
|
1161
|
-
)
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
varnames.extend(stdnames - objcoords)
|
|
1184
|
-
coords.extend(stdnames & objcoords)
|
|
1208
|
+
geometries.extend(*new)
|
|
1209
|
+
if len(geometries) > 1 and scalar_key:
|
|
1210
|
+
raise ValueError(
|
|
1211
|
+
f"CF geometries must be represented by an Xarray Dataset. To request a Dataset in return please pass `[{k!r}]` instead."
|
|
1212
|
+
)
|
|
1213
|
+
successful[k] = bool(geometries)
|
|
1214
|
+
if geometries:
|
|
1215
|
+
varnames.extend(geometries)
|
|
1216
|
+
elif k in custom_criteria or k in cf_role_criteria:
|
|
1217
|
+
names = _get_all(obj, k)
|
|
1218
|
+
check_results(names, k)
|
|
1219
|
+
successful[k] = bool(names)
|
|
1220
|
+
varnames.extend(names)
|
|
1221
|
+
else:
|
|
1222
|
+
stdnames = set(_get_with_standard_name(obj, k))
|
|
1223
|
+
objcoords = set(obj.coords)
|
|
1224
|
+
stdnames = drop_bounds(stdnames)
|
|
1225
|
+
if "coords" in skip:
|
|
1226
|
+
stdnames -= objcoords
|
|
1227
|
+
check_results(stdnames, k)
|
|
1228
|
+
successful[k] = bool(stdnames)
|
|
1229
|
+
varnames.extend(stdnames - objcoords)
|
|
1230
|
+
coords.extend(stdnames & objcoords)
|
|
1185
1231
|
|
|
1186
1232
|
# these are not special names but could be variable names in underlying object
|
|
1187
1233
|
# we allow this so that we can return variables with appropriate CF auxiliary variables
|
|
@@ -1814,10 +1860,6 @@ class CFAccessor:
|
|
|
1814
1860
|
for k, v in value.items()
|
|
1815
1861
|
)
|
|
1816
1862
|
)
|
|
1817
|
-
|
|
1818
|
-
elif value is Ellipsis:
|
|
1819
|
-
pass
|
|
1820
|
-
|
|
1821
1863
|
else:
|
|
1822
1864
|
# things like sum which have dim
|
|
1823
1865
|
newvalue = [
|
|
@@ -2047,7 +2089,7 @@ class CFAccessor:
|
|
|
2047
2089
|
]
|
|
2048
2090
|
as_dataset = self._maybe_to_dataset().reset_coords()
|
|
2049
2091
|
|
|
2050
|
-
keys = {}
|
|
2092
|
+
keys: dict[str, str] = {}
|
|
2051
2093
|
for attr in set(all_attrs):
|
|
2052
2094
|
try:
|
|
2053
2095
|
keys.update(parse_cell_methods_attr(attr))
|
|
@@ -2840,14 +2882,38 @@ class CFDatasetAccessor(CFAccessor):
|
|
|
2840
2882
|
"""
|
|
2841
2883
|
|
|
2842
2884
|
obj = self._obj
|
|
2843
|
-
|
|
2885
|
+
variables = obj._variables
|
|
2886
|
+
|
|
2887
|
+
# Single linear scan to discover which variables in the dataset have
|
|
2888
|
+
# a ``bounds`` attribute pointing at another existing variable. The
|
|
2889
|
+
# naive implementation called ``apply_mapper(_get_bounds, ...)`` for
|
|
2890
|
+
# every key in ``self.keys() | set(variables)``, which itself fans
|
|
2891
|
+
# out through ``_get_all`` and runs the regex criteria against every
|
|
2892
|
+
# variable's attrs. That is O(num_keys × num_vars × num_criteria)
|
|
2893
|
+
# for an answer that is structurally a single attribute lookup.
|
|
2894
|
+
var_to_bounds: dict[Hashable, Hashable] = {}
|
|
2895
|
+
for name, var in variables.items():
|
|
2896
|
+
attrs_or_encoding = ChainMap(var.attrs, var.encoding)
|
|
2897
|
+
bounds_name = attrs_or_encoding.get("bounds")
|
|
2898
|
+
if bounds_name is not None and bounds_name in variables:
|
|
2899
|
+
var_to_bounds[name] = bounds_name
|
|
2900
|
+
|
|
2901
|
+
if not var_to_bounds:
|
|
2902
|
+
return {}
|
|
2844
2903
|
|
|
2845
|
-
vardict = {
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2904
|
+
vardict: dict[Hashable, set[Hashable]] = {}
|
|
2905
|
+
# Direct variable-name keys: O(1) lookup, no mapper machinery.
|
|
2906
|
+
for name, bounds_name in var_to_bounds.items():
|
|
2907
|
+
vardict[name] = {bounds_name}
|
|
2908
|
+
|
|
2909
|
+
# CF keys (axes, coordinate names, custom criteria, ...): resolve to
|
|
2910
|
+
# their target variable names once, then read the precomputed
|
|
2911
|
+
# ``var_to_bounds`` map. ``_get_bounds`` is no longer invoked.
|
|
2912
|
+
for key in self.keys() - set(variables):
|
|
2913
|
+
target_vars = apply_mapper(_get_all, obj, key, error=False, default=[])
|
|
2914
|
+
bounds_vars = {var_to_bounds[v] for v in target_vars if v in var_to_bounds}
|
|
2915
|
+
if bounds_vars:
|
|
2916
|
+
vardict[key] = bounds_vars
|
|
2851
2917
|
|
|
2852
2918
|
return {k: sort_maybe_hashable(v) for k, v in vardict.items() if v}
|
|
2853
2919
|
|
|
@@ -17,24 +17,23 @@ def _guess_bounds_1d(da, dim):
|
|
|
17
17
|
"""
|
|
18
18
|
if dim not in da.dims:
|
|
19
19
|
(dim,) = da.cf.axes[dim]
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
upper = da
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
|
|
21
|
+
bound_position = 0.5
|
|
22
|
+
|
|
23
|
+
diff = da.diff(dim).pad({dim: (1, 1)}, mode="edge")
|
|
24
|
+
lower = da.copy(
|
|
25
|
+
deep=False,
|
|
26
|
+
data=da.data - bound_position * diff.isel({dim: slice(0, -1)}).data,
|
|
27
|
+
)
|
|
28
|
+
upper = da.copy(
|
|
29
|
+
deep=False,
|
|
30
|
+
data=da.data + bound_position * diff.isel({dim: slice(1, None)}).data,
|
|
31
|
+
)
|
|
32
|
+
return (
|
|
33
|
+
xr.concat([lower, upper], dim="bounds")
|
|
34
|
+
.transpose(..., "bounds")
|
|
35
|
+
.drop_attrs(deep=False)
|
|
33
36
|
)
|
|
34
|
-
result = xr.concat([first, bounds], dim=dim).transpose(..., "bounds")
|
|
35
|
-
if ADDED_INDEX:
|
|
36
|
-
result = result.drop_vars(dim)
|
|
37
|
-
return result.drop_attrs(deep=False)
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def _guess_bounds_2d(da, dims):
|
|
@@ -3,6 +3,7 @@ import pickle
|
|
|
3
3
|
import warnings
|
|
4
4
|
from textwrap import dedent
|
|
5
5
|
|
|
6
|
+
import dask.array
|
|
6
7
|
import matplotlib as mpl
|
|
7
8
|
import numpy as np
|
|
8
9
|
import pandas as pd
|
|
@@ -839,6 +840,51 @@ def test_add_bounds(dims):
|
|
|
839
840
|
_check_unchanged(original, ds)
|
|
840
841
|
|
|
841
842
|
|
|
843
|
+
def test_add_bounds_preserves_array_type() -> None:
|
|
844
|
+
# Test that the array type of the bounds variable is the same as the original variable.
|
|
845
|
+
ds = airds
|
|
846
|
+
original = ds.copy(deep=True)
|
|
847
|
+
ds = ds.drop_indexes("lat").rename_dims(lat="x")
|
|
848
|
+
ds["lat"] = ds.lat.copy(data=dask.array.asarray(ds.lat.data))
|
|
849
|
+
added = ds.cf.add_bounds("lat")
|
|
850
|
+
|
|
851
|
+
assert isinstance(added.lat.data, dask.array.Array)
|
|
852
|
+
assert isinstance(added.lat_bounds.data, dask.array.Array)
|
|
853
|
+
|
|
854
|
+
assert isinstance(ds.lat.data, dask.array.Array)
|
|
855
|
+
_check_unchanged(original, ds)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def test_add_irregularly_spaced_bounds_do_not_overlap() -> None:
|
|
859
|
+
# Test that added bounds with irregular spacing do not overlap.
|
|
860
|
+
ds = airds
|
|
861
|
+
original = ds.copy(deep=True)
|
|
862
|
+
ds["time"] = ds["time"].copy(data=pd.date_range("2013-01", "2013-04", freq="MS"))
|
|
863
|
+
expected = xr.DataArray(
|
|
864
|
+
data=[
|
|
865
|
+
pd.to_datetime(t)
|
|
866
|
+
for t in [
|
|
867
|
+
["2012-12-16T12", "2013-01-16T12"],
|
|
868
|
+
["2013-01-16T12", "2013-02-15T00"],
|
|
869
|
+
["2013-02-15T00", "2013-03-16T12"],
|
|
870
|
+
["2013-03-16T12", "2013-04-16T12"],
|
|
871
|
+
]
|
|
872
|
+
],
|
|
873
|
+
dims=["time", "bounds"],
|
|
874
|
+
name="time_bounds",
|
|
875
|
+
coords={"time": ds.time.data},
|
|
876
|
+
)
|
|
877
|
+
added = ds.copy(deep=False)
|
|
878
|
+
added = added.cf.add_bounds("time")
|
|
879
|
+
|
|
880
|
+
name = "time_bounds"
|
|
881
|
+
assert name in added.coords
|
|
882
|
+
assert added["time"].attrs["bounds"] == name
|
|
883
|
+
assert_allclose(added[name].reset_coords(drop=True), expected)
|
|
884
|
+
|
|
885
|
+
_check_unchanged(original, ds)
|
|
886
|
+
|
|
887
|
+
|
|
842
888
|
def test_add_bounds_multiple() -> None:
|
|
843
889
|
# Test multiple dimensions
|
|
844
890
|
assert not {"x1_bounds", "x2_bounds"} <= set(multiple.variables)
|
|
@@ -853,7 +899,7 @@ def test_add_bounds_nd_variable() -> None:
|
|
|
853
899
|
|
|
854
900
|
# 2D
|
|
855
901
|
expected = (
|
|
856
|
-
vertices_to_bounds(
|
|
902
|
+
vertices_to_bounds(
|
|
857
903
|
xr.DataArray(
|
|
858
904
|
np.arange(0, 13, 3).reshape(5, 1) + np.arange(-2, 2).reshape(1, 4),
|
|
859
905
|
dims=("x", "y"),
|
|
@@ -861,7 +907,7 @@ def test_add_bounds_nd_variable() -> None:
|
|
|
861
907
|
out_dims=("bounds", "x", "y"),
|
|
862
908
|
)
|
|
863
909
|
.rename("z_bounds")
|
|
864
|
-
.assign_coords(**ds.coords)
|
|
910
|
+
.assign_coords(**ds.coords) # type: ignore[arg-type]
|
|
865
911
|
)
|
|
866
912
|
actual = ds.cf.add_bounds("z").z_bounds.reset_coords(drop=True)
|
|
867
913
|
xr.testing.assert_identical(actual, expected)
|
|
@@ -882,11 +928,11 @@ def test_add_bounds_nd_variable() -> None:
|
|
|
882
928
|
expected = (
|
|
883
929
|
xr.concat([ds.z - 1.5, ds.z + 1.5], dim="bounds")
|
|
884
930
|
.rename("z_bounds")
|
|
885
|
-
.transpose("bounds"
|
|
931
|
+
.transpose(..., "bounds")
|
|
886
932
|
)
|
|
887
933
|
|
|
888
934
|
actual = ds.cf.add_bounds("z", dim="x").z_bounds.reset_coords(drop=True)
|
|
889
|
-
xr.testing.assert_identical(expected
|
|
935
|
+
xr.testing.assert_identical(expected, actual)
|
|
890
936
|
|
|
891
937
|
# Requesting bounds on a non-variable dimension
|
|
892
938
|
with pytest.raises(ValueError, match="are dimensions with no index."):
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import inspect
|
|
2
3
|
import os
|
|
3
4
|
import warnings
|
|
4
5
|
from collections import defaultdict
|
|
5
|
-
from collections.abc import Iterable
|
|
6
|
+
from collections.abc import Iterable, Mapping
|
|
6
7
|
from typing import Any
|
|
7
8
|
from xml.etree import ElementTree
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
10
11
|
from xarray import DataArray
|
|
12
|
+
from xarray.core.utils import Frozen
|
|
11
13
|
|
|
12
14
|
try:
|
|
13
15
|
import cftime
|
|
@@ -64,7 +66,8 @@ def _is_datetime_like(da: DataArray) -> bool:
|
|
|
64
66
|
return False
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
@functools.lru_cache(maxsize=256)
|
|
70
|
+
def parse_cell_methods_attr(attr: str) -> Mapping[str, str]:
|
|
68
71
|
"""
|
|
69
72
|
Parse cell_methods attributes (format is 'measure: name').
|
|
70
73
|
|
|
@@ -75,14 +78,14 @@ def parse_cell_methods_attr(attr: str) -> dict[str, str]:
|
|
|
75
78
|
|
|
76
79
|
Returns
|
|
77
80
|
-------
|
|
78
|
-
|
|
81
|
+
Read-only mapping from measure to name.
|
|
79
82
|
"""
|
|
80
83
|
strings = [s for scolons in attr.split(":") for s in scolons.split()]
|
|
81
84
|
if len(strings) % 2 != 0:
|
|
82
85
|
raise ValueError(f"attrs['cell_measures'] = {attr!r} is malformed.")
|
|
83
86
|
|
|
84
|
-
return
|
|
85
|
-
zip(strings[slice(0, None, 2)], strings[slice(1, None, 2)], strict=False)
|
|
87
|
+
return Frozen(
|
|
88
|
+
dict(zip(strings[slice(0, None, 2)], strings[slice(1, None, 2)], strict=False))
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
|
|
@@ -35,7 +35,7 @@ or using ``conda``
|
|
|
35
35
|
examples/introduction
|
|
36
36
|
EarthCube 2021 demo <https://earthcube2021.github.io/ec21_book/notebooks/ec21_cherian_etal/DC_01_cf-xarray.html>
|
|
37
37
|
CESM ocean model demo <https://pop-tools.readthedocs.io/en/latest/examples/cesm-cmip-cf-xarray.html>
|
|
38
|
-
COSIMA ocean-sea ice model demo <https://cosima-recipes.readthedocs.io/en/latest/
|
|
38
|
+
COSIMA ocean-sea ice model demo <https://cosima-recipes.readthedocs.io/en/latest/01-Cooking-Lessons-101/02-Advanced/Model_Agnostic_Analysis.html>
|
|
39
39
|
|
|
40
40
|
.. toctree::
|
|
41
41
|
:maxdepth: 2
|