77from .coding import strings , times , variables
88from .coding .variables import SerializationWarning
99from .core import duck_array_ops , indexing
10+ from .core .common import contains_cftime_datetimes
1011from .core .pycompat import dask_array_type
1112from .core .variable import IndexVariable , Variable , as_variable
1213
@@ -355,6 +356,51 @@ def _update_bounds_attributes(variables):
355356 bounds_attrs .setdefault ('calendar' , attrs ['calendar' ])
356357
357358
359+ def _update_bounds_encoding (variables ):
360+ """Adds time encoding to time bounds variables.
361+
362+ Variables handling time bounds ("Cell boundaries" in the CF
363+ conventions) do not necessarily carry the necessary attributes to be
364+ decoded. This copies the encoding from the time variable to the
365+ associated bounds variable so that we write CF-compliant files.
366+
367+ See Also:
368+
369+ http://cfconventions.org/Data/cf-conventions/cf-conventions-1.7/
370+ cf-conventions.html#cell-boundaries
371+
372+ https://github.com/pydata/xarray/issues/2565
373+ """
374+
375+ # For all time variables with bounds
376+ for v in variables .values ():
377+ attrs = v .attrs
378+ encoding = v .encoding
379+ has_date_units = 'units' in encoding and 'since' in encoding ['units' ]
380+ is_datetime_type = (np .issubdtype (v .dtype , np .datetime64 ) or
381+ contains_cftime_datetimes (v ))
382+
383+ if (is_datetime_type and not has_date_units and
384+ 'bounds' in attrs and attrs ['bounds' ] in variables ):
385+ warnings .warn ("Variable '{0}' has datetime type and a "
386+ "bounds variable but {0}.encoding does not have "
387+ "units specified. The units encodings for '{0}' "
388+ "and '{1}' will be determined independently "
389+ "and may not be equal, counter to CF-conventions. "
390+ "If this is a concern, specify a units encoding for "
391+ "'{0}' before writing to a file."
392+ .format (v .name , attrs ['bounds' ]),
393+ UserWarning )
394+
395+ if has_date_units and 'bounds' in attrs :
396+ if attrs ['bounds' ] in variables :
397+ bounds_encoding = variables [attrs ['bounds' ]].encoding
398+ bounds_encoding .setdefault ('units' , encoding ['units' ])
399+ if 'calendar' in encoding :
400+ bounds_encoding .setdefault ('calendar' ,
401+ encoding ['calendar' ])
402+
403+
358404def decode_cf_variables (variables , attributes , concat_characters = True ,
359405 mask_and_scale = True , decode_times = True ,
360406 decode_coords = True , drop_variables = None ,
@@ -492,8 +538,6 @@ def cf_decoder(variables, attributes,
492538 """
493539 Decode a set of CF encoded variables and attributes.
494540
495- See Also, decode_cf_variable
496-
497541 Parameters
498542 ----------
499543 variables : dict
@@ -515,6 +559,10 @@ def cf_decoder(variables, attributes,
515559 A dictionary mapping from variable name to xarray.Variable objects.
516560 decoded_attributes : dict
517561 A dictionary mapping from attribute name to values.
562+
563+ See also
564+ --------
565+ decode_cf_variable
518566 """
519567 variables , attributes , _ = decode_cf_variables (
520568 variables , attributes , concat_characters , mask_and_scale , decode_times )
@@ -595,14 +643,12 @@ def encode_dataset_coordinates(dataset):
595643
596644def cf_encoder (variables , attributes ):
597645 """
598- A function which takes a dicts of variables and attributes
599- and encodes them to conform to CF conventions as much
600- as possible. This includes masking, scaling, character
601- array handling, and CF-time encoding.
602-
603- Decode a set of CF encoded variables and attributes.
646+ Encode a set of CF encoded variables and attributes.
647+ Takes a dicts of variables and attributes and encodes them
648+ to conform to CF conventions as much as possible.
649+ This includes masking, scaling, character array handling,
650+ and CF-time encoding.
604651
605- See Also, decode_cf_variable
606652
607653 Parameters
608654 ----------
@@ -618,8 +664,27 @@ def cf_encoder(variables, attributes):
618664 encoded_attributes : dict
619665 A dictionary mapping from attribute name to value
620666
621- See also: encode_cf_variable
667+ See also
668+ --------
669+ decode_cf_variable, encode_cf_variable
622670 """
671+
672+ # add encoding for time bounds variables if present.
673+ _update_bounds_encoding (variables )
674+
623675 new_vars = OrderedDict ((k , encode_cf_variable (v , name = k ))
624676 for k , v in variables .items ())
677+
678+ # Remove attrs from bounds variables (issue #2921)
679+ for var in new_vars .values ():
680+ bounds = var .attrs ['bounds' ] if 'bounds' in var .attrs else None
681+ if bounds and bounds in new_vars :
682+ # see http://cfconventions.org/cf-conventions/cf-conventions.html#cell-boundaries # noqa
683+ for attr in ['units' , 'standard_name' , 'axis' , 'positive' ,
684+ 'calendar' , 'long_name' , 'leap_month' , 'leap_year' ,
685+ 'month_lengths' ]:
686+ if attr in new_vars [bounds ].attrs and attr in var .attrs :
687+ if new_vars [bounds ].attrs [attr ] == var .attrs [attr ]:
688+ new_vars [bounds ].attrs .pop (attr )
689+
625690 return new_vars , attributes
0 commit comments