Skip to content

Commit 16d8c39

Browse files
authored
Add support for flag variables. (#252)
1 parent 5185d95 commit 16d8c39

File tree

6 files changed

+217
-4
lines changed

6 files changed

+217
-4
lines changed

cf_xarray/accessor.py

+142-4
Original file line numberDiff line numberDiff line change
@@ -886,15 +886,134 @@ def __getattr__(self, attr):
886886
)
887887

888888

889+
def create_flag_dict(da):
890+
if not da.cf.is_flag_variable:
891+
raise ValueError(
892+
"Comparisons are only supported for DataArrays that represent CF flag variables."
893+
".attrs must contain 'flag_values' and 'flag_meanings'"
894+
)
895+
896+
flag_meanings = da.attrs["flag_meanings"].split(" ")
897+
flag_values = da.attrs["flag_values"]
898+
# TODO: assert flag_values is iterable
899+
assert len(flag_values) == len(flag_meanings)
900+
return dict(zip(flag_meanings, flag_values))
901+
902+
889903
class CFAccessor:
890904
"""
891905
Common Dataset and DataArray accessor functionality.
892906
"""
893907

894-
def __init__(self, da):
895-
self._obj = da
908+
def __init__(self, obj):
909+
self._obj = obj
896910
self._all_cell_measures = None
897911

912+
def _assert_valid_other_comparison(self, other):
913+
flag_dict = create_flag_dict(self._obj)
914+
if other not in flag_dict:
915+
raise ValueError(
916+
f"Did not find flag value meaning [{other}] in known flag meanings: [{flag_dict.keys()!r}]"
917+
)
918+
return flag_dict
919+
920+
def __eq__(self, other):
921+
"""
922+
Compare flag values against `other`.
923+
924+
`other` must be in the 'flag_meanings' attribute.
925+
`other` is mapped to the corresponding value in the 'flag_values' attribute, and then
926+
compared.
927+
"""
928+
flag_dict = self._assert_valid_other_comparison(other)
929+
return self._obj == flag_dict[other]
930+
931+
def __ne__(self, other):
932+
"""
933+
Compare flag values against `other`.
934+
935+
`other` must be in the 'flag_meanings' attribute.
936+
`other` is mapped to the corresponding value in the 'flag_values' attribute, and then
937+
compared.
938+
"""
939+
flag_dict = self._assert_valid_other_comparison(other)
940+
return self._obj != flag_dict[other]
941+
942+
def __lt__(self, other):
943+
"""
944+
Compare flag values against `other`.
945+
946+
`other` must be in the 'flag_meanings' attribute.
947+
`other` is mapped to the corresponding value in the 'flag_values' attribute, and then
948+
compared.
949+
"""
950+
flag_dict = self._assert_valid_other_comparison(other)
951+
return self._obj < flag_dict[other]
952+
953+
def __le__(self, other):
954+
"""
955+
Compare flag values against `other`.
956+
957+
`other` must be in the 'flag_meanings' attribute.
958+
`other` is mapped to the corresponding value in the 'flag_values' attribute, and then
959+
compared.
960+
"""
961+
flag_dict = self._assert_valid_other_comparison(other)
962+
return self._obj <= flag_dict[other]
963+
964+
def __gt__(self, other):
965+
"""
966+
Compare flag values against `other`.
967+
968+
`other` must be in the 'flag_meanings' attribute.
969+
`other` is mapped to the corresponding value in the 'flag_values' attribute, and then
970+
compared.
971+
"""
972+
flag_dict = self._assert_valid_other_comparison(other)
973+
return self._obj > flag_dict[other]
974+
975+
def __ge__(self, other):
976+
"""
977+
Compare flag values against `other`.
978+
979+
`other` must be in the 'flag_meanings' attribute.
980+
`other` is mapped to the corresponding value in the 'flag_values' attribute, and then
981+
compared.
982+
"""
983+
flag_dict = self._assert_valid_other_comparison(other)
984+
return self._obj >= flag_dict[other]
985+
986+
def isin(self, test_elements):
987+
"""Test each value in the array for whether it is in test_elements.
988+
989+
Parameters
990+
----------
991+
test_elements : array_like, 1D
992+
The values against which to test each value of `element`.
993+
These must be in "flag_meanings" attribute, and are mapped
994+
to the corresponding value in "flag_values" before passing
995+
that on to DataArray.isin.
996+
997+
998+
Returns
999+
-------
1000+
isin : DataArray
1001+
Has the same type and shape as this object, but with a bool dtype.
1002+
"""
1003+
if not isinstance(self._obj, DataArray):
1004+
raise ValueError(
1005+
".cf.isin is only supported on DataArrays that contain CF flag attributes."
1006+
)
1007+
flag_dict = create_flag_dict(self._obj)
1008+
mapped_test_elements = []
1009+
for elem in test_elements:
1010+
if elem not in flag_dict:
1011+
raise ValueError(
1012+
f"Did not find flag value meaning [{elem}] in known flag meanings: [{flag_dict.keys()!r}]"
1013+
)
1014+
mapped_test_elements.append(flag_dict[elem])
1015+
return self._obj.isin(mapped_test_elements)
1016+
8981017
def _get_all_cell_measures(self):
8991018
"""
9001019
Get all cell measures defined in the object, adding CF pre-defined measures.
@@ -1130,7 +1249,12 @@ def make_text_section(subtitle, attr, valid_values, default_keys=None):
11301249

11311250
return "\n".join(rows) + "\n"
11321251

1133-
text = "Coordinates:"
1252+
if isinstance(self._obj, DataArray) and self._obj.cf.is_flag_variable:
1253+
flag_dict = create_flag_dict(self._obj)
1254+
text = f"CF Flag variable with mapping:\n\t{flag_dict!r}\n\n"
1255+
else:
1256+
text = ""
1257+
text += "Coordinates:"
11341258
text += make_text_section("CF Axes", "axes", coords, _AXIS_NAMES)
11351259
text += make_text_section("CF Coordinates", "coordinates", coords, _COORD_NAMES)
11361260
text += make_text_section(
@@ -2057,4 +2181,18 @@ def __getitem__(self, key: Union[str, List[str]]) -> DataArray:
20572181

20582182
return _getitem(self, key)
20592183

2060-
pass
2184+
@property
2185+
def is_flag_variable(self):
2186+
"""
2187+
Returns True if the DataArray satisfies CF conventions for flag variables.
2188+
2189+
Flag masks are not supported yet.
2190+
"""
2191+
if (
2192+
isinstance(self._obj, DataArray)
2193+
and "flag_meanings" in self._obj.attrs
2194+
and "flag_values" in self._obj.attrs
2195+
):
2196+
return True
2197+
else:
2198+
return False

cf_xarray/datasets.py

+12
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,15 @@
279279
}
280280
)
281281
)
282+
283+
284+
basin = xr.DataArray(
285+
[1, 2, 1, 1, 2, 2, 3, 3, 3, 3],
286+
dims=("time",),
287+
attrs={
288+
"flag_values": [1, 2, 3],
289+
"flag_meanings": "atlantic_ocean pacific_ocean indian_ocean",
290+
"standard_name": "region",
291+
},
292+
name="basin",
293+
)

cf_xarray/tests/test_accessor.py

+36
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ..datasets import (
1818
airds,
1919
anc,
20+
basin,
2021
ds_no_attrs,
2122
forecast,
2223
mollwds,
@@ -128,6 +129,9 @@ def test_repr():
128129
"""
129130
assert actual == dedent(expected)
130131

132+
# Flag DataArray
133+
assert "CF Flag variable" in repr(basin.cf)
134+
131135

132136
def test_axes():
133137
expected = dict(T=["time"], X=["lon"], Y=["lat"])
@@ -1389,3 +1393,35 @@ def test_add_canonical_attributes(override, skip, verbose, capsys):
13891393

13901394
cf_da.attrs.pop("history")
13911395
assert_identical(cf_da, cf_ds["air"])
1396+
1397+
1398+
@pytest.mark.parametrize("op", ["ge", "gt", "eq", "ne", "le", "lt"])
1399+
def test_flag_features(op):
1400+
actual = getattr(basin.cf, f"__{op}__")("atlantic_ocean")
1401+
expected = getattr(basin, f"__{op}__")(1)
1402+
assert_identical(actual, expected)
1403+
1404+
1405+
def test_flag_isin():
1406+
actual = basin.cf.isin(["atlantic_ocean", "pacific_ocean"])
1407+
expected = basin.isin([1, 2])
1408+
assert_identical(actual, expected)
1409+
1410+
1411+
def test_flag_errors():
1412+
with pytest.raises(ValueError):
1413+
basin.cf.isin(["arctic_ocean"])
1414+
1415+
with pytest.raises(ValueError):
1416+
basin.cf == "arctic_ocean"
1417+
1418+
ds = xr.Dataset({"basin": basin})
1419+
with pytest.raises(ValueError):
1420+
ds.cf.isin(["atlantic_ocean"])
1421+
1422+
basin.attrs.pop("flag_values")
1423+
with pytest.raises(ValueError):
1424+
basin.cf.isin(["pacific_ocean"])
1425+
1426+
with pytest.raises(ValueError):
1427+
basin.cf == "pacific_ocean"

ci/doc.yml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies:
1616
- ipykernel
1717
- ipywidgets
1818
- pandas
19+
- pooch
1920
- pydata-sphinx-theme
2021
- pip:
2122
- git+https://github.com/xarray-contrib/cf-xarray

doc/api.rst

+22
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Attributes
3131
DataArray.cf.cell_measures
3232
DataArray.cf.coordinates
3333
DataArray.cf.formula_terms
34+
DataArray.cf.is_flag_variable
3435
DataArray.cf.standard_names
3536
DataArray.cf.plot
3637

@@ -52,6 +53,24 @@ Methods
5253
DataArray.cf.keys
5354
DataArray.cf.rename_like
5455

56+
Flag Variables
57+
++++++++++++++
58+
59+
cf_xarray supports rich comparisons for `CF flag variables`_. Flag masks are not yet supported.
60+
61+
.. autosummary::
62+
:toctree: generated/
63+
:template: autosummary/accessor_method.rst
64+
65+
DataArray.cf.__lt__
66+
DataArray.cf.__le__
67+
DataArray.cf.__eq__
68+
DataArray.cf.__ne__
69+
DataArray.cf.__ge__
70+
DataArray.cf.__gt__
71+
DataArray.cf.isin
72+
73+
5574
Dataset
5675
-------
5776

@@ -92,3 +111,6 @@ Methods
92111
Dataset.cf.guess_coord_axis
93112
Dataset.cf.keys
94113
Dataset.cf.rename_like
114+
115+
116+
.. _`CF flag variables`: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags

doc/whats-new.rst

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ v0.6.1 (unreleased)
77
===================
88
- Support detecting pint-backed Variables with units-based criteria. By `Deepak Cherian`_.
99
- Support reshaping nD bounds arrays to (n-1)D vertex arrays. By `Deepak Cherian`_.
10+
- Support rich comparisons with ``DataArray.cf`` and :py:meth:`DataArray.cf.isin` for `flag variables`_.
11+
By `Deepak Cherian`_ and `Julius Busecke`_
1012

1113
v0.6.0 (June 29, 2021)
1214
======================
@@ -125,3 +127,5 @@ v0.1.3
125127
.. _`Filipe Fernandes`: https://github.com/ocefpaf
126128
.. _`Julia Kent`: https://github.com/jukent
127129
.. _`Kristen Thyng`: https://github.com/kthyng
130+
.. _`Julius Busecke`: https://github.com/jbusecke
131+
.. _`flag variables`: http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags

0 commit comments

Comments
 (0)