Skip to content

Commit 36b98fe

Browse files
authored
add bounds property (#214)
1 parent 64e8695 commit 36b98fe

File tree

4 files changed

+100
-17
lines changed

4 files changed

+100
-17
lines changed

cf_xarray/accessor.py

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,31 @@ def _get_measure(obj: Union[DataArray, Dataset], key: str) -> List[str]:
255255
return list(results)
256256

257257

258+
def _get_bounds(obj: Union[DataArray, Dataset], key: str) -> List[str]:
259+
"""
260+
Translate from key (either CF key or variable name) to its bounds' variable names.
261+
This function interprets the ``bounds`` attribute on DataArrays.
262+
263+
Parameters
264+
----------
265+
obj : DataArray, Dataset
266+
DataArray belonging to the coordinate to be checked
267+
key : str
268+
key to check for.
269+
270+
Returns
271+
-------
272+
List[str], Variable name(s) in parent xarray object that are bounds of `key`
273+
"""
274+
275+
results = set()
276+
for var in apply_mapper(_get_all, obj, key, error=False, default=[key]):
277+
if "bounds" in obj[var].attrs:
278+
results |= {obj[var].attrs["bounds"]}
279+
280+
return list(results)
281+
282+
258283
def _get_with_standard_name(
259284
obj: Union[DataArray, Dataset], name: Union[str, List[str]]
260285
) -> List[str]:
@@ -436,6 +461,14 @@ def _getattr(
436461
try:
437462
attribute: Union[Mapping, Callable] = getattr(obj, attr)
438463
except AttributeError:
464+
if getattr(
465+
CFDatasetAccessor if isinstance(obj, DataArray) else CFDataArrayAccessor,
466+
attr,
467+
None,
468+
):
469+
raise AttributeError(
470+
f"{obj.__class__.__name__+'.cf'!r} object has no attribute {attr!r}"
471+
)
439472
raise AttributeError(
440473
f"{attr!r} is not a valid attribute on the underlying xarray object."
441474
)
@@ -976,7 +1009,9 @@ def __repr__(self):
9761009
coords = self._obj.coords
9771010
dims = self._obj.dims
9781011

979-
def make_text_section(subtitle, vardict, valid_values, default_keys=None):
1012+
def make_text_section(subtitle, attr, valid_values, default_keys=None):
1013+
1014+
vardict = getattr(self, attr, {})
9801015

9811016
star = " * "
9821017
tab = len(star) * " "
@@ -1019,21 +1054,21 @@ def make_text_section(subtitle, vardict, valid_values, default_keys=None):
10191054
return "\n".join(rows) + "\n"
10201055

10211056
text = "Coordinates:"
1022-
text += make_text_section("CF Axes", self.axes, coords, _AXIS_NAMES)
1057+
text += make_text_section("CF Axes", "axes", coords, _AXIS_NAMES)
1058+
text += make_text_section("CF Coordinates", "coordinates", coords, _COORD_NAMES)
10231059
text += make_text_section(
1024-
"CF Coordinates", self.coordinates, coords, _COORD_NAMES
1060+
"Cell Measures", "cell_measures", coords, _CELL_MEASURES
10251061
)
1026-
text += make_text_section(
1027-
"Cell Measures", self.cell_measures, coords, _CELL_MEASURES
1028-
)
1029-
text += make_text_section("Standard Names", self.standard_names, coords)
1062+
text += make_text_section("Standard Names", "standard_names", coords)
1063+
text += make_text_section("Bounds", "bounds", coords)
10301064
if isinstance(self._obj, Dataset):
10311065
data_vars = self._obj.data_vars
10321066
text += "\nData Variables:"
10331067
text += make_text_section(
1034-
"Cell Measures", self.cell_measures, data_vars, _CELL_MEASURES
1068+
"Cell Measures", "cell_measures", data_vars, _CELL_MEASURES
10351069
)
1036-
text += make_text_section("Standard Names", self.standard_names, data_vars)
1070+
text += make_text_section("Standard Names", "standard_names", data_vars)
1071+
text += make_text_section("Bounds", "bounds", data_vars)
10371072

10381073
return text
10391074

@@ -1144,7 +1179,7 @@ def get_standard_names(self) -> List[str]:
11441179
@property
11451180
def standard_names(self) -> Dict[str, List[str]]:
11461181
"""
1147-
Returns a sorted list of standard names in Dataset.
1182+
Returns a dictionary mapping standard names to variable names.
11481183
11491184
Parameters
11501185
----------
@@ -1153,7 +1188,7 @@ def standard_names(self) -> Dict[str, List[str]]:
11531188
11541189
Returns
11551190
-------
1156-
Dictionary of standard names in dataset
1191+
Dictionary mapping standard names to variable names.
11571192
"""
11581193
if isinstance(self._obj, Dataset):
11591194
variables = self._obj.variables
@@ -1480,6 +1515,26 @@ def __getitem__(self, key: Union[str, List[str]]) -> Union[DataArray, Dataset]:
14801515
"""
14811516
return _getitem(self, key)
14821517

1518+
@property
1519+
def bounds(self) -> Dict[str, List[str]]:
1520+
"""
1521+
Property that returns a dictionary mapping valid keys
1522+
to the variable names of their bounds.
1523+
1524+
Returns
1525+
-------
1526+
Dictionary mapping valid keys to the variable names of their bounds.
1527+
"""
1528+
1529+
obj = self._obj
1530+
keys = self.keys() | set(obj.variables)
1531+
1532+
vardict = {
1533+
key: apply_mapper(_get_bounds, obj, key, error=False) for key in keys
1534+
}
1535+
1536+
return {k: sorted(v) for k, v in vardict.items() if v}
1537+
14831538
def get_bounds(self, key: str) -> DataArray:
14841539
"""
14851540
Get bounds variable corresponding to key.
@@ -1493,12 +1548,8 @@ def get_bounds(self, key: str) -> DataArray:
14931548
-------
14941549
DataArray
14951550
"""
1496-
name = apply_mapper(
1497-
_single(_get_all), self._obj, key, error=False, default=[key]
1498-
)[0]
1499-
bounds = self._obj[name].attrs["bounds"]
1500-
obj = self._maybe_to_dataset()
1501-
return obj[bounds]
1551+
1552+
return apply_mapper(_variables(_single(_get_bounds)), self._obj, key)[0]
15021553

15031554
def get_bounds_dim_name(self, key: str) -> str:
15041555
"""

cf_xarray/tests/test_accessor.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,14 @@ def test_repr():
6161
* longitude: ['lon']
6262
* time: ['time']
6363
64+
- Bounds: n/a
65+
6466
Data Variables:
6567
- Cell Measures: area, volume: n/a
6668
6769
- Standard Names: air_temperature: ['air']
70+
71+
- Bounds: n/a
6872
"""
6973
assert actual == dedent(expected)
7074

@@ -89,6 +93,8 @@ def test_repr():
8993
- Standard Names: * latitude: ['lat']
9094
* longitude: ['lon']
9195
* time: ['time']
96+
97+
- Bounds: n/a
9298
"""
9399
assert actual == dedent(expected)
94100

@@ -108,11 +114,15 @@ def test_repr():
108114
109115
- Standard Names: n/a
110116
117+
- Bounds: n/a
118+
111119
Data Variables:
112120
- Cell Measures: area, volume: n/a
113121
114122
- Standard Names: sea_water_potential_temperature: ['TEMP']
115123
sea_water_x_velocity: ['UVEL']
124+
125+
- Bounds: n/a
116126
"""
117127
assert actual == dedent(expected)
118128

@@ -163,6 +173,8 @@ def test_cell_measures():
163173
164174
- Standard Names: air_temperature: ['air']
165175
foo_std_name: ['foo']
176+
177+
- Bounds: n/a
166178
"""
167179
assert actual.endswith(dedent(expected))
168180

@@ -625,6 +637,11 @@ def test_add_bounds(obj, dims):
625637

626638
def test_bounds():
627639
ds = airds.copy(deep=True).cf.add_bounds("lat")
640+
641+
actual = ds.cf.bounds
642+
expected = {"Y": ["lat_bounds"], "lat": ["lat_bounds"], "latitude": ["lat_bounds"]}
643+
assert ds.cf.bounds == expected
644+
628645
actual = ds.cf[["lat"]]
629646
expected = ds[["lat", "lat_bounds"]]
630647
assert_identical(actual, expected)
@@ -651,6 +668,19 @@ def test_bounds():
651668
with pytest.warns(UserWarning, match="{'foo'} not found in object"):
652669
ds.cf[["air"]]
653670

671+
# Dataset has bounds
672+
expected = """\
673+
- Bounds: Y: ['lat_bounds']
674+
lat: ['lat_bounds']
675+
latitude: ['lat_bounds']
676+
"""
677+
assert dedent(expected) in ds.cf.__repr__()
678+
679+
# DataArray does not have bounds
680+
expected = airds.cf["air"].cf.__repr__()
681+
actual = ds.cf["air"].cf.__repr__()
682+
assert actual == expected
683+
654684

655685
def test_bounds_to_vertices():
656686
# All available

doc/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Attributes
6363
:template: autosummary/accessor_attribute.rst
6464

6565
Dataset.cf.axes
66+
Dataset.cf.bounds
6667
Dataset.cf.cell_measures
6768
Dataset.cf.coordinates
6869
Dataset.cf.standard_names

doc/whats-new.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ What's New
66
v0.5.2 (unreleased)
77
===================
88

9+
- Added :py:attr:`Dataset.cf.axes` to return a dictionary mapping valid keys to the variable names of their bounds. By `Mattia Almansi`_.
910
- :py:meth:`DataArray.cf.differentiate` and :py:meth:`Dataset.cf.differentiate` can optionally correct
1011
sign of the derivative by interpreting the ``"positive"`` attribute. By `Deepak Cherian`_.
1112

0 commit comments

Comments
 (0)