cf-xarray 0.11.1__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.
Files changed (92) hide show
  1. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/PKG-INFO +1 -1
  2. cf_xarray-0.11.2/cf_xarray/_version.py +1 -0
  3. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/accessor.py +40 -10
  4. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/sgrid.py +25 -0
  5. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_accessor.py +75 -0
  6. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray.egg-info/PKG-INFO +1 -1
  7. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/sgrid_ugrid.md +17 -0
  8. cf_xarray-0.11.1/cf_xarray/_version.py +0 -1
  9. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.binder/environment.yml +0 -0
  10. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.deepsource.toml +0 -0
  11. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/dependabot.yml +0 -0
  12. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/release.yml +0 -0
  13. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/workflows/ci.yaml +0 -0
  14. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/workflows/parse_logs.py +0 -0
  15. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/workflows/pypi.yaml +0 -0
  16. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/workflows/testpypi-release.yaml +0 -0
  17. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.github/workflows/upstream-dev-ci.yaml +0 -0
  18. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.gitignore +0 -0
  19. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.pre-commit-config.yaml +0 -0
  20. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.readthedocs.yml +0 -0
  21. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/.tributors +0 -0
  22. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/CITATION.cff +0 -0
  23. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/LICENSE +0 -0
  24. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/README.rst +0 -0
  25. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/__init__.py +0 -0
  26. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/coding.py +0 -0
  27. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/criteria.py +0 -0
  28. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/datasets.py +0 -0
  29. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/formatting.py +0 -0
  30. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/geometry.py +0 -0
  31. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/groupers.py +0 -0
  32. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/helpers.py +0 -0
  33. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/options.py +0 -0
  34. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/parametric.py +0 -0
  35. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/py.typed +0 -0
  36. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/scripts/make_doc.py +0 -0
  37. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/scripts/print_versions.py +0 -0
  38. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/__init__.py +0 -0
  39. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/conftest.py +0 -0
  40. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_coding.py +0 -0
  41. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_geometry.py +0 -0
  42. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_groupers.py +0 -0
  43. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_helpers.py +0 -0
  44. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_options.py +0 -0
  45. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_parametric.py +0 -0
  46. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_scripts.py +0 -0
  47. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/tests/test_units.py +0 -0
  48. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/units.py +0 -0
  49. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray/utils.py +0 -0
  50. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray.egg-info/SOURCES.txt +0 -0
  51. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray.egg-info/dependency_links.txt +0 -0
  52. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray.egg-info/requires.txt +0 -0
  53. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/cf_xarray.egg-info/top_level.txt +0 -0
  54. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/codecov.yml +0 -0
  55. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/2D_bounds_averaged.png +0 -0
  56. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/2D_bounds_error.png +0 -0
  57. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/2D_bounds_nonunique.png +0 -0
  58. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/Makefile +0 -0
  59. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/_static/dataset-diagram-logo.tex +0 -0
  60. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/_static/full-logo.png +0 -0
  61. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/_static/logo.png +0 -0
  62. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/_static/logo.svg +0 -0
  63. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/_static/rich-repr-example.png +0 -0
  64. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/_static/style.css +0 -0
  65. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/api.rst +0 -0
  66. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/bounds.md +0 -0
  67. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/cartopy_rotated_pole.png +0 -0
  68. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/coding.md +0 -0
  69. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/conf.py +0 -0
  70. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/contributing.rst +0 -0
  71. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/coord_axes.md +0 -0
  72. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/custom-criteria.md +0 -0
  73. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/dsg.md +0 -0
  74. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/examples/introduction.ipynb +0 -0
  75. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/faq.md +0 -0
  76. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/flags.md +0 -0
  77. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/geometry.md +0 -0
  78. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/grid_mappings.md +0 -0
  79. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/howtouse.md +0 -0
  80. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/index.rst +0 -0
  81. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/make.bat +0 -0
  82. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/parametricz.md +0 -0
  83. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/plotting.md +0 -0
  84. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/provenance.md +0 -0
  85. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/quickstart.md +0 -0
  86. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/roadmap.rst +0 -0
  87. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/selecting.md +0 -0
  88. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/units.md +0 -0
  89. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/doc/whats-new.rst +0 -0
  90. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/pyproject.toml +0 -0
  91. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/setup.cfg +0 -0
  92. {cf_xarray-0.11.1 → cf_xarray-0.11.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.11.1
3
+ Version: 0.11.2
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -0,0 +1 @@
1
+ __version__ = "0.11.2"
@@ -556,6 +556,37 @@ def _parse_grid_mapping_attribute(
556
556
  return Frozen(result)
557
557
 
558
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))
588
+
589
+
559
590
  def _create_grid_mapping(
560
591
  var_name: str,
561
592
  ds: Dataset,
@@ -669,7 +700,7 @@ def _create_grid_mapping(
669
700
  }
670
701
  )
671
702
  else:
672
- crs = pyproj.CRS.from_cf(var.attrs)
703
+ crs = _crs_from_cf_attrs(_hashable_attrs(var.attrs))
673
704
 
674
705
  # Get associated coordinate variables, fallback to dimension names
675
706
  coordinates: list[Hashable] = grid_mapping_dict.get(var_name, [])
@@ -2076,17 +2107,14 @@ class CFAccessor:
2076
2107
  """
2077
2108
 
2078
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())
2079
2114
  all_attrs = [
2080
- ChainMap(da.attrs, da.encoding).get("cell_measures", "")
2081
- for da in obj.coords.values()
2115
+ ChainMap(var.attrs, var.encoding).get("cell_measures", "")
2116
+ for var in variables
2082
2117
  ]
2083
- if isinstance(obj, DataArray):
2084
- all_attrs += [ChainMap(obj.attrs, obj.encoding).get("cell_measures", "")]
2085
- elif isinstance(obj, Dataset):
2086
- all_attrs += [
2087
- ChainMap(da.attrs, da.encoding).get("cell_measures", "")
2088
- for da in obj.data_vars.values()
2089
- ]
2090
2118
  as_dataset = self._maybe_to_dataset().reset_coords()
2091
2119
 
2092
2120
  keys: dict[str, str] = {}
@@ -2269,6 +2297,8 @@ class CFAccessor:
2269
2297
 
2270
2298
  if grid := attrs_or_encoding.get("grid", None):
2271
2299
  coords["grid"] = [grid]
2300
+ if isinstance(self._obj, Dataset):
2301
+ coords["coordinates"].extend(sgrid.get_topology_coords(self._obj, grid))
2272
2302
 
2273
2303
  if grid_mapping_attr := attrs_or_encoding.get("grid_mapping", None):
2274
2304
  # Parse grid mapping variables and their coordinates
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cf_xarray
3
- Version: 0.11.1
3
+ Version: 0.11.2
4
4
  Summary: A convenience wrapper for using CF attributes on xarray objects
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -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
@@ -1 +0,0 @@
1
- __version__ = "0.11.1"
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