Skip to content

Commit 5356069

Browse files
add cf.grid_mapping_names (#391)
* added grid_mapping attribute * added get_grid_mapping_name function * updated rotds with grid_mapping * renamed dummy variable * updated grid mapping scalar * update repr tests and rotds dims * api docs update * updated examples in grid_mappings docstring * added references * remove zero dim from rotds * format fix * added basic grid mappings section in docs * update section * update docs section * added grid_mappgin to associated variables * added grid_mapping accessor tests * updated docs section * syntax fix * eval-rst * typo * fix eval-rst * polish * Update cf_xarray/accessor.py Co-authored-by: Deepak Cherian <[email protected]> * Update cf_xarray/accessor.py Co-authored-by: Deepak Cherian <[email protected]> * Update cf_xarray/accessor.py Co-authored-by: Deepak Cherian <[email protected]> * Update cf_xarray/accessor.py Co-authored-by: Deepak Cherian <[email protected]> * Update doc/grid_mappings.md Co-authored-by: Deepak Cherian <[email protected]> * updated see also sections * added warning tests * extract warning tests * remove skip_grid_mappings * added dataarray grid_mapping accessor * added dataarray grid_mapping_name * docstrings * docs updates * fix * skip grid_mapping coords repr * cf.grid_mappings switchted logic * update grid_mapping accessor logic * test updates * revert changes * fixed grid_mapping key critera * adapted rotds dim names * small cleanup * Add "grid_mapping" as special name * Swtich to .cf.grid_mapping_names * Avoid DataArray.cf.grid_mapping * Avoid misleading n/a grid mapping in DataArray repr * Fix repr * Fix typing * Update docs. * fix cartopy cell * fix typing * Add to quickstart too * docs udpates --------- Co-authored-by: Deepak Cherian <[email protected]> Co-authored-by: dcherian <[email protected]>
1 parent 0169831 commit 5356069

File tree

10 files changed

+467
-14
lines changed

10 files changed

+467
-14
lines changed

cf_xarray/accessor.py

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@
3030
from xarray.core.rolling import Coarsen, Rolling
3131
from xarray.core.weighted import Weighted
3232

33-
from .criteria import cf_role_criteria, coordinate_criteria, regex
33+
from .criteria import (
34+
cf_role_criteria,
35+
coordinate_criteria,
36+
grid_mapping_var_criteria,
37+
regex,
38+
)
3439
from .helpers import _guess_bounds_1d, _guess_bounds_2d, bounds_to_vertices
3540
from .options import OPTIONS
3641
from .utils import (
@@ -369,6 +374,41 @@ def _get_bounds(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
369374
return list(results)
370375

371376

377+
def _get_grid_mapping_name(obj: DataArray | Dataset, key: str) -> list[str]:
378+
"""
379+
Translate from grid mapping name attribute to appropriate variable name.
380+
This function interprets the ``grid_mapping`` attribute on DataArrays.
381+
382+
Parameters
383+
----------
384+
obj : DataArray, Dataset
385+
DataArray belonging to the coordinate to be checked
386+
key : str
387+
key to check for.
388+
389+
Returns
390+
-------
391+
List[str], Variable name(s) in parent xarray object that matches grid_mapping_name `key`
392+
"""
393+
394+
if isinstance(obj, DataArray):
395+
obj = obj._to_temp_dataset()
396+
397+
results = set()
398+
for var in obj.variables:
399+
da = obj[var]
400+
attrs_or_encoding = ChainMap(da.attrs, da.encoding)
401+
if "grid_mapping" in attrs_or_encoding:
402+
grid_mapping_var_name = attrs_or_encoding["grid_mapping"]
403+
if grid_mapping_var_name not in obj.variables:
404+
raise ValueError(
405+
f"{var} defines non-existing grid_mapping variable {grid_mapping_var_name}."
406+
)
407+
if key == obj[grid_mapping_var_name].attrs["grid_mapping_name"]:
408+
results.update([grid_mapping_var_name])
409+
return list(results)
410+
411+
372412
def _get_with_standard_name(
373413
obj: DataArray | Dataset, name: Hashable | Iterable[Hashable]
374414
) -> list[Hashable]:
@@ -395,8 +435,10 @@ def _get_all(obj: DataArray | Dataset, key: Hashable) -> list[Hashable]:
395435
all_mappers: tuple[Mapper] = (
396436
_get_custom_criteria,
397437
functools.partial(_get_custom_criteria, criteria=cf_role_criteria), # type: ignore
438+
functools.partial(_get_custom_criteria, criteria=grid_mapping_var_criteria),
398439
_get_axis_coord,
399440
_get_measure,
441+
_get_grid_mapping_name,
400442
_get_with_standard_name,
401443
)
402444
results = apply_mapper(all_mappers, obj, key, error=False, default=None)
@@ -706,6 +748,15 @@ def check_results(names, key):
706748
measures = []
707749
warnings.warn("Ignoring bad cell_measures attribute.", UserWarning)
708750

751+
if isinstance(obj, Dataset):
752+
grid_mapping_names = list(accessor.grid_mapping_names)
753+
else:
754+
try:
755+
grid_mapping_names = [accessor.grid_mapping_name]
756+
except ValueError:
757+
grid_mapping_names = []
758+
grid_mapping_names.append("grid_mapping")
759+
709760
custom_criteria = ChainMap(*OPTIONS["custom_criteria"])
710761

711762
varnames: list[Hashable] = []
@@ -724,6 +775,12 @@ def check_results(names, key):
724775
successful[k] = bool(measure)
725776
if measure:
726777
varnames.extend(measure)
778+
elif "grid_mapping_names" not in skip and k in grid_mapping_names:
779+
grid_mapping = _get_all(obj, k)
780+
check_results(grid_mapping, k)
781+
successful[k] = bool(grid_mapping)
782+
if grid_mapping:
783+
varnames.extend(grid_mapping)
727784
elif k in custom_criteria or k in cf_role_criteria:
728785
names = _get_all(obj, k)
729786
check_results(names, k)
@@ -1415,13 +1472,15 @@ def make_text_section(subtitle, attr, valid_values=None, default_keys=None):
14151472
text += make_text_section("Standard Names", "standard_names", coords)
14161473
text += make_text_section("Bounds", "bounds", coords)
14171474
if isinstance(self._obj, Dataset):
1475+
text += make_text_section("Grid Mappings", "grid_mapping_names", coords)
14181476
data_vars = self._obj.data_vars
14191477
text += "\nData Variables:"
14201478
text += make_text_section(
14211479
"Cell Measures", "cell_measures", data_vars, _CELL_MEASURES
14221480
)
14231481
text += make_text_section("Standard Names", "standard_names", data_vars)
14241482
text += make_text_section("Bounds", "bounds", data_vars)
1483+
text += make_text_section("Grid Mappings", "grid_mapping_names", data_vars)
14251484

14261485
return text
14271486

@@ -1442,6 +1501,14 @@ def keys(self) -> set[Hashable]:
14421501
varnames.extend(list(self.cell_measures))
14431502
varnames.extend(list(self.standard_names))
14441503
varnames.extend(list(self.cf_roles))
1504+
if isinstance(self._obj, xr.Dataset):
1505+
varnames.extend(list(self.grid_mapping_names))
1506+
else:
1507+
try:
1508+
gmname = self.grid_mapping_name
1509+
varnames.extend(list(gmname))
1510+
except ValueError:
1511+
pass
14451512

14461513
return set(varnames)
14471514

@@ -1604,6 +1671,7 @@ def get_associated_variable_names(
16041671
2. "bounds"
16051672
3. "cell_measures"
16061673
4. "coordinates"
1674+
5. "grid_mapping"
16071675
to a list of variable names referred to in the appropriate attribute
16081676
16091677
Parameters
@@ -1618,7 +1686,13 @@ def get_associated_variable_names(
16181686
names : dict
16191687
Dictionary with keys "ancillary_variables", "cell_measures", "coordinates", "bounds".
16201688
"""
1621-
keys = ["ancillary_variables", "cell_measures", "coordinates", "bounds"]
1689+
keys = [
1690+
"ancillary_variables",
1691+
"cell_measures",
1692+
"coordinates",
1693+
"bounds",
1694+
"grid_mapping",
1695+
]
16221696
coords: dict[str, list[Hashable]] = {k: [] for k in keys}
16231697
attrs_or_encoding = ChainMap(self._obj[name].attrs, self._obj[name].encoding)
16241698

@@ -1660,6 +1734,9 @@ def get_associated_variable_names(
16601734
if dbounds:
16611735
coords["bounds"].append(dbounds)
16621736

1737+
if "grid_mapping" in attrs_or_encoding:
1738+
coords["grid_mapping"] = [attrs_or_encoding["grid_mapping"]]
1739+
16631740
allvars = itertools.chain(*coords.values())
16641741
missing = set(allvars) - set(self._maybe_to_dataset()._variables)
16651742
if missing:
@@ -2048,6 +2125,8 @@ def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray | Dataset
20482125
- cell measures: "area", "volume", or other names present in the \
20492126
``cell_measures`` attribute
20502127
- standard names: names present in ``standard_name`` attribute
2128+
- cf roles: 'timeseries_id', 'profile_id', 'trajectory_id', 'mesh_topology', 'grid_topology'
2129+
- grid mappings: 'grid_mapping' or a grid_mapping_name like 'rotated_latitude_longitude'
20512130
20522131
Returns
20532132
-------
@@ -2372,6 +2451,51 @@ def bounds_to_vertices(
23722451
)
23732452
return obj
23742453

2454+
@property
2455+
def grid_mapping_names(self) -> dict[str, list[str]]:
2456+
"""
2457+
Property that returns a dictionary mapping the CF grid mapping name
2458+
to the variable name containing the grid mapping attributes.
2459+
2460+
Returns
2461+
-------
2462+
dict
2463+
Dictionary mapping the CF grid mapping name to the grid mapping variable name.
2464+
2465+
See Also
2466+
--------
2467+
DataArray.cf.grid_mapping
2468+
2469+
References
2470+
----------
2471+
Please refer to the CF conventions document : https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#grid-mappings-and-projections
2472+
2473+
For a list of valid grid_mapping names, refer to: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#appendix-grid-mappings
2474+
2475+
Examples
2476+
--------
2477+
>>> from cf_xarray.datasets import rotds
2478+
>>> rotds.cf.grid_mapping_names
2479+
{'rotated_latitude_longitude': ['rotated_pole']}
2480+
"""
2481+
2482+
obj = self._obj
2483+
keys = set(obj.variables)
2484+
2485+
vardict = {
2486+
key: obj.variables[key].attrs["grid_mapping_name"]
2487+
for key in keys
2488+
if "grid_mapping_name" in obj.variables[key].attrs
2489+
}
2490+
2491+
results = {}
2492+
for k, v in vardict.items():
2493+
if v not in results:
2494+
results[v] = [k]
2495+
else:
2496+
results[v].append(k)
2497+
return results
2498+
23752499
def decode_vertical_coords(self, *, outnames=None, prefix=None):
23762500
"""
23772501
Decode parameterized vertical coordinates in place.
@@ -2547,6 +2671,43 @@ def formula_terms(self) -> dict[str, str]:
25472671
terms[key] = value
25482672
return terms
25492673

2674+
@property
2675+
def grid_mapping_name(self) -> str:
2676+
"""
2677+
Get CF grid mapping name associated with this variable.
2678+
2679+
Parameters
2680+
----------
2681+
key : str
2682+
Name of variable whose grid_mapping name is desired.
2683+
2684+
Returns
2685+
-------
2686+
str
2687+
CF Name of the associated grid mapping.
2688+
2689+
See Also
2690+
--------
2691+
Dataset.cf.grid_mapping_names
2692+
2693+
Examples
2694+
--------
2695+
>>> from cf_xarray.datasets import rotds
2696+
>>> rotds.cf["temp"].cf.grid_mapping_name
2697+
'rotated_latitude_longitude'
2698+
2699+
"""
2700+
2701+
da = self._obj
2702+
2703+
attrs_or_encoding = ChainMap(da.attrs, da.encoding)
2704+
grid_mapping = attrs_or_encoding.get("grid_mapping", None)
2705+
if not grid_mapping:
2706+
raise ValueError("No 'grid_mapping' attribute present.")
2707+
2708+
grid_mapping_var = da[grid_mapping]
2709+
return grid_mapping_var.attrs["grid_mapping_name"]
2710+
25502711
def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray:
25512712
"""
25522713
Index into a DataArray making use of CF attributes.
@@ -2561,6 +2722,8 @@ def __getitem__(self, key: Hashable | Iterable[Hashable]) -> DataArray:
25612722
``cell_measures`` attribute
25622723
- standard names: names present in ``standard_name`` attribute of \
25632724
coordinate variables
2725+
- cf roles: 'timeseries_id', 'profile_id', 'trajectory_id', 'mesh_topology', 'grid_topology'
2726+
- grid mappings: 'grid_mapping' or a grid_mapping_name like 'rotated_latitude_longitude'
25642727
25652728
Returns
25662729
-------

cf_xarray/criteria.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
Copyright (c) 2017 MetPy Developers.
55
"""
66

7+
try:
8+
import regex as re
9+
except ImportError:
10+
import re # type: ignore
711

8-
import re
9-
from typing import Mapping, MutableMapping, Tuple
12+
from typing import Any, Mapping, MutableMapping, Tuple
1013

1114
cf_role_criteria: Mapping[str, Mapping[str, str]] = {
1215
k: {"cf_role": k}
@@ -22,6 +25,11 @@
2225
)
2326
}
2427

28+
# A grid mapping varibale is anything with a grid_mapping_name attribute
29+
grid_mapping_var_criteria: Mapping[str, Mapping[str, Any]] = {
30+
"grid_mapping": {"grid_mapping_name": re.compile(".")}
31+
}
32+
2533
coordinate_criteria: MutableMapping[str, MutableMapping[str, Tuple]] = {
2634
"latitude": {
2735
"standard_name": ("latitude",),

cf_xarray/datasets.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@ def _create_mollw_ds():
228228
def _create_inexact_bounds():
229229
# Dataset that creates rotated pole curvilinear coordinates with CF bounds in
230230
# counterclockwise order that have precision issues.
231+
# dataset created using: https://gist.github.com/larsbuntemeyer/105d83c1eb39b1462150d3fabca0b66b
232+
rlon = np.array([17.935, 18.045, 18.155])
233+
rlat = np.array([21.615, 21.725, 21.835])
234+
231235
lon = np.array(
232236
[
233237
[64.21746363939087, 64.42305921561967, 64.62774455060337],
@@ -296,23 +300,70 @@ def _create_inexact_bounds():
296300

297301
rotated = xr.Dataset(
298302
coords=dict(
303+
rlon=xr.DataArray(
304+
rlon,
305+
dims="rlon",
306+
attrs={
307+
"units": "degrees",
308+
"axis": "X",
309+
"standard_name": "grid_longitude",
310+
},
311+
),
312+
rlat=xr.DataArray(
313+
rlat,
314+
dims="rlat",
315+
attrs={
316+
"units": "degrees",
317+
"axis": "Y",
318+
"standard_name": "grid_latitude",
319+
},
320+
),
299321
lon=xr.DataArray(
300322
lon,
301-
dims=("x", "y"),
302-
attrs={"units": "degrees_east", "bounds": "lon_bounds"},
323+
dims=("rlon", "rlat"),
324+
attrs={
325+
"units": "degrees_east",
326+
"bounds": "lon_bounds",
327+
"standard_name": "longitude",
328+
},
303329
),
304330
lat=xr.DataArray(
305331
lat,
306-
dims=("x", "y"),
307-
attrs={"units": "degrees_north", "bounds": "lat_bounds"},
332+
dims=("rlon", "rlat"),
333+
attrs={
334+
"units": "degrees_north",
335+
"bounds": "lat_bounds",
336+
"standard_name": "latitude",
337+
},
308338
),
309339
),
310340
data_vars=dict(
311341
lon_bounds=xr.DataArray(
312-
lon_bounds, dims=("bounds", "x", "y"), attrs={"units": "degrees_east"}
342+
lon_bounds,
343+
dims=("bounds", "rlon", "rlat"),
344+
attrs={"units": "degrees_east"},
313345
),
314346
lat_bounds=xr.DataArray(
315-
lat_bounds, dims=("bounds", "x", "y"), attrs={"units": "degrees_north"}
347+
lat_bounds,
348+
dims=("bounds", "rlon", "rlat"),
349+
attrs={"units": "degrees_north"},
350+
),
351+
rotated_pole=xr.DataArray(
352+
np.zeros((), dtype=np.int32),
353+
dims=None,
354+
attrs={
355+
"grid_mapping_name": "rotated_latitude_longitude",
356+
"grid_north_pole_latitude": 39.25,
357+
"grid_north_pole_longitude": -162.0,
358+
},
359+
),
360+
temp=xr.DataArray(
361+
np.random.rand(3, 3),
362+
dims=("rlat", "rlon"),
363+
attrs={
364+
"standard_name": "air_temperature",
365+
"grid_mapping": "rotated_pole",
366+
},
316367
),
317368
),
318369
)

0 commit comments

Comments
 (0)