Skip to content

Commit 499a753

Browse files
authored
Increased support for cf_role (#327)
1. Add cf_roles attribute 2. Add to .cf.keys() 3. Add to repr Closes #305
1 parent d7838c4 commit 499a753

File tree

6 files changed

+104
-6
lines changed

6 files changed

+104
-6
lines changed

cf_xarray/accessor.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,7 @@ def __repr__(self):
12391239
coords = self._obj.coords
12401240
dims = self._obj.dims
12411241

1242-
def make_text_section(subtitle, attr, valid_values, default_keys=None):
1242+
def make_text_section(subtitle, attr, valid_values=None, default_keys=None):
12431243

12441244
with warnings.catch_warnings():
12451245
warnings.simplefilter("ignore")
@@ -1260,11 +1260,12 @@ def make_text_section(subtitle, attr, valid_values, default_keys=None):
12601260
vardict = {key: vardict[key] for key in ordered_keys if key in vardict}
12611261

12621262
# Keep only valid values (e.g., coords or data_vars)
1263-
vardict = {
1264-
key: set(value).intersection(valid_values)
1265-
for key, value in vardict.items()
1266-
if set(value).intersection(valid_values)
1267-
}
1263+
if valid_values is not None:
1264+
vardict = {
1265+
key: set(value).intersection(valid_values)
1266+
for key, value in vardict.items()
1267+
if set(value).intersection(valid_values)
1268+
}
12681269

12691270
# Star for keys with dims only, tab otherwise
12701271
rows = [
@@ -1293,6 +1294,11 @@ def make_text_section(subtitle, attr, valid_values, default_keys=None):
12931294
text = f"CF Flag variable with mapping:\n\t{flag_dict!r}\n\n"
12941295
else:
12951296
text = ""
1297+
1298+
if self.cf_roles:
1299+
text += make_text_section("CF Roles", "cf_roles")
1300+
text += "\n"
1301+
12961302
text += "Coordinates:"
12971303
text += make_text_section("CF Axes", "axes", coords, _AXIS_NAMES)
12981304
text += make_text_section("CF Coordinates", "coordinates", coords, _COORD_NAMES)
@@ -1337,6 +1343,7 @@ def keys(self) -> set[str]:
13371343
varnames = list(self.axes) + list(self.coordinates)
13381344
varnames.extend(list(self.cell_measures))
13391345
varnames.extend(list(self.standard_names))
1346+
varnames.extend(list(self.cf_roles))
13401347

13411348
return set(varnames)
13421349

@@ -1461,6 +1468,33 @@ def standard_names(self) -> dict[str, list[str]]:
14611468

14621469
return {k: sorted(v) for k, v in vardict.items()}
14631470

1471+
@property
1472+
def cf_roles(self) -> dict[str, list[str]]:
1473+
"""
1474+
Returns a dictionary mapping cf_role names to variable names.
1475+
1476+
Returns
1477+
-------
1478+
dict
1479+
Dictionary mapping cf_role names to variable names.
1480+
1481+
References
1482+
----------
1483+
Please refer to the CF conventions document : http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#coordinates-metadata
1484+
"""
1485+
if isinstance(self._obj, Dataset):
1486+
variables = self._obj.variables
1487+
elif isinstance(self._obj, DataArray):
1488+
variables = self._obj.coords
1489+
1490+
vardict: dict[str, list[str]] = {}
1491+
for k, v in variables.items():
1492+
if "cf_role" in v.attrs:
1493+
role = v.attrs["cf_role"]
1494+
vardict[role] = vardict.setdefault(role, []) + [k]
1495+
1496+
return {k: sorted(v) for k, v in vardict.items()}
1497+
14641498
def get_associated_variable_names(
14651499
self, name: Hashable, skip_bounds: bool = False, error: bool = True
14661500
) -> dict[str, list[str]]:

cf_xarray/datasets.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,3 +483,12 @@
483483
},
484484
}
485485
)
486+
487+
488+
dsg = xr.Dataset(
489+
{"foo": (("trajectory", "profile"), [[1, 2, 3], [1, 2, 3]])},
490+
coords={
491+
"profile": ("profile", [0, 1, 2], {"cf_role": "profile_id"}),
492+
"trajectory": ("trajectory", [0, 1], {"cf_role": "trajectory_id"}),
493+
},
494+
)

cf_xarray/tests/test_accessor.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
anc,
2222
basin,
2323
ds_no_attrs,
24+
dsg,
2425
forecast,
2526
mollwds,
2627
multiple,
@@ -168,6 +169,32 @@ def test_repr():
168169
"""
169170
assert actual == dedent(expected)
170171

172+
# CF roles
173+
actual = dsg.cf.__repr__()
174+
expected = """
175+
- CF Roles: * profile_id: ['profile']
176+
* trajectory_id: ['trajectory']
177+
178+
Coordinates:
179+
- CF Axes: X, Y, Z, T: n/a
180+
181+
- CF Coordinates: longitude, latitude, vertical, time: n/a
182+
183+
- Cell Measures: area, volume: n/a
184+
185+
- Standard Names: n/a
186+
187+
- Bounds: n/a
188+
189+
Data Variables:
190+
- Cell Measures: area, volume: n/a
191+
192+
- Standard Names: n/a
193+
194+
- Bounds: n/a
195+
"""
196+
assert actual == dedent(expected)
197+
171198

172199
def test_axes():
173200
expected = dict(T=["time"], X=["lon"], Y=["lat"])
@@ -1579,3 +1606,16 @@ def test_pickle():
15791606
ds = da.to_dataset()
15801607
pickle.loads(pickle.dumps(da.cf))
15811608
pickle.loads(pickle.dumps(ds.cf))
1609+
1610+
1611+
def test_cf_role():
1612+
for name in ["profile_id", "trajectory_id"]:
1613+
assert name in dsg.cf.keys()
1614+
1615+
assert dsg.cf.cf_roles == {
1616+
"profile_id": ["profile"],
1617+
"trajectory_id": ["trajectory"],
1618+
}
1619+
1620+
dsg.foo.cf.plot(x="profile_id")
1621+
dsg.foo.cf.plot(x="trajectory_id")

doc/api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Attributes
3333

3434
DataArray.cf.axes
3535
DataArray.cf.cell_measures
36+
DataArray.cf.cf_roles
3637
DataArray.cf.coordinates
3738
DataArray.cf.formula_terms
3839
DataArray.cf.is_flag_variable
@@ -90,6 +91,7 @@ Attributes
9091
Dataset.cf.axes
9192
Dataset.cf.bounds
9293
Dataset.cf.cell_measures
94+
Dataset.cf.cf_roles
9395
Dataset.cf.coordinates
9496
Dataset.cf.formula_terms
9597
Dataset.cf.standard_names

doc/dsg.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,17 @@ ds = xr.Dataset(
3030
{"temp": ("x", np.arange(10))},
3131
coords={"cast": ("x", np.arange(10), {"cf_role": "profile_id"})}
3232
)
33+
ds.cf
34+
```
35+
36+
Access `"cast"` using it's `cf_role`
37+
38+
```{code-cell}
3339
ds.cf["profile_id"]
3440
```
41+
42+
Find all `cf_role` variables using {py:attr}`Dataset.cf.cf_roles` and {py:attr}`DataArray.cf.cf_roles`
43+
44+
```{code-cell}
45+
ds.cf.cf_roles
46+
```

doc/whats-new.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ What's New
55

66
v0.7.3 (unreleased)
77
===================
8+
- Increased support for ``cf_role`` variables. Added :py:attr:`Dataset.cf.cf_roles` By `Deepak Cherian`_.
89

910
v0.7.2 (April 5, 2022)
1011
======================

0 commit comments

Comments
 (0)