cf-xarray 0.11.0__tar.gz → 0.11.2__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.11.0 → cf_xarray-0.11.2}/PKG-INFO +1 -1
- cf_xarray-0.11.2/cf_xarray/_version.py +1 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/accessor.py +183 -82
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/sgrid.py +25 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_accessor.py +75 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/utils.py +8 -5
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray.egg-info/PKG-INFO +1 -1
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/sgrid_ugrid.md +17 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/uv.lock +3 -3
- cf_xarray-0.11.0/cf_xarray/_version.py +0 -1
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.binder/environment.yml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.deepsource.toml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/dependabot.yml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/release.yml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/workflows/ci.yaml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/workflows/parse_logs.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/workflows/pypi.yaml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/workflows/testpypi-release.yaml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.github/workflows/upstream-dev-ci.yaml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.gitignore +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.pre-commit-config.yaml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.readthedocs.yml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/.tributors +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/CITATION.cff +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/LICENSE +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/README.rst +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/__init__.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/coding.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/criteria.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/datasets.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/formatting.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/geometry.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/groupers.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/helpers.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/options.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/parametric.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/py.typed +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/scripts/make_doc.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/scripts/print_versions.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/__init__.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/conftest.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_coding.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_geometry.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_groupers.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_helpers.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_options.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_parametric.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_scripts.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/tests/test_units.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray/units.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray.egg-info/SOURCES.txt +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray.egg-info/dependency_links.txt +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray.egg-info/requires.txt +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/cf_xarray.egg-info/top_level.txt +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/codecov.yml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/2D_bounds_averaged.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/2D_bounds_error.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/2D_bounds_nonunique.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/Makefile +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/_static/dataset-diagram-logo.tex +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/_static/full-logo.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/_static/logo.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/_static/logo.svg +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/_static/rich-repr-example.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/_static/style.css +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/api.rst +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/bounds.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/cartopy_rotated_pole.png +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/coding.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/conf.py +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/contributing.rst +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/coord_axes.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/custom-criteria.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/dsg.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/examples/introduction.ipynb +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/faq.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/flags.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/geometry.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/grid_mappings.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/howtouse.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/index.rst +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/make.bat +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/parametricz.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/plotting.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/provenance.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/quickstart.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/roadmap.rst +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/selecting.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/units.md +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/doc/whats-new.rst +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/pyproject.toml +0 -0
- {cf_xarray-0.11.0 → cf_xarray-0.11.2}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.11.2"
|
|
@@ -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]
|
|
@@ -495,7 +496,10 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
|
|
|
495
496
|
return list(results)
|
|
496
497
|
|
|
497
498
|
|
|
498
|
-
|
|
499
|
+
@functools.lru_cache(maxsize=256)
|
|
500
|
+
def _parse_grid_mapping_attribute(
|
|
501
|
+
grid_mapping_attr: str,
|
|
502
|
+
) -> Mapping[str, list[Hashable]]:
|
|
499
503
|
"""
|
|
500
504
|
Parse a grid_mapping attribute that may contain multiple grid mappings.
|
|
501
505
|
|
|
@@ -507,11 +511,12 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
|
|
|
507
511
|
- Multiple: "spatial_ref: crs_4326: latitude longitude crs_27700: x27700 y27700"
|
|
508
512
|
-> {"spatial_ref": [], "crs_4326": ["latitude", "longitude"], "crs_27700": ["x27700", "y27700"]}
|
|
509
513
|
|
|
510
|
-
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.
|
|
511
516
|
"""
|
|
512
517
|
# Check if there are colons indicating multiple mappings
|
|
513
518
|
if ":" not in grid_mapping_attr:
|
|
514
|
-
return {grid_mapping_attr.strip(): []}
|
|
519
|
+
return Frozen({grid_mapping_attr.strip(): []})
|
|
515
520
|
|
|
516
521
|
# Use regex to parse the format
|
|
517
522
|
# First, find all grid mapping variables (words before colons)
|
|
@@ -519,7 +524,7 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
|
|
|
519
524
|
grid_mappings = re.findall(grid_pattern, grid_mapping_attr)
|
|
520
525
|
|
|
521
526
|
if not grid_mappings:
|
|
522
|
-
return {grid_mapping_attr.strip(): []}
|
|
527
|
+
return Frozen({grid_mapping_attr.strip(): []})
|
|
523
528
|
|
|
524
529
|
result: dict[str, list[Hashable]] = {}
|
|
525
530
|
|
|
@@ -548,13 +553,44 @@ def _parse_grid_mapping_attribute(grid_mapping_attr: str) -> dict[str, list[Hash
|
|
|
548
553
|
else:
|
|
549
554
|
result[gm] = []
|
|
550
555
|
|
|
551
|
-
return result
|
|
556
|
+
return Frozen(result)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _hashable_attrs(attrs: Mapping[Any, Any]) -> tuple:
|
|
560
|
+
"""Return a hashable, order-independent representation of an attrs mapping.
|
|
561
|
+
|
|
562
|
+
List- and array-valued attributes (e.g. ``standard_parallel``) are coerced
|
|
563
|
+
to tuples so the result can be used as an ``lru_cache`` key.
|
|
564
|
+
"""
|
|
565
|
+
frozen = []
|
|
566
|
+
for key, value in attrs.items():
|
|
567
|
+
if hasattr(value, "tolist"): # numpy scalars/arrays
|
|
568
|
+
value = value.tolist()
|
|
569
|
+
if isinstance(value, list | tuple):
|
|
570
|
+
value = tuple(value)
|
|
571
|
+
frozen.append((key, value))
|
|
572
|
+
frozen.sort(key=lambda kv: repr(kv[0]))
|
|
573
|
+
return tuple(frozen)
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@functools.lru_cache(maxsize=256)
|
|
577
|
+
def _crs_from_cf_attrs(attrs_items: tuple) -> Any:
|
|
578
|
+
"""Build a ``pyproj.CRS`` from frozen CF grid-mapping attrs (memoized).
|
|
579
|
+
|
|
580
|
+
``pyproj.CRS.from_cf`` re-parses the datum/ellipsoid on every call, which is
|
|
581
|
+
expensive for grid mappings carrying explicit ellipsoid parameters (e.g.
|
|
582
|
+
geostationary). A dataset routinely references the same grid mapping from
|
|
583
|
+
many variables, so cache on the attribute items.
|
|
584
|
+
"""
|
|
585
|
+
import pyproj
|
|
586
|
+
|
|
587
|
+
return pyproj.CRS.from_cf(dict(attrs_items))
|
|
552
588
|
|
|
553
589
|
|
|
554
590
|
def _create_grid_mapping(
|
|
555
591
|
var_name: str,
|
|
556
592
|
ds: Dataset,
|
|
557
|
-
grid_mapping_dict:
|
|
593
|
+
grid_mapping_dict: Mapping[str, list[Hashable]],
|
|
558
594
|
) -> GridMapping:
|
|
559
595
|
"""
|
|
560
596
|
Create a GridMapping dataclass instance from a grid mapping variable.
|
|
@@ -664,7 +700,7 @@ def _create_grid_mapping(
|
|
|
664
700
|
}
|
|
665
701
|
)
|
|
666
702
|
else:
|
|
667
|
-
crs =
|
|
703
|
+
crs = _crs_from_cf_attrs(_hashable_attrs(var.attrs))
|
|
668
704
|
|
|
669
705
|
# Get associated coordinate variables, fallback to dimension names
|
|
670
706
|
coordinates: list[Hashable] = grid_mapping_dict.get(var_name, [])
|
|
@@ -1007,7 +1043,11 @@ def _getattr(
|
|
|
1007
1043
|
newmap.update(dict.fromkeys(inverted[key], value))
|
|
1008
1044
|
newmap.update({key: attribute[key] for key in unused_keys})
|
|
1009
1045
|
|
|
1010
|
-
skip: dict[
|
|
1046
|
+
skip: dict[
|
|
1047
|
+
str,
|
|
1048
|
+
list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1049
|
+
| None,
|
|
1050
|
+
] = {
|
|
1011
1051
|
"data_vars": ["coords"],
|
|
1012
1052
|
"coords": None,
|
|
1013
1053
|
}
|
|
@@ -1048,7 +1088,8 @@ def _getattr(
|
|
|
1048
1088
|
def _getitem(
|
|
1049
1089
|
accessor: CFAccessor,
|
|
1050
1090
|
key: Hashable,
|
|
1051
|
-
skip: list[Literal["coords", "measures"]]
|
|
1091
|
+
skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1092
|
+
| None = None,
|
|
1052
1093
|
) -> DataArray: ...
|
|
1053
1094
|
|
|
1054
1095
|
|
|
@@ -1056,14 +1097,16 @@ def _getitem(
|
|
|
1056
1097
|
def _getitem(
|
|
1057
1098
|
accessor: CFAccessor,
|
|
1058
1099
|
key: Iterable[Hashable],
|
|
1059
|
-
skip: list[Literal["coords", "measures"]]
|
|
1100
|
+
skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1101
|
+
| None = None,
|
|
1060
1102
|
) -> Dataset: ...
|
|
1061
1103
|
|
|
1062
1104
|
|
|
1063
1105
|
def _getitem(
|
|
1064
1106
|
accessor: CFAccessor,
|
|
1065
1107
|
key: Hashable | Iterable[Hashable],
|
|
1066
|
-
skip: list[Literal["coords", "measures"]]
|
|
1108
|
+
skip: list[Literal["coords", "measures", "grid_mapping_names", "geometries"]]
|
|
1109
|
+
| None = None,
|
|
1067
1110
|
):
|
|
1068
1111
|
"""
|
|
1069
1112
|
Index into obj using key. Attaches CF associated variables.
|
|
@@ -1077,9 +1120,14 @@ def _getitem(
|
|
|
1077
1120
|
"""
|
|
1078
1121
|
|
|
1079
1122
|
obj = accessor._obj
|
|
1080
|
-
all_bounds = obj.cf.bounds if isinstance(obj, Dataset) else {}
|
|
1081
1123
|
kind = str(type(obj).__name__)
|
|
1082
1124
|
scalar_key = isinstance(key, Hashable)
|
|
1125
|
+
# obj.cf.bounds is expensive; only compute it when scalar lookup on a
|
|
1126
|
+
# Dataset actually needs to drop bounds variables.
|
|
1127
|
+
if not isinstance(obj, DataArray) and scalar_key:
|
|
1128
|
+
all_bounds = obj.cf.bounds
|
|
1129
|
+
else:
|
|
1130
|
+
all_bounds = {}
|
|
1083
1131
|
|
|
1084
1132
|
key_iter: Iterable[Hashable]
|
|
1085
1133
|
if isinstance(key, Hashable): # using scalar_key breaks mypy type narrowing
|
|
@@ -1127,60 +1175,90 @@ def _getitem(
|
|
|
1127
1175
|
|
|
1128
1176
|
custom_criteria = ChainMap(*OPTIONS["custom_criteria"])
|
|
1129
1177
|
|
|
1130
|
-
|
|
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
|
-
|
|
1178
|
+
# Fast path: when every key is just a plain variable in the Dataset (no CF
|
|
1179
|
+
# special meaning), skip the per-key classification loop below. Test the
|
|
1180
|
+
# cheap predicates first and only build the reserved-name set / consult
|
|
1181
|
+
# accessor.standard_names (a full attrs scan) if those could pass — the
|
|
1182
|
+
# common ds.cf["X"] / ds.cf["longitude"] paths must not pay that cost.
|
|
1183
|
+
fast_path = (
|
|
1184
|
+
isinstance(obj, Dataset)
|
|
1185
|
+
and not skip
|
|
1186
|
+
and all(k in obj._variables for k in key_iter)
|
|
1187
|
+
)
|
|
1188
|
+
if fast_path:
|
|
1189
|
+
reserved: set[Hashable] = set(_AXIS_NAMES).union(
|
|
1190
|
+
_COORD_NAMES,
|
|
1191
|
+
_GEOMETRY_TYPES,
|
|
1192
|
+
("geometry",),
|
|
1193
|
+
measures,
|
|
1194
|
+
grid_mapping_names,
|
|
1195
|
+
custom_criteria,
|
|
1196
|
+
cf_role_criteria,
|
|
1197
|
+
)
|
|
1198
|
+
standard_names = accessor.standard_names
|
|
1199
|
+
fast_path = all(k not in reserved and k not in standard_names for k in key_iter)
|
|
1200
|
+
|
|
1201
|
+
varnames: list[Hashable]
|
|
1202
|
+
coords: list[Hashable]
|
|
1203
|
+
if fast_path:
|
|
1204
|
+
varnames = list(key_iter)
|
|
1205
|
+
coords = []
|
|
1206
|
+
successful = dict.fromkeys(key_iter, True)
|
|
1207
|
+
else:
|
|
1208
|
+
varnames = []
|
|
1209
|
+
coords = []
|
|
1210
|
+
successful = dict.fromkeys(key_iter, False)
|
|
1211
|
+
for k in key_iter:
|
|
1212
|
+
if "coords" not in skip and k in _AXIS_NAMES + _COORD_NAMES:
|
|
1213
|
+
names = _get_all(obj, k)
|
|
1214
|
+
names = drop_bounds(names)
|
|
1215
|
+
check_results(names, k)
|
|
1216
|
+
successful[k] = bool(names)
|
|
1217
|
+
coords.extend(names)
|
|
1218
|
+
elif "measures" not in skip and k in measures:
|
|
1219
|
+
measure = _get_all(obj, k)
|
|
1220
|
+
check_results(measure, k)
|
|
1221
|
+
successful[k] = bool(measure)
|
|
1222
|
+
if measure:
|
|
1223
|
+
varnames.extend(measure)
|
|
1224
|
+
elif "grid_mapping_names" not in skip and k in grid_mapping_names:
|
|
1225
|
+
grid_mapping = _get_all(obj, k)
|
|
1226
|
+
check_results(grid_mapping, k)
|
|
1227
|
+
successful[k] = bool(grid_mapping)
|
|
1228
|
+
if grid_mapping:
|
|
1229
|
+
varnames.extend(grid_mapping)
|
|
1230
|
+
elif "geometries" not in skip and (k == "geometry" or k in _GEOMETRY_TYPES):
|
|
1231
|
+
geometries = _get_all(obj, k)
|
|
1232
|
+
if geometries and k in _GEOMETRY_TYPES:
|
|
1233
|
+
new = itertools.chain(
|
|
1234
|
+
_parse_related_geometry_vars(
|
|
1235
|
+
ChainMap(obj[g].attrs, obj[g].encoding)
|
|
1236
|
+
)
|
|
1237
|
+
for g in geometries
|
|
1158
1238
|
)
|
|
1159
|
-
|
|
1160
|
-
)
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
varnames.extend(stdnames - objcoords)
|
|
1183
|
-
coords.extend(stdnames & objcoords)
|
|
1239
|
+
geometries.extend(*new)
|
|
1240
|
+
if len(geometries) > 1 and scalar_key:
|
|
1241
|
+
raise ValueError(
|
|
1242
|
+
f"CF geometries must be represented by an Xarray Dataset. To request a Dataset in return please pass `[{k!r}]` instead."
|
|
1243
|
+
)
|
|
1244
|
+
successful[k] = bool(geometries)
|
|
1245
|
+
if geometries:
|
|
1246
|
+
varnames.extend(geometries)
|
|
1247
|
+
elif k in custom_criteria or k in cf_role_criteria:
|
|
1248
|
+
names = _get_all(obj, k)
|
|
1249
|
+
check_results(names, k)
|
|
1250
|
+
successful[k] = bool(names)
|
|
1251
|
+
varnames.extend(names)
|
|
1252
|
+
else:
|
|
1253
|
+
stdnames = set(_get_with_standard_name(obj, k))
|
|
1254
|
+
objcoords = set(obj.coords)
|
|
1255
|
+
stdnames = drop_bounds(stdnames)
|
|
1256
|
+
if "coords" in skip:
|
|
1257
|
+
stdnames -= objcoords
|
|
1258
|
+
check_results(stdnames, k)
|
|
1259
|
+
successful[k] = bool(stdnames)
|
|
1260
|
+
varnames.extend(stdnames - objcoords)
|
|
1261
|
+
coords.extend(stdnames & objcoords)
|
|
1184
1262
|
|
|
1185
1263
|
# these are not special names but could be variable names in underlying object
|
|
1186
1264
|
# we allow this so that we can return variables with appropriate CF auxiliary variables
|
|
@@ -2029,20 +2107,17 @@ class CFAccessor:
|
|
|
2029
2107
|
"""
|
|
2030
2108
|
|
|
2031
2109
|
obj = self._obj
|
|
2110
|
+
if isinstance(obj, DataArray):
|
|
2111
|
+
variables = [*obj.coords.variables.values(), obj.variable]
|
|
2112
|
+
else:
|
|
2113
|
+
variables = list(obj.variables.values())
|
|
2032
2114
|
all_attrs = [
|
|
2033
|
-
ChainMap(
|
|
2034
|
-
for
|
|
2115
|
+
ChainMap(var.attrs, var.encoding).get("cell_measures", "")
|
|
2116
|
+
for var in variables
|
|
2035
2117
|
]
|
|
2036
|
-
if isinstance(obj, DataArray):
|
|
2037
|
-
all_attrs += [ChainMap(obj.attrs, obj.encoding).get("cell_measures", "")]
|
|
2038
|
-
elif isinstance(obj, Dataset):
|
|
2039
|
-
all_attrs += [
|
|
2040
|
-
ChainMap(da.attrs, da.encoding).get("cell_measures", "")
|
|
2041
|
-
for da in obj.data_vars.values()
|
|
2042
|
-
]
|
|
2043
2118
|
as_dataset = self._maybe_to_dataset().reset_coords()
|
|
2044
2119
|
|
|
2045
|
-
keys = {}
|
|
2120
|
+
keys: dict[str, str] = {}
|
|
2046
2121
|
for attr in set(all_attrs):
|
|
2047
2122
|
try:
|
|
2048
2123
|
keys.update(parse_cell_methods_attr(attr))
|
|
@@ -2222,6 +2297,8 @@ class CFAccessor:
|
|
|
2222
2297
|
|
|
2223
2298
|
if grid := attrs_or_encoding.get("grid", None):
|
|
2224
2299
|
coords["grid"] = [grid]
|
|
2300
|
+
if isinstance(self._obj, Dataset):
|
|
2301
|
+
coords["coordinates"].extend(sgrid.get_topology_coords(self._obj, grid))
|
|
2225
2302
|
|
|
2226
2303
|
if grid_mapping_attr := attrs_or_encoding.get("grid_mapping", None):
|
|
2227
2304
|
# Parse grid mapping variables and their coordinates
|
|
@@ -2835,14 +2912,38 @@ class CFDatasetAccessor(CFAccessor):
|
|
|
2835
2912
|
"""
|
|
2836
2913
|
|
|
2837
2914
|
obj = self._obj
|
|
2838
|
-
|
|
2915
|
+
variables = obj._variables
|
|
2916
|
+
|
|
2917
|
+
# Single linear scan to discover which variables in the dataset have
|
|
2918
|
+
# a ``bounds`` attribute pointing at another existing variable. The
|
|
2919
|
+
# naive implementation called ``apply_mapper(_get_bounds, ...)`` for
|
|
2920
|
+
# every key in ``self.keys() | set(variables)``, which itself fans
|
|
2921
|
+
# out through ``_get_all`` and runs the regex criteria against every
|
|
2922
|
+
# variable's attrs. That is O(num_keys × num_vars × num_criteria)
|
|
2923
|
+
# for an answer that is structurally a single attribute lookup.
|
|
2924
|
+
var_to_bounds: dict[Hashable, Hashable] = {}
|
|
2925
|
+
for name, var in variables.items():
|
|
2926
|
+
attrs_or_encoding = ChainMap(var.attrs, var.encoding)
|
|
2927
|
+
bounds_name = attrs_or_encoding.get("bounds")
|
|
2928
|
+
if bounds_name is not None and bounds_name in variables:
|
|
2929
|
+
var_to_bounds[name] = bounds_name
|
|
2930
|
+
|
|
2931
|
+
if not var_to_bounds:
|
|
2932
|
+
return {}
|
|
2839
2933
|
|
|
2840
|
-
vardict = {
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2934
|
+
vardict: dict[Hashable, set[Hashable]] = {}
|
|
2935
|
+
# Direct variable-name keys: O(1) lookup, no mapper machinery.
|
|
2936
|
+
for name, bounds_name in var_to_bounds.items():
|
|
2937
|
+
vardict[name] = {bounds_name}
|
|
2938
|
+
|
|
2939
|
+
# CF keys (axes, coordinate names, custom criteria, ...): resolve to
|
|
2940
|
+
# their target variable names once, then read the precomputed
|
|
2941
|
+
# ``var_to_bounds`` map. ``_get_bounds`` is no longer invoked.
|
|
2942
|
+
for key in self.keys() - set(variables):
|
|
2943
|
+
target_vars = apply_mapper(_get_all, obj, key, error=False, default=[])
|
|
2944
|
+
bounds_vars = {var_to_bounds[v] for v in target_vars if v in var_to_bounds}
|
|
2945
|
+
if bounds_vars:
|
|
2946
|
+
vardict[key] = bounds_vars
|
|
2846
2947
|
|
|
2847
2948
|
return {k: sort_maybe_hashable(v) for k, v in vardict.items() if v}
|
|
2848
2949
|
|
|
@@ -11,6 +11,31 @@ SGRID_DIM_ATTRS = [
|
|
|
11
11
|
# "edge3_dimensions",
|
|
12
12
|
]
|
|
13
13
|
|
|
14
|
+
SGRID_COORD_ATTRS = [
|
|
15
|
+
"node_coordinates",
|
|
16
|
+
"face_coordinates",
|
|
17
|
+
"edge1_coordinates",
|
|
18
|
+
"edge2_coordinates",
|
|
19
|
+
"volume_coordinates",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_topology_coords(ds, grid_var_name):
|
|
24
|
+
"""Return coordinate variable names referenced by an SGRID topology variable.
|
|
25
|
+
|
|
26
|
+
Reads ``node_coordinates``, ``face_coordinates``, ``edge{1,2}_coordinates``,
|
|
27
|
+
and ``volume_coordinates`` from the topology variable's attrs and filters
|
|
28
|
+
to names that are actually present in ``ds``.
|
|
29
|
+
"""
|
|
30
|
+
if grid_var_name not in ds.variables:
|
|
31
|
+
return []
|
|
32
|
+
grid_attrs = ds[grid_var_name].attrs
|
|
33
|
+
names: list[str] = []
|
|
34
|
+
for attr_name in SGRID_COORD_ATTRS:
|
|
35
|
+
if coord_str := grid_attrs.get(attr_name):
|
|
36
|
+
names.extend(n for n in coord_str.split() if n in ds.variables)
|
|
37
|
+
return names
|
|
38
|
+
|
|
14
39
|
|
|
15
40
|
def parse_axes(ds):
|
|
16
41
|
import re
|
|
@@ -1254,6 +1254,44 @@ def test_grid_mappings_property():
|
|
|
1254
1254
|
assert gm.coordinates == ("x", "y")
|
|
1255
1255
|
|
|
1256
1256
|
|
|
1257
|
+
@requires_pyproj
|
|
1258
|
+
def test_grid_mappings_crs_construction_is_cached(monkeypatch):
|
|
1259
|
+
"""``pyproj.CRS.from_cf`` is memoized per grid-mapping attrs.
|
|
1260
|
+
|
|
1261
|
+
Building the CRS re-parses the datum/ellipsoid on every call. A dataset
|
|
1262
|
+
references the same grid mapping from many variables and the property may
|
|
1263
|
+
be accessed repeatedly, so each distinct grid mapping should be built once.
|
|
1264
|
+
"""
|
|
1265
|
+
import pyproj
|
|
1266
|
+
|
|
1267
|
+
from ..accessor import _crs_from_cf_attrs
|
|
1268
|
+
|
|
1269
|
+
_crs_from_cf_attrs.cache_clear()
|
|
1270
|
+
|
|
1271
|
+
from ..datasets import hrrrds
|
|
1272
|
+
|
|
1273
|
+
ds = hrrrds
|
|
1274
|
+
|
|
1275
|
+
calls = {"n": 0}
|
|
1276
|
+
orig = pyproj.CRS.from_cf
|
|
1277
|
+
|
|
1278
|
+
def counting_from_cf(*args, **kwargs):
|
|
1279
|
+
calls["n"] += 1
|
|
1280
|
+
return orig(*args, **kwargs)
|
|
1281
|
+
|
|
1282
|
+
monkeypatch.setattr(pyproj.CRS, "from_cf", staticmethod(counting_from_cf))
|
|
1283
|
+
|
|
1284
|
+
# Repeated property accesses, including via a DataArray, must not rebuild
|
|
1285
|
+
# the same CRS: hrrrds has 3 distinct grid mappings, each built once.
|
|
1286
|
+
ds.cf.grid_mappings
|
|
1287
|
+
ds.cf.grid_mappings
|
|
1288
|
+
ds.foo.cf.grid_mappings
|
|
1289
|
+
|
|
1290
|
+
assert calls["n"] == 3
|
|
1291
|
+
|
|
1292
|
+
_crs_from_cf_attrs.cache_clear()
|
|
1293
|
+
|
|
1294
|
+
|
|
1257
1295
|
@requires_pyproj
|
|
1258
1296
|
def test_grid_mappings_coordinates_attribute():
|
|
1259
1297
|
"""Test that coordinates attribute is always populated correctly for DataArray grid mappings."""
|
|
@@ -2484,6 +2522,43 @@ def test_sgrid():
|
|
|
2484
2522
|
}
|
|
2485
2523
|
|
|
2486
2524
|
|
|
2525
|
+
def test_sgrid_includes_topology_coordinates():
|
|
2526
|
+
"""Variables referenced in node/face/edge/volume_coordinates of the
|
|
2527
|
+
grid_topology variable should be pulled in by ds.cf[[var]]."""
|
|
2528
|
+
roms = sgrid_roms.copy()
|
|
2529
|
+
for pos in ("psi", "rho", "u", "v"):
|
|
2530
|
+
roms[f"lon_{pos}"] = ((f"xi_{pos}", f"eta_{pos}"), np.zeros((2, 2)))
|
|
2531
|
+
roms[f"lat_{pos}"] = ((f"xi_{pos}", f"eta_{pos}"), np.zeros((2, 2)))
|
|
2532
|
+
|
|
2533
|
+
expected_coord_vars = {
|
|
2534
|
+
"lon_psi",
|
|
2535
|
+
"lat_psi",
|
|
2536
|
+
"lon_rho",
|
|
2537
|
+
"lat_rho",
|
|
2538
|
+
"lon_u",
|
|
2539
|
+
"lat_u",
|
|
2540
|
+
"lon_v",
|
|
2541
|
+
"lat_v",
|
|
2542
|
+
}
|
|
2543
|
+
assoc = roms.cf.get_associated_variable_names("u")
|
|
2544
|
+
assert expected_coord_vars.issubset(set(assoc["coordinates"]))
|
|
2545
|
+
|
|
2546
|
+
subset = roms.cf[["u"]]
|
|
2547
|
+
assert "grid" in subset.variables
|
|
2548
|
+
assert expected_coord_vars.issubset(set(subset.variables))
|
|
2549
|
+
|
|
2550
|
+
# only dim-compatible coords attach to the DataArray form
|
|
2551
|
+
u_da = roms.cf["u"]
|
|
2552
|
+
assert {"lon_u", "lat_u"}.issubset(set(u_da.coords))
|
|
2553
|
+
|
|
2554
|
+
delft = sgrid_delft.copy()
|
|
2555
|
+
delft["node_lon"] = (("inode", "jnode"), np.zeros((2, 2)))
|
|
2556
|
+
delft["node_lat"] = (("inode", "jnode"), np.zeros((2, 2)))
|
|
2557
|
+
delft["foo"] = (("icell", "jcell"), np.ones((2, 2)), {"grid": "grid"})
|
|
2558
|
+
delft_subset = delft.cf[["foo"]]
|
|
2559
|
+
assert {"grid", "node_lon", "node_lat"}.issubset(set(delft_subset.variables))
|
|
2560
|
+
|
|
2561
|
+
|
|
2487
2562
|
def test_ancillary_variables_extra_dim():
|
|
2488
2563
|
ds = xr.Dataset(
|
|
2489
2564
|
{
|
|
@@ -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
|
|
|
@@ -61,6 +61,23 @@ only `xi_u`, `eta_u` are listed in the repr even though the attributes on the `g
|
|
|
61
61
|
variable `grid` list many more dimension names.
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
### Coordinate variables
|
|
65
|
+
|
|
66
|
+
`cf_xarray` also follows the `node_coordinates`, `face_coordinates`,
|
|
67
|
+
`edge1_coordinates`, `edge2_coordinates`, and `volume_coordinates` attributes
|
|
68
|
+
on the `grid_topology` variable. When you select a data variable that
|
|
69
|
+
references a `grid_topology` via its `grid` attribute, the referenced
|
|
70
|
+
coordinate variables are pulled in alongside it:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
ds.cf[["u"]] # includes `grid`, lon_psi/lat_psi, lon_rho/lat_rho, ...
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Only names actually present in the dataset are propagated. For the
|
|
77
|
+
`DataArray` form (`ds.cf["u"]`) xarray only attaches coordinates whose
|
|
78
|
+
dimensions are compatible with the variable, so e.g. only `lon_u`/`lat_u`
|
|
79
|
+
appear as coords on `u`.
|
|
80
|
+
|
|
64
81
|
## UGRID
|
|
65
82
|
|
|
66
83
|
### Topology variable
|
|
@@ -2983,11 +2983,11 @@ wheels = [
|
|
|
2983
2983
|
|
|
2984
2984
|
[[package]]
|
|
2985
2985
|
name = "urllib3"
|
|
2986
|
-
version = "2.
|
|
2986
|
+
version = "2.7.0"
|
|
2987
2987
|
source = { registry = "https://pypi.org/simple" }
|
|
2988
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
2988
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
|
2989
2989
|
wheels = [
|
|
2990
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
2990
|
+
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
|
2991
2991
|
]
|
|
2992
2992
|
|
|
2993
2993
|
[[package]]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.11.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|