7
7
from .coding import strings , times , variables
8
8
from .coding .variables import SerializationWarning
9
9
from .core import duck_array_ops , indexing
10
+ from .core .common import contains_cftime_datetimes
10
11
from .core .pycompat import dask_array_type
11
12
from .core .variable import IndexVariable , Variable , as_variable
12
13
@@ -355,6 +356,51 @@ def _update_bounds_attributes(variables):
355
356
bounds_attrs .setdefault ('calendar' , attrs ['calendar' ])
356
357
357
358
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
+
358
404
def decode_cf_variables (variables , attributes , concat_characters = True ,
359
405
mask_and_scale = True , decode_times = True ,
360
406
decode_coords = True , drop_variables = None ,
@@ -492,8 +538,6 @@ def cf_decoder(variables, attributes,
492
538
"""
493
539
Decode a set of CF encoded variables and attributes.
494
540
495
- See Also, decode_cf_variable
496
-
497
541
Parameters
498
542
----------
499
543
variables : dict
@@ -515,6 +559,10 @@ def cf_decoder(variables, attributes,
515
559
A dictionary mapping from variable name to xarray.Variable objects.
516
560
decoded_attributes : dict
517
561
A dictionary mapping from attribute name to values.
562
+
563
+ See also
564
+ --------
565
+ decode_cf_variable
518
566
"""
519
567
variables , attributes , _ = decode_cf_variables (
520
568
variables , attributes , concat_characters , mask_and_scale , decode_times )
@@ -595,14 +643,12 @@ def encode_dataset_coordinates(dataset):
595
643
596
644
def cf_encoder (variables , attributes ):
597
645
"""
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.
604
651
605
- See Also, decode_cf_variable
606
652
607
653
Parameters
608
654
----------
@@ -618,8 +664,27 @@ def cf_encoder(variables, attributes):
618
664
encoded_attributes : dict
619
665
A dictionary mapping from attribute name to value
620
666
621
- See also: encode_cf_variable
667
+ See also
668
+ --------
669
+ decode_cf_variable, encode_cf_variable
622
670
"""
671
+
672
+ # add encoding for time bounds variables if present.
673
+ _update_bounds_encoding (variables )
674
+
623
675
new_vars = OrderedDict ((k , encode_cf_variable (v , name = k ))
624
676
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
+
625
690
return new_vars , attributes
0 commit comments