Skip to content

Commit 57348ab

Browse files
fmaussiondcherian
authored andcommitted
CF: also decode time bounds when available (#2571)
* CF: also decode time bounds when available * Fix failing test when cftime not present and what's new * Fix windows * Reviews * Reviews 2
1 parent 778ffc4 commit 57348ab

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

doc/whats-new.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ v0.11.1 (unreleased)
3333
Breaking changes
3434
~~~~~~~~~~~~~~~~
3535

36+
- Time bounds variables are now also decoded according to CF conventions
37+
(:issue:`2565`). The previous behavior was to decode them only if they
38+
had specific time attributes, now these attributes are copied
39+
automatically from the corresponding time coordinate. This might
40+
brake downstream code that was relying on these variables to be
41+
not decoded.
42+
By `Fabien Maussion <https://github.com/fmaussion>`_.
43+
3644
Enhancements
3745
~~~~~~~~~~~~
3846

@@ -49,7 +57,6 @@ Enhancements
4957
``loffset`` kwarg just like Pandas.
5058
By `Deepak Cherian <https://github.com/dcherian>`_
5159

52-
5360
Bug fixes
5461
~~~~~~~~~
5562

xarray/conventions.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,39 @@ def decode_cf_variable(name, var, concat_characters=True, mask_and_scale=True,
320320
return Variable(dimensions, data, attributes, encoding=encoding)
321321

322322

323+
def _update_bounds_attributes(variables):
324+
"""Adds time attributes to time bounds variables.
325+
326+
Variables handling time bounds ("Cell boundaries" in the CF
327+
conventions) do not necessarily carry the necessary attributes to be
328+
decoded. This copies the attributes from the time variable to the
329+
associated boundaries.
330+
331+
See Also:
332+
333+
http://cfconventions.org/Data/cf-conventions/cf-conventions-1.7/
334+
cf-conventions.html#cell-boundaries
335+
336+
https://github.com/pydata/xarray/issues/2565
337+
"""
338+
339+
# For all time variables with bounds
340+
for v in variables.values():
341+
attrs = v.attrs
342+
has_date_units = 'units' in attrs and 'since' in attrs['units']
343+
if has_date_units and 'bounds' in attrs:
344+
if attrs['bounds'] in variables:
345+
bounds_attrs = variables[attrs['bounds']].attrs
346+
bounds_attrs.setdefault('units', attrs['units'])
347+
if 'calendar' in attrs:
348+
bounds_attrs.setdefault('calendar', attrs['calendar'])
349+
350+
323351
def decode_cf_variables(variables, attributes, concat_characters=True,
324352
mask_and_scale=True, decode_times=True,
325353
decode_coords=True, drop_variables=None):
326354
"""
327-
Decode a several CF encoded variables.
355+
Decode several CF encoded variables.
328356
329357
See: decode_cf_variable
330358
"""
@@ -350,6 +378,10 @@ def stackable(dim):
350378
drop_variables = []
351379
drop_variables = set(drop_variables)
352380

381+
# Time bounds coordinates might miss the decoding attributes
382+
if decode_times:
383+
_update_bounds_attributes(variables)
384+
353385
new_vars = OrderedDict()
354386
for k, v in iteritems(variables):
355387
if k in drop_variables:

xarray/tests/test_coding_times.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from xarray import DataArray, Variable, coding, decode_cf
1111
from xarray.coding.times import (_import_cftime, cftime_to_nptime,
1212
decode_cf_datetime, encode_cf_datetime)
13+
from xarray.conventions import _update_bounds_attributes
1314
from xarray.core.common import contains_cftime_datetimes
1415

1516
from . import (
@@ -624,6 +625,41 @@ def test_decode_cf(calendar):
624625
assert ds.test.dtype == np.dtype('M8[ns]')
625626

626627

628+
def test_decode_cf_time_bounds():
629+
630+
da = DataArray(np.arange(6, dtype='int64').reshape((3, 2)),
631+
coords={'time': [1, 2, 3]},
632+
dims=('time', 'nbnd'), name='time_bnds')
633+
634+
attrs = {'units': 'days since 2001-01',
635+
'calendar': 'standard',
636+
'bounds': 'time_bnds'}
637+
638+
ds = da.to_dataset()
639+
ds['time'].attrs.update(attrs)
640+
_update_bounds_attributes(ds.variables)
641+
assert ds.variables['time_bnds'].attrs == {'units': 'days since 2001-01',
642+
'calendar': 'standard'}
643+
dsc = decode_cf(ds)
644+
assert dsc.time_bnds.dtype == np.dtype('M8[ns]')
645+
dsc = decode_cf(ds, decode_times=False)
646+
assert dsc.time_bnds.dtype == np.dtype('int64')
647+
648+
# Do not overwrite existing attrs
649+
ds = da.to_dataset()
650+
ds['time'].attrs.update(attrs)
651+
bnd_attr = {'units': 'hours since 2001-01', 'calendar': 'noleap'}
652+
ds['time_bnds'].attrs.update(bnd_attr)
653+
_update_bounds_attributes(ds.variables)
654+
assert ds.variables['time_bnds'].attrs == bnd_attr
655+
656+
# If bounds variable not available do not complain
657+
ds = da.to_dataset()
658+
ds['time'].attrs.update(attrs)
659+
ds['time'].attrs['bounds'] = 'fake_var'
660+
_update_bounds_attributes(ds.variables)
661+
662+
627663
@pytest.fixture(params=_ALL_CALENDARS)
628664
def calendar(request):
629665
return request.param

0 commit comments

Comments
 (0)