Skip to content

Commit 023af99

Browse files
authored
Merge pull request #118 from xarray-contrib/extra_cell_measures
new cell measures implementation
2 parents d2dedfa + 36bc9ac commit 023af99

File tree

4 files changed

+57
-32
lines changed

4 files changed

+57
-32
lines changed

cf_xarray/accessor.py

+32-19
Original file line numberDiff line numberDiff line change
@@ -338,27 +338,21 @@ def _get_measure_variable(
338338

339339
def _get_measure(obj: Union[DataArray, Dataset], key: str) -> List[str]:
340340
"""
341-
Translate from cell measures ("area" or "volume") to appropriate variable name.
341+
Translate from cell measures to appropriate variable name.
342342
This function interprets the ``cell_measures`` attribute on DataArrays.
343343
344344
Parameters
345345
----------
346346
obj: DataArray, Dataset
347347
DataArray belonging to the coordinate to be checked
348-
key: str, ["area", "volume"]
348+
key: str
349349
key to check for.
350350
351351
Returns
352352
-------
353353
List[str], Variable name(s) in parent xarray object that matches axis or coordinate `key`
354354
"""
355355

356-
valid_keys = _CELL_MEASURES
357-
if key not in valid_keys:
358-
raise KeyError(
359-
f"cf_xarray did not understand key {key!r}. Expected one of {valid_keys!r}"
360-
)
361-
362356
if isinstance(obj, DataArray):
363357
obj = obj._to_temp_dataset()
364358

@@ -438,7 +432,7 @@ def _build_docstring(func):
438432
mapper_docstrings = {
439433
_get_axis_coord: f"One or more of {(_AXIS_NAMES + _COORD_NAMES)!r}",
440434
_get_axis_coord_single: f"One of {(_AXIS_NAMES + _COORD_NAMES)!r}",
441-
_get_measure_variable: f"One of {_CELL_MEASURES!r}",
435+
# _get_measure_variable: f"One of {_CELL_MEASURES!r}",
442436
}
443437

444438
sig = inspect.signature(func)
@@ -653,6 +647,18 @@ class CFAccessor:
653647

654648
def __init__(self, da):
655649
self._obj = da
650+
self._all_cell_measures = None
651+
652+
def _get_all_cell_measures(self):
653+
"""
654+
Get all cell measures defined in the object, adding CF pre-defined measures.
655+
"""
656+
657+
# get all_cell_measures only once
658+
if not self._all_cell_measures:
659+
self._all_cell_measures = set(_CELL_MEASURES + tuple(self.cell_measures))
660+
661+
return self._all_cell_measures
656662

657663
def _process_signature(
658664
self,
@@ -833,7 +839,7 @@ def describe(self):
833839

834840
text += "\nCell Measures:\n"
835841
measures = self.cell_measures
836-
for key in _CELL_MEASURES:
842+
for key in sorted(self._get_all_cell_measures()):
837843
text += f"\t{key}: {measures[key] if key in measures else []}\n"
838844

839845
text += "\nStandard Names:\n"
@@ -868,8 +874,7 @@ def keys(self) -> Set[str]:
868874
"""
869875

870876
varnames = list(self.axes) + list(self.coordinates)
871-
if not isinstance(self._obj, Dataset):
872-
varnames.extend(list(self.cell_measures))
877+
varnames.extend(list(self.cell_measures))
873878
varnames.extend(list(self.standard_names))
874879

875880
return set(varnames)
@@ -930,15 +935,23 @@ def cell_measures(self) -> Dict[str, List[str]]:
930935
Returns
931936
-------
932937
Dictionary of valid cell measure names that can be used with __getitem__ or .cf[key].
933-
Will be ("area", "volume") or a subset thereof.
934938
"""
935939

936-
measures = {
937-
key: apply_mapper(_get_measure, self._obj, key, error=False)
938-
for key in _CELL_MEASURES
939-
}
940+
obj = self._obj
941+
all_attrs = [da.attrs.get("cell_measures", "") for da in obj.coords.values()]
942+
if isinstance(obj, DataArray):
943+
all_attrs += [obj.attrs.get("cell_measures", "")]
944+
elif isinstance(obj, Dataset):
945+
all_attrs += [
946+
da.attrs.get("cell_measures", "") for da in obj.data_vars.values()
947+
]
948+
949+
measures: Dict[str, List[str]] = dict()
950+
for attr in all_attrs:
951+
for key, value in parse_cell_methods_attr(attr).items():
952+
measures[key] = measures.setdefault(key, []) + [value]
940953

941-
return {k: sorted(v) for k, v in measures.items() if v}
954+
return {k: sorted(set(v)) for k, v in measures.items() if v}
942955

943956
def get_standard_names(self) -> List[str]:
944957

@@ -1069,7 +1082,7 @@ def check_results(names, k):
10691082
check_results(names, k)
10701083
successful[k] = bool(names)
10711084
coords.extend(names)
1072-
elif k in _CELL_MEASURES:
1085+
elif k in self._get_all_cell_measures():
10731086
measure = _get_measure(self._obj, k)
10741087
check_results(measure, k)
10751088
successful[k] = bool(measure)

cf_xarray/tests/test_accessor.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,30 @@ def test_coordinates():
5252
assert actual == expected
5353

5454

55-
def test_cell_measures():
55+
def test_cell_measures(capsys):
5656
ds = airds.copy(deep=True)
5757
ds["foo"] = xr.DataArray(ds["cell_area"], attrs=dict(standard_name="foo_std_name"))
5858
ds["air"].attrs["cell_measures"] += " foo_measure: foo"
59-
assert "foo_std_name" in ds.cf["air_temperature"].cf
59+
assert ("foo_std_name" in ds.cf["air_temperature"].cf) and ("foo_measure" in ds.cf)
6060

6161
ds["air"].attrs["cell_measures"] += " volume: foo"
62-
expected = dict(area=["cell_area"], volume=["foo"])
63-
actual = ds["air"].cf.cell_measures
64-
assert actual == expected
62+
ds["foo"].attrs["cell_measures"] = ds["air"].attrs["cell_measures"]
63+
expected = dict(area=["cell_area"], foo_measure=["foo"], volume=["foo"])
64+
actual_air = ds["air"].cf.cell_measures
65+
actual_foo = ds.cf["foo_measure"].cf.cell_measures
66+
assert actual_air == actual_foo == expected
6567

6668
actual = ds.cf.cell_measures
6769
assert actual == expected
6870

71+
ds.cf.describe()
72+
actual = capsys.readouterr().out
73+
expected = (
74+
"\nCell Measures:\n\tarea: ['cell_area']\n\tfoo_measure: ['foo']\n\tvolume: ['foo']\n"
75+
"\nStandard Names:\n\tair_temperature: ['air']\n\tfoo_std_name: ['foo']\n\n"
76+
)
77+
assert actual.endswith(expected)
78+
6979

7080
def test_standard_names():
7181
expected = dict(
@@ -240,7 +250,10 @@ def test_kwargs_expand_key_to_multiple_keys():
240250
@pytest.mark.parametrize(
241251
"obj, expected",
242252
[
243-
(ds, {"latitude", "longitude", "time", "X", "Y", "T", "air_temperature"}),
253+
(
254+
ds,
255+
{"latitude", "longitude", "time", "X", "Y", "T", "air_temperature", "area"},
256+
),
244257
(ds.air, {"latitude", "longitude", "time", "X", "Y", "T", "area"}),
245258
(ds_no_attrs.air, set()),
246259
],

doc/examples/introduction.ipynb

+5-6
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,9 @@
559559
"cell_type": "markdown",
560560
"metadata": {},
561561
"source": [
562-
"**Note:** Although it is possible to assign additional coordinates and cell\n",
563-
"measures, `.cf.coordinates` and `.cf.cell_measures` only return a subset of\n",
564-
"`(\"longitude\", \"latitude\", \"vertical\", \"time\")` and `(\"area\", \"volume\")`,\n",
565-
"respectively.\n"
562+
"**Note:** Although it is possible to assign additional coordinates,\n",
563+
"`.cf.coordinates` only returns a subset of\n",
564+
"`(\"longitude\", \"latitude\", \"vertical\", \"time\")`.\n"
566565
]
567566
},
568567
{
@@ -927,8 +926,8 @@
927926
"source": [
928927
"## Feature: Weight by Cell Measures\n",
929928
"\n",
930-
"`cf_xarray` can weight by cell measure variables `\"area\"` and `\"volume\"` if the\n",
931-
"appropriate attribute is set\n"
929+
"`cf_xarray` can weight by cell measure variables if the appropriate attribute is\n",
930+
"set\n"
932931
]
933932
},
934933
{

doc/whats-new.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ What's New
33

44
v0.4.0 (unreleased)
55
===================
6-
6+
- Support for arbitrary cell measures indexing. By `Mattia Almansi`_.
77

88
v0.3.1 (Nov 25, 2020)
99
=====================

0 commit comments

Comments
 (0)