@@ -674,31 +674,54 @@ def encode_cf_datetime(
674
674
calendar : str | None = None ,
675
675
dtype : np .dtype | None = None ,
676
676
):
677
+ """Given an array of datetime objects, returns the tuple `(num, units,
678
+ calendar)` suitable for a CF compliant time variable.
679
+
680
+ Unlike `date2num`, this function can handle datetime64 arrays.
681
+
682
+ See Also
683
+ --------
684
+ cftime.date2num
685
+ """
677
686
dates = asarray (dates )
678
687
if isinstance (dates , np .ndarray ):
679
688
return _eagerly_encode_cf_datetime (dates , units , calendar , dtype )
680
689
elif is_duck_dask_array (dates ):
681
690
return _lazily_encode_cf_datetime (dates , units , calendar , dtype )
682
691
683
692
684
- def _cast_to_dtype_safe (num , dtype ) -> np .ndarray :
685
- cast_num = np .asarray (num , dtype = dtype )
693
+ def _cast_to_dtype_safe (num : np .ndarray , dtype : np .dtype ) -> np .ndarray :
694
+ with warnings .catch_warnings ():
695
+ warnings .filterwarnings ("ignore" , message = "overflow" )
696
+ cast_num = np .asarray (num , dtype = dtype )
686
697
687
698
if np .issubdtype (dtype , np .integer ):
688
699
if not (num == cast_num ).all ():
689
- raise ValueError (
690
- f"Not possible to cast all encoded times from dtype { num .dtype !r} "
691
- f"to dtype { dtype !r} without changing any of their values. "
692
- f"Consider removing the dtype encoding or explicitly switching to "
693
- f"a dtype encoding with a higher precision."
694
- )
700
+ if np .issubdtype (num .dtype , np .floating ):
701
+ raise ValueError (
702
+ f"Not possible to cast all encoded times from dtype "
703
+ f"{ num .dtype !r} to integer dtype { dtype !r} without losing "
704
+ f"precision. Consider modifying the units such that "
705
+ f"integer values can be used, or removing the units and "
706
+ f"dtype encoding, at which point xarray will make an "
707
+ f"appropriate choice."
708
+ )
709
+ else :
710
+ raise OverflowError (
711
+ f"Not possible to cast encoded times from dtype "
712
+ f"{ num .dtype !r} to dtype { dtype !r} without overflow. "
713
+ f"Consider removing the dtype encoding, at which point "
714
+ f"xarray will make an appropriate choice, or explicitly "
715
+ f"switching to a larger integer dtype."
716
+ )
695
717
else :
696
718
if np .isinf (cast_num ).any ():
697
719
raise OverflowError (
698
720
f"Not possible to cast encoded times from dtype { num .dtype !r} "
699
721
f"to dtype { dtype !r} without overflow. Consider removing the "
700
- f"dtype encoding or explicitly switching to a dtype encoding "
701
- f"with a higher precision."
722
+ f"dtype encoding, at which point xarray will make an "
723
+ f"appropriate choice, or explicitly switching to a larger "
724
+ f"floating point dtype."
702
725
)
703
726
704
727
return cast_num
@@ -711,15 +734,6 @@ def _eagerly_encode_cf_datetime(
711
734
dtype : np .dtype | None = None ,
712
735
called_via_map_blocks : bool = False ,
713
736
) -> tuple [np .ndarray , str , str ]:
714
- """Given an array of datetime objects, returns the tuple `(num, units,
715
- calendar)` suitable for a CF compliant time variable.
716
-
717
- Unlike `date2num`, this function can handle datetime64 arrays.
718
-
719
- See Also
720
- --------
721
- cftime.date2num
722
- """
723
737
dates = np .asarray (dates )
724
738
725
739
data_units = infer_datetime_units (dates )
@@ -796,7 +810,7 @@ def _eagerly_encode_cf_datetime(
796
810
if called_via_map_blocks :
797
811
return num
798
812
else :
799
- return ( num , units , calendar )
813
+ return num , units , calendar
800
814
801
815
802
816
def _lazily_encode_cf_datetime (
@@ -821,10 +835,10 @@ def _lazily_encode_cf_datetime(
821
835
822
836
if units is None or dtype is None :
823
837
raise ValueError (
824
- f"When encoding chunked arrays of datetime values, both the units and "
825
- f"dtype must be prescribed or both must be unprescribed. Prescribing "
826
- f"only one or the other is not currently supported. Got a units "
827
- f"encoding of { units } and a dtype encoding of { dtype } ."
838
+ f"When encoding chunked arrays of datetime values, both the units "
839
+ f"and dtype must be prescribed or both must be unprescribed. "
840
+ f"Prescribing only one or the other is not currently supported. "
841
+ f"Got a units encoding of { units } and a dtype encoding of { dtype } ."
828
842
)
829
843
830
844
num = dask .array .map_blocks (
@@ -841,6 +855,19 @@ def _lazily_encode_cf_datetime(
841
855
842
856
def encode_cf_timedelta (
843
857
timedeltas , units : str | None = None , dtype : np .dtype | None = None
858
+ ):
859
+ timedeltas = asarray (timedeltas )
860
+ if is_duck_dask_array (timedeltas ):
861
+ return _lazily_encode_cf_timedelta (timedeltas , units , dtype )
862
+ else :
863
+ return _eagerly_encode_cf_timedelta (timedeltas , units , dtype )
864
+
865
+
866
+ def _eagerly_encode_cf_timedelta (
867
+ timedeltas ,
868
+ units : str | None = None ,
869
+ dtype : np .dtype | None = None ,
870
+ called_via_map_blocks : bool = False ,
844
871
) -> tuple [np .ndarray , str ]:
845
872
data_units = infer_timedelta_units (timedeltas )
846
873
@@ -868,7 +895,7 @@ def encode_cf_timedelta(
868
895
f"Set encoding['dtype'] to integer dtype to serialize to int64. "
869
896
f"Set encoding['dtype'] to floating point dtype to silence this warning."
870
897
)
871
- elif np .issubdtype (dtype , np .integer ):
898
+ elif np .issubdtype (dtype , np .integer ) and not called_via_map_blocks :
872
899
emit_user_level_warning (
873
900
f"Timedeltas can't be serialized faithfully with requested units { units !r} . "
874
901
f"Serializing with units { needed_units !r} instead. "
@@ -881,7 +908,43 @@ def encode_cf_timedelta(
881
908
882
909
num = _division (time_deltas , time_delta , floor_division )
883
910
num = num .values .reshape (timedeltas .shape )
884
- return (num , units )
911
+
912
+ if dtype is not None :
913
+ num = _cast_to_dtype_safe (num , dtype )
914
+
915
+ if called_via_map_blocks :
916
+ return num
917
+ else :
918
+ return num , units
919
+
920
+
921
+ def _lazily_encode_cf_timedelta (
922
+ timedeltas , units : str | None = None , dtype : np .dtype | None = None
923
+ ):
924
+ import dask .array
925
+
926
+ if units is None and dtype is None :
927
+ units = "nanoseconds"
928
+ dtype = np .dtype ("int64" )
929
+
930
+ if units is None or dtype is None :
931
+ raise ValueError (
932
+ f"When encoding chunked arrays of timedelta values, both the "
933
+ f"units and dtype must be prescribed or both must be "
934
+ f"unprescribed. Prescribing only one or the other is not "
935
+ f"currently supported. Got a units encoding of { units } and a "
936
+ f"dtype encoding of { dtype } ."
937
+ )
938
+
939
+ num = dask .array .map_blocks (
940
+ _eagerly_encode_cf_timedelta ,
941
+ timedeltas ,
942
+ units ,
943
+ dtype ,
944
+ called_via_map_blocks = True ,
945
+ dtype = dtype ,
946
+ )
947
+ return num , units
885
948
886
949
887
950
class CFDatetimeCoder (VariableCoder ):
0 commit comments