diff --git a/tests/fixtures.py b/tests/fixtures.py index d79fb05a..6ac3c513 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -41,17 +41,26 @@ "standard_name": "time", }, ) +# NOTE: With `decode_times=True`, the "calendar" and "units" attributes are +# stored in `.encoding`. +time_cf.encoding["calendar"] = "standard" +time_cf.encoding["units"] = "days since 2000-01-01" + + +# NOTE: With `decode_times=False`, the "calendar" and "units" attributes are +# stored in `.attrs`. time_non_cf = xr.DataArray( data=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], dims=["time"], attrs={ - "units": "months since 2000-01-01", - "calendar": "standard", "axis": "T", "long_name": "time", "standard_name": "time", + "calendar": "standard", + "units": "months since 2000-01-01", }, ) + time_non_cf_unsupported = xr.DataArray( data=np.arange(1850 + 1 / 24.0, 1851 + 3 / 12.0, 1 / 12.0), dims=["time"], diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 81d85826..571bb330 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -64,22 +64,53 @@ def test_non_cf_compliant_time_is_decoded(self): name="time", data=np.array( [ - cftime.datetime(2000, 1, 1), - cftime.datetime(2000, 2, 1), - cftime.datetime(2000, 3, 1), - cftime.datetime(2000, 4, 1), - cftime.datetime(2000, 5, 1), - cftime.datetime(2000, 6, 1), - cftime.datetime(2000, 7, 1), - cftime.datetime(2000, 8, 1), - cftime.datetime(2000, 9, 1), - cftime.datetime(2000, 10, 1), - cftime.datetime(2000, 11, 1), - cftime.datetime(2000, 12, 1), - cftime.datetime(2001, 1, 1), - cftime.datetime(2001, 2, 1), - cftime.datetime(2001, 3, 1), + cftime.DatetimeGregorian( + 2000, 1, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 4, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 5, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 6, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 7, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 8, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 9, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 10, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 11, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 12, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 1, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), ], + dtype="object", ), dims="time", ) @@ -88,53 +119,133 @@ def test_non_cf_compliant_time_is_decoded(self): data=np.array( [ [ - cftime.datetime(1999, 12, 16, 12), - cftime.datetime(2000, 1, 16, 12), + cftime.DatetimeGregorian( + 1999, 12, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 2, 15, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 2, 15, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 3, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 3, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 4, 16, 0, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 4, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 5, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 5, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 6, 16, 0, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 6, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 7, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 7, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 8, 16, 12, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 1, 16, 12), - cftime.datetime(2000, 2, 15, 12), + cftime.DatetimeGregorian( + 2000, 8, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 9, 16, 0, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 2, 15, 12), - cftime.datetime(2000, 3, 16, 12), + cftime.DatetimeGregorian( + 2000, 9, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 10, 16, 12, 0, 0, 0, has_year_zero=False + ), ], - [cftime.datetime(2000, 3, 16, 12), cftime.datetime(2000, 4, 16, 0)], - [cftime.datetime(2000, 4, 16, 0), cftime.datetime(2000, 5, 16, 12)], - [cftime.datetime(2000, 5, 16, 12), cftime.datetime(2000, 6, 16, 0)], - [cftime.datetime(2000, 6, 16, 0), cftime.datetime(2000, 7, 16, 12)], [ - cftime.datetime(2000, 7, 16, 12), - cftime.datetime(2000, 8, 16, 12), + cftime.DatetimeGregorian( + 2000, 10, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 11, 16, 0, 0, 0, 0, has_year_zero=False + ), ], - [cftime.datetime(2000, 8, 16, 12), cftime.datetime(2000, 9, 16, 0)], [ - cftime.datetime(2000, 9, 16, 0), - cftime.datetime(2000, 10, 16, 12), + cftime.DatetimeGregorian( + 2000, 11, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 12, 16, 12, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 10, 16, 12), - cftime.datetime(2000, 11, 16, 0), + cftime.DatetimeGregorian( + 2000, 12, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 11, 16, 0), - cftime.datetime(2000, 12, 16, 12), + cftime.DatetimeGregorian( + 2001, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 2, 15, 0, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 12, 16, 12), - cftime.datetime(2001, 1, 16, 12), + cftime.DatetimeGregorian( + 2001, 2, 15, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 3, 15, 0, 0, 0, 0, has_year_zero=False + ), ], - [cftime.datetime(2001, 1, 16, 12), cftime.datetime(2001, 2, 15, 0)], - [cftime.datetime(2001, 2, 15, 0), cftime.datetime(2001, 3, 15, 0)], - ] + ], + dtype="object", ), dims=["time", "bnds"], attrs={"xcdat_bounds": "True"}, ) expected.time.attrs = { - "units": "months since 2000-01-01", - "calendar": "standard", "axis": "T", "long_name": "time", "standard_name": "time", @@ -218,27 +329,57 @@ def test_non_cf_compliant_time_is_decoded(self): # Generate an expected dataset with decoded non-CF compliant time units. expected = generate_dataset(cf_compliant=True, has_bounds=True) - expected["time"] = xr.DataArray( name="time", data=np.array( [ - cftime.datetime(2000, 1, 1), - cftime.datetime(2000, 2, 1), - cftime.datetime(2000, 3, 1), - cftime.datetime(2000, 4, 1), - cftime.datetime(2000, 5, 1), - cftime.datetime(2000, 6, 1), - cftime.datetime(2000, 7, 1), - cftime.datetime(2000, 8, 1), - cftime.datetime(2000, 9, 1), - cftime.datetime(2000, 10, 1), - cftime.datetime(2000, 11, 1), - cftime.datetime(2000, 12, 1), - cftime.datetime(2001, 1, 1), - cftime.datetime(2001, 2, 1), - cftime.datetime(2001, 3, 1), + cftime.DatetimeGregorian( + 2000, 1, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 4, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 5, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 6, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 7, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 8, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 9, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 10, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 11, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 12, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 1, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), ], + dtype="object", ), dims="time", ) @@ -247,53 +388,133 @@ def test_non_cf_compliant_time_is_decoded(self): data=np.array( [ [ - cftime.datetime(1999, 12, 16, 12), - cftime.datetime(2000, 1, 16, 12), + cftime.DatetimeGregorian( + 1999, 12, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 2, 15, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 2, 15, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 3, 16, 12, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 1, 16, 12), - cftime.datetime(2000, 2, 15, 12), + cftime.DatetimeGregorian( + 2000, 3, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 4, 16, 0, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 2, 15, 12), - cftime.datetime(2000, 3, 16, 12), + cftime.DatetimeGregorian( + 2000, 4, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 5, 16, 12, 0, 0, 0, has_year_zero=False + ), ], - [cftime.datetime(2000, 3, 16, 12), cftime.datetime(2000, 4, 16, 0)], - [cftime.datetime(2000, 4, 16, 0), cftime.datetime(2000, 5, 16, 12)], - [cftime.datetime(2000, 5, 16, 12), cftime.datetime(2000, 6, 16, 0)], - [cftime.datetime(2000, 6, 16, 0), cftime.datetime(2000, 7, 16, 12)], [ - cftime.datetime(2000, 7, 16, 12), - cftime.datetime(2000, 8, 16, 12), + cftime.DatetimeGregorian( + 2000, 5, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 6, 16, 0, 0, 0, 0, has_year_zero=False + ), ], - [cftime.datetime(2000, 8, 16, 12), cftime.datetime(2000, 9, 16, 0)], [ - cftime.datetime(2000, 9, 16, 0), - cftime.datetime(2000, 10, 16, 12), + cftime.DatetimeGregorian( + 2000, 6, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 7, 16, 12, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 10, 16, 12), - cftime.datetime(2000, 11, 16, 0), + cftime.DatetimeGregorian( + 2000, 7, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 8, 16, 12, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 11, 16, 0), - cftime.datetime(2000, 12, 16, 12), + cftime.DatetimeGregorian( + 2000, 8, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 9, 16, 0, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 12, 16, 12), - cftime.datetime(2001, 1, 16, 12), + cftime.DatetimeGregorian( + 2000, 9, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 10, 16, 12, 0, 0, 0, has_year_zero=False + ), ], - [cftime.datetime(2001, 1, 16, 12), cftime.datetime(2001, 2, 15, 0)], - [cftime.datetime(2001, 2, 15, 0), cftime.datetime(2001, 3, 15, 0)], - ] + [ + cftime.DatetimeGregorian( + 2000, 10, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 11, 16, 0, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 11, 16, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 12, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2000, 12, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2001, 1, 16, 12, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 2, 15, 0, 0, 0, 0, has_year_zero=False + ), + ], + [ + cftime.DatetimeGregorian( + 2001, 2, 15, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2001, 3, 15, 0, 0, 0, 0, has_year_zero=False + ), + ], + ], + dtype="object", ), dims=["time", "bnds"], attrs={"xcdat_bounds": "True"}, ) expected.time.attrs = { - "units": "months since 2000-01-01", - "calendar": "standard", "axis": "T", "long_name": "time", "standard_name": "time", @@ -439,7 +660,7 @@ def setup(self): "axis": "T", "long_name": "time", "standard_name": "time", - # calendar attr is specified by test. + # calendar attr and units is specified by test. }, ) time_bnds = xr.DataArray( @@ -508,31 +729,56 @@ def test_decodes_months_with_a_reference_date_at_the_start_of_the_month(self): name="time", data=np.array( [ - cftime.datetime(2000, 2, 1, calendar=calendar), - cftime.datetime(2000, 3, 1, calendar=calendar), - cftime.datetime(2000, 4, 1, calendar=calendar), + cftime.DatetimeGregorian( + 2000, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 4, 1, 0, 0, 0, 0, has_year_zero=False + ), ], + dtype="object", ), dims=["time"], - attrs=ds.time.attrs, + attrs={ + "bounds": "time_bnds", + "axis": "T", + "long_name": "time", + "standard_name": "time", + }, ), "time_bnds": xr.DataArray( name="time_bnds", data=np.array( [ [ - cftime.datetime(2000, 1, 1, calendar=calendar), - cftime.datetime(2000, 2, 1, calendar=calendar), + cftime.DatetimeGregorian( + 2000, 1, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 2, 1, calendar=calendar), - cftime.datetime(2000, 3, 1, calendar=calendar), + cftime.DatetimeGregorian( + 2000, 2, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), ], [ - cftime.datetime(2000, 3, 1, calendar=calendar), - cftime.datetime(2000, 4, 1, calendar=calendar), + cftime.DatetimeGregorian( + 2000, 3, 1, 0, 0, 0, 0, has_year_zero=False + ), + cftime.DatetimeGregorian( + 2000, 4, 1, 0, 0, 0, 0, has_year_zero=False + ), ], ], + dtype="object", ), dims=["time", "bnds"], attrs=ds.time_bnds.attrs, @@ -571,7 +817,12 @@ def test_decodes_months_with_a_reference_date_at_the_middle_of_the_month(self): ], ), dims=["time"], - attrs=ds.time.attrs, + attrs={ + "bounds": "time_bnds", + "axis": "T", + "long_name": "time", + "standard_name": "time", + }, ), "time_bnds": xr.DataArray( name="time_bnds", @@ -628,7 +879,12 @@ def test_decodes_months_with_a_reference_date_at_the_end_of_the_month(self): ], ), dims=["time"], - attrs=ds.time.attrs, + attrs={ + "bounds": "time_bnds", + "axis": "T", + "long_name": "time", + "standard_name": "time", + }, ), "time_bnds": xr.DataArray( name="time_bnds", @@ -686,7 +942,12 @@ def test_decodes_months_with_a_reference_date_on_a_leap_year(self): ], ), dims=["time"], - attrs=ds.time.attrs, + attrs={ + "bounds": "time_bnds", + "axis": "T", + "long_name": "time", + "standard_name": "time", + }, ), "time_bnds": xr.DataArray( name="time_bnds", @@ -745,7 +1006,12 @@ def test_decodes_years_with_a_reference_date_at_the_middle_of_the_year(self): ], ), dims=["time"], - attrs=ds.time.attrs, + attrs={ + "bounds": "time_bnds", + "axis": "T", + "long_name": "time", + "standard_name": "time", + }, ), "time_bnds": xr.DataArray( name="time_bnds", @@ -804,7 +1070,12 @@ def test_decodes_years_with_a_reference_date_on_a_leap_year(self): ], ), dims=["time"], - attrs=ds.time.attrs, + attrs={ + "bounds": "time_bnds", + "axis": "T", + "long_name": "time", + "standard_name": "time", + }, ), "time_bnds": xr.DataArray( name="time_bnds", @@ -1110,8 +1381,6 @@ def callable(ds): dims=["time", "bnds"], ) expected.time.attrs = { - "units": "months since 2000-01-01", - "calendar": "standard", "axis": "T", "long_name": "time", "standard_name": "time", diff --git a/tests/test_temporal.py b/tests/test_temporal.py index af09c47b..67a5fcea 100644 --- a/tests/test_temporal.py +++ b/tests/test_temporal.py @@ -19,6 +19,13 @@ def test_decorator(self): obj = ds.temporal assert obj._dataset.identical(ds) + def test_raises_error_if_calendar_encoding_attr_is_not_set(self): + ds: xr.Dataset = generate_dataset(cf_compliant=True, has_bounds=True) + ds.time.encoding = {} + + with pytest.raises(KeyError): + TemporalAccessor(ds) + class TestAverage: def test_averages_for_yearly_time_series(self): @@ -47,6 +54,7 @@ def test_averages_for_yearly_time_series(self): ), } ) + ds.time.encoding = {"calendar": "standard"} ds["time_bnds"] = xr.DataArray( name="time_bnds", data=np.array( @@ -132,6 +140,8 @@ def test_averages_for_monthly_time_series(self): ), } ) + ds.time.encoding = {"calendar": "standard"} + ds["time_bnds"] = xr.DataArray( name="time_bnds", data=np.array( @@ -215,6 +225,8 @@ def test_averages_for_daily_time_series(self): ), } ) + ds.time.encoding = {"calendar": "standard"} + ds["time_bnds"] = xr.DataArray( name="time_bnds", data=np.array( @@ -298,6 +310,8 @@ def test_averages_for_hourly_time_series(self): ), } ) + ds.time.encoding = {"calendar": "standard"} + ds["time_bnds"] = xr.DataArray( name="time_bnds", data=np.array( @@ -374,6 +388,7 @@ def setup(self): dims=["time"], attrs={"axis": "T", "long_name": "time", "standard_name": "time"}, ) + time.encoding = {"calendar": "standard"} time_bnds = xr.DataArray( name="time_bnds", data=np.array( @@ -420,18 +435,16 @@ def test_weighted_annual_averages(self): "time": xr.DataArray( data=np.array( [ - "2000-01-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ), coords={ "time": np.array( [ - "2000-01-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ) }, dims=["time"], @@ -471,18 +484,16 @@ def test_weighted_annual_averages_with_chunking(self): "time": xr.DataArray( data=np.array( [ - "2000-01-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ), coords={ "time": np.array( [ - "2000-01-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ) }, dims=["time"], @@ -527,12 +538,11 @@ def test_weighted_seasonal_averages_with_DJF_and_drop_incomplete_seasons(self): "time": xr.DataArray( data=np.array( [ - "2000-04-01T00:00:00.000000000", - "2000-07-01T00:00:00.000000000", - "2000-10-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 4, 1), + cftime.DatetimeGregorian(2000, 7, 1), + cftime.DatetimeGregorian(2000, 10, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ), dims=["time"], attrs={ @@ -577,13 +587,12 @@ def test_weighted_seasonal_averages_with_DJF_without_dropping_incomplete_seasons "time": xr.DataArray( data=np.array( [ - "2000-01-01T00:00:00.000000000", - "2000-04-01T00:00:00.000000000", - "2000-07-01T00:00:00.000000000", - "2000-10-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2000, 4, 1), + cftime.DatetimeGregorian(2000, 7, 1), + cftime.DatetimeGregorian(2000, 10, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ), dims=["time"], attrs={ @@ -626,24 +635,22 @@ def test_weighted_seasonal_averages_with_JFD(self): "time": xr.DataArray( data=np.array( [ - "2000-01-01T00:00:00.000000000", - "2000-04-01T00:00:00.000000000", - "2000-07-01T00:00:00.000000000", - "2000-10-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2000, 4, 1), + cftime.DatetimeGregorian(2000, 7, 1), + cftime.DatetimeGregorian(2000, 10, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ), coords={ "time": np.array( [ - "2000-01-01T00:00:00.000000000", - "2000-04-01T00:00:00.000000000", - "2000-07-01T00:00:00.000000000", - "2000-10-01T00:00:00.000000000", - "2001-01-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2000, 4, 1), + cftime.DatetimeGregorian(2000, 7, 1), + cftime.DatetimeGregorian(2000, 10, 1), + cftime.DatetimeGregorian(2001, 1, 1), ], - dtype="datetime64[ns]", ) }, dims=["time"], @@ -692,12 +699,11 @@ def test_weighted_custom_seasonal_averages(self): "time": xr.DataArray( data=np.array( [ - "2000-02-01T00:00:00.000000000", - "2000-05-01T00:00:00.000000000", - "2000-08-01T00:00:00.000000000", - "2001-02-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 2, 1), + cftime.DatetimeGregorian(2000, 5, 1), + cftime.DatetimeGregorian(2000, 8, 1), + cftime.DatetimeGregorian(2001, 2, 1), ], - dtype="datetime64[ns]", ), dims=["time"], attrs={ @@ -783,13 +789,12 @@ def test_weighted_monthly_averages(self): "time": xr.DataArray( data=np.array( [ - "2000-01-01T00:00:00.000000000", - "2000-03-01T00:00:00.000000000", - "2000-06-01T00:00:00.000000000", - "2000-09-01T00:00:00.000000000", - "2001-02-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2000, 3, 1), + cftime.DatetimeGregorian(2000, 6, 1), + cftime.DatetimeGregorian(2000, 9, 1), + cftime.DatetimeGregorian(2001, 2, 1), ], - dtype="datetime64[ns]", ), dims=["time"], attrs={ @@ -833,13 +838,12 @@ def test_weighted_monthly_averages_with_masked_data(self): "time": xr.DataArray( data=np.array( [ - "2000-01-01T00:00:00.000000000", - "2000-03-01T00:00:00.000000000", - "2000-06-01T00:00:00.000000000", - "2000-09-01T00:00:00.000000000", - "2001-02-01T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 1), + cftime.DatetimeGregorian(2000, 3, 1), + cftime.DatetimeGregorian(2000, 6, 1), + cftime.DatetimeGregorian(2000, 9, 1), + cftime.DatetimeGregorian(2001, 2, 1), ], - dtype="datetime64[ns]", ), dims=["time"], attrs={ @@ -876,13 +880,12 @@ def test_weighted_daily_averages(self): "time": xr.DataArray( data=np.array( [ - "2000-01-16T00:00:00.000000000", - "2000-03-16T00:00:00.000000000", - "2000-06-16T00:00:00.000000000", - "2000-09-16T00:00:00.000000000", - "2001-02-15T00:00:00.000000000", + cftime.DatetimeGregorian(2000, 1, 16), + cftime.DatetimeGregorian(2000, 3, 16), + cftime.DatetimeGregorian(2000, 6, 16), + cftime.DatetimeGregorian(2000, 9, 16), + cftime.DatetimeGregorian(2001, 2, 15), ], - dtype="datetime64[ns]", ), dims=["time"], attrs={ @@ -917,7 +920,24 @@ def test_weighted_hourly_averages(self): coords={ "lat": expected.lat, "lon": expected.lon, - "time": ds.time, + "time": xr.DataArray( + data=np.array( + [ + cftime.DatetimeGregorian(2000, 1, 16, 12), + cftime.DatetimeGregorian(2000, 3, 16, 12), + cftime.DatetimeGregorian(2000, 6, 16, 0), + cftime.DatetimeGregorian(2000, 9, 16, 0), + cftime.DatetimeGregorian(2001, 2, 15, 12), + ], + ), + dims=["time"], + attrs={ + "axis": "T", + "long_name": "time", + "standard_name": "time", + "bounds": "time_bnds", + }, + ), }, dims=["time", "lat", "lon"], attrs={ @@ -952,19 +972,19 @@ def test_weighted_seasonal_climatology_with_DJF(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), ], ), }, @@ -1007,19 +1027,19 @@ def test_chunked_weighted_seasonal_climatology_with_DJF(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), ], ), }, @@ -1059,19 +1079,19 @@ def test_weighted_seasonal_climatology_with_JFD(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), ], ), }, @@ -1116,19 +1136,19 @@ def test_weighted_custom_seasonal_climatology(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 2, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 11, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 11, 1), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 2, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 11, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 11, 1), ], ), }, @@ -1169,35 +1189,35 @@ def test_weighted_monthly_climatology(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 3, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 6, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 9, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 11, 1), - cftime.datetime(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 3, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 6, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 9, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 11, 1), + cftime.DatetimeGregorian(1, 12, 1), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 3, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 6, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 9, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 11, 1), - cftime.datetime(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 3, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 6, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 9, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 11, 1), + cftime.DatetimeGregorian(1, 12, 1), ], ), }, @@ -1232,35 +1252,35 @@ def test_unweighted_monthly_climatology(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 3, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 6, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 9, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 11, 1), - cftime.datetime(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 3, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 6, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 9, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 11, 1), + cftime.DatetimeGregorian(1, 12, 1), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 3, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 6, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 9, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 11, 1), - cftime.datetime(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 3, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 6, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 9, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 11, 1), + cftime.DatetimeGregorian(1, 12, 1), ], ), }, @@ -1294,35 +1314,35 @@ def test_weighted_daily_climatology(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 16), - cftime.datetime(1, 2, 15), - cftime.datetime(1, 3, 16), - cftime.datetime(1, 4, 16), - cftime.datetime(1, 5, 16), - cftime.datetime(1, 6, 16), - cftime.datetime(1, 7, 16), - cftime.datetime(1, 8, 16), - cftime.datetime(1, 9, 16), - cftime.datetime(1, 10, 16), - cftime.datetime(1, 11, 16), - cftime.datetime(1, 12, 16), + cftime.DatetimeGregorian(1, 1, 16), + cftime.DatetimeGregorian(1, 2, 15), + cftime.DatetimeGregorian(1, 3, 16), + cftime.DatetimeGregorian(1, 4, 16), + cftime.DatetimeGregorian(1, 5, 16), + cftime.DatetimeGregorian(1, 6, 16), + cftime.DatetimeGregorian(1, 7, 16), + cftime.DatetimeGregorian(1, 8, 16), + cftime.DatetimeGregorian(1, 9, 16), + cftime.DatetimeGregorian(1, 10, 16), + cftime.DatetimeGregorian(1, 11, 16), + cftime.DatetimeGregorian(1, 12, 16), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 16), - cftime.datetime(1, 2, 15), - cftime.datetime(1, 3, 16), - cftime.datetime(1, 4, 16), - cftime.datetime(1, 5, 16), - cftime.datetime(1, 6, 16), - cftime.datetime(1, 7, 16), - cftime.datetime(1, 8, 16), - cftime.datetime(1, 9, 16), - cftime.datetime(1, 10, 16), - cftime.datetime(1, 11, 16), - cftime.datetime(1, 12, 16), + cftime.DatetimeGregorian(1, 1, 16), + cftime.DatetimeGregorian(1, 2, 15), + cftime.DatetimeGregorian(1, 3, 16), + cftime.DatetimeGregorian(1, 4, 16), + cftime.DatetimeGregorian(1, 5, 16), + cftime.DatetimeGregorian(1, 6, 16), + cftime.DatetimeGregorian(1, 7, 16), + cftime.DatetimeGregorian(1, 8, 16), + cftime.DatetimeGregorian(1, 9, 16), + cftime.DatetimeGregorian(1, 10, 16), + cftime.DatetimeGregorian(1, 11, 16), + cftime.DatetimeGregorian(1, 12, 16), ], ), }, @@ -1356,35 +1376,35 @@ def test_unweighted_daily_climatology(self): expected_time = xr.DataArray( data=np.array( [ - cftime.datetime(1, 1, 16), - cftime.datetime(1, 2, 15), - cftime.datetime(1, 3, 16), - cftime.datetime(1, 4, 16), - cftime.datetime(1, 5, 16), - cftime.datetime(1, 6, 16), - cftime.datetime(1, 7, 16), - cftime.datetime(1, 8, 16), - cftime.datetime(1, 9, 16), - cftime.datetime(1, 10, 16), - cftime.datetime(1, 11, 16), - cftime.datetime(1, 12, 16), + cftime.DatetimeGregorian(1, 1, 16), + cftime.DatetimeGregorian(1, 2, 15), + cftime.DatetimeGregorian(1, 3, 16), + cftime.DatetimeGregorian(1, 4, 16), + cftime.DatetimeGregorian(1, 5, 16), + cftime.DatetimeGregorian(1, 6, 16), + cftime.DatetimeGregorian(1, 7, 16), + cftime.DatetimeGregorian(1, 8, 16), + cftime.DatetimeGregorian(1, 9, 16), + cftime.DatetimeGregorian(1, 10, 16), + cftime.DatetimeGregorian(1, 11, 16), + cftime.DatetimeGregorian(1, 12, 16), ], ), coords={ "time": np.array( [ - cftime.datetime(1, 1, 16), - cftime.datetime(1, 2, 15), - cftime.datetime(1, 3, 16), - cftime.datetime(1, 4, 16), - cftime.datetime(1, 5, 16), - cftime.datetime(1, 6, 16), - cftime.datetime(1, 7, 16), - cftime.datetime(1, 8, 16), - cftime.datetime(1, 9, 16), - cftime.datetime(1, 10, 16), - cftime.datetime(1, 11, 16), - cftime.datetime(1, 12, 16), + cftime.DatetimeGregorian(1, 1, 16), + cftime.DatetimeGregorian(1, 2, 15), + cftime.DatetimeGregorian(1, 3, 16), + cftime.DatetimeGregorian(1, 4, 16), + cftime.DatetimeGregorian(1, 5, 16), + cftime.DatetimeGregorian(1, 6, 16), + cftime.DatetimeGregorian(1, 7, 16), + cftime.DatetimeGregorian(1, 8, 16), + cftime.DatetimeGregorian(1, 9, 16), + cftime.DatetimeGregorian(1, 10, 16), + cftime.DatetimeGregorian(1, 11, 16), + cftime.DatetimeGregorian(1, 12, 16), ], ), }, @@ -1652,21 +1672,21 @@ def test_weights_for_monthly_averages(self): name="month", data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 3, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 6, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 9, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 11, 1), - cftime.datetime(1, 12, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 3, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 6, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 9, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 11, 1), + cftime.DatetimeGregorian(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 12, 1), ], ), coords={"time": ds.time}, @@ -1846,6 +1866,7 @@ def test_weights_for_seasonal_averages_with_DJF_and_drop_incomplete_seasons( "bounds": "time_bnds", }, ) + ds.time.encoding = {"calendar": "standard"} ds["ts"] = xr.DataArray( name="ts", data=np.ones((12, 4, 4)), @@ -2242,6 +2263,7 @@ def test_weights_for_seasonal_climatology_with_DJF(self): "bounds": "time_bnds", }, ) + ds.time.encoding = {"calendar": "standard"} ds["ts"] = xr.DataArray( name="ts", data=np.ones((12, 4, 4)), @@ -2319,18 +2341,18 @@ def test_weights_for_seasonal_climatology_with_DJF(self): name="season", data=np.array( [ - cftime.datetime(1, 4, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), ], ), coords={"time": ds.time}, @@ -2375,21 +2397,21 @@ def test_weights_for_seasonal_climatology_with_JFD(self): name="season", data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 1, 1), ], ), coords={"time": ds.time}, @@ -2438,21 +2460,21 @@ def test_weights_for_annual_climatology(self): name="month", data=np.array( [ - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 3, 1), - cftime.datetime(1, 4, 1), - cftime.datetime(1, 5, 1), - cftime.datetime(1, 6, 1), - cftime.datetime(1, 7, 1), - cftime.datetime(1, 8, 1), - cftime.datetime(1, 9, 1), - cftime.datetime(1, 10, 1), - cftime.datetime(1, 11, 1), - cftime.datetime(1, 12, 1), - cftime.datetime(1, 1, 1), - cftime.datetime(1, 2, 1), - cftime.datetime(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 3, 1), + cftime.DatetimeGregorian(1, 4, 1), + cftime.DatetimeGregorian(1, 5, 1), + cftime.DatetimeGregorian(1, 6, 1), + cftime.DatetimeGregorian(1, 7, 1), + cftime.DatetimeGregorian(1, 8, 1), + cftime.DatetimeGregorian(1, 9, 1), + cftime.DatetimeGregorian(1, 10, 1), + cftime.DatetimeGregorian(1, 11, 1), + cftime.DatetimeGregorian(1, 12, 1), + cftime.DatetimeGregorian(1, 1, 1), + cftime.DatetimeGregorian(1, 2, 1), + cftime.DatetimeGregorian(1, 12, 1), ], ), coords={"time": ds.time}, @@ -2500,21 +2522,21 @@ def test_weights_for_daily_climatology(self): name="month_day", data=np.array( [ - cftime.datetime(1, 1, 16), - cftime.datetime(1, 2, 15), - cftime.datetime(1, 3, 16), - cftime.datetime(1, 4, 16), - cftime.datetime(1, 5, 6), - cftime.datetime(1, 6, 16), - cftime.datetime(1, 7, 16), - cftime.datetime(1, 8, 16), - cftime.datetime(1, 9, 16), - cftime.datetime(1, 10, 16), - cftime.datetime(1, 11, 16), - cftime.datetime(1, 12, 16), - cftime.datetime(1, 1, 16), - cftime.datetime(1, 2, 15), - cftime.datetime(1, 12, 16), + cftime.DatetimeGregorian(1, 1, 16), + cftime.DatetimeGregorian(1, 2, 15), + cftime.DatetimeGregorian(1, 3, 16), + cftime.DatetimeGregorian(1, 4, 16), + cftime.DatetimeGregorian(1, 5, 6), + cftime.DatetimeGregorian(1, 6, 16), + cftime.DatetimeGregorian(1, 7, 16), + cftime.DatetimeGregorian(1, 8, 16), + cftime.DatetimeGregorian(1, 9, 16), + cftime.DatetimeGregorian(1, 10, 16), + cftime.DatetimeGregorian(1, 11, 16), + cftime.DatetimeGregorian(1, 12, 16), + cftime.DatetimeGregorian(1, 1, 16), + cftime.DatetimeGregorian(1, 2, 15), + cftime.DatetimeGregorian(1, 12, 16), ], ), coords={"time": ds.time}, diff --git a/xcdat/dataset.py b/xcdat/dataset.py index 141dec61..b778441e 100644 --- a/xcdat/dataset.py +++ b/xcdat/dataset.py @@ -321,24 +321,32 @@ def decode_non_cf_time(dataset: xr.Dataset) -> xr.Dataset: """ ds = dataset.copy() time = get_axis_coord(ds, "T") - - try: - calendar = time.attrs["calendar"] - except KeyError: + time_attrs = time.attrs + + # NOTE: When opening datasets with `decode_times=False`, the "calendar" and + # "units" attributes are stored in `.attrs` (unlike `decode_times=True` + # which stores them in `.encoding`). Since xCDAT manually decodes non-CF + # compliant time coordinates by first setting `decode_times=False`, the + # "calendar" and "units" attrs are popped from the `.attrs` dict and stored + # in the `.encoding` dict to mimic xarray's behavior. + calendar = time_attrs.pop("calendar", None) + units_attr = time_attrs.pop("units", None) + + if calendar is None: logger.warning( "This dataset's time coordinates do not have a 'calendar' attribute set, " "so time coordinates could not be decoded. Set the 'calendar' attribute " - "and try decoding the time coordinates again." + f"(`ds.{time.name}.attrs['calendar]`) and try decoding the time " + "coordinates again." ) return ds - try: - units_attr = time.attrs["units"] - except KeyError: + if units_attr is None: logger.warning( "This dataset's time coordinates do not have a 'units' attribute set, " "so the time coordinates could not be decoded. Set the 'units' attribute " - "and try decoding the time coordinates again." + f"(`ds.{time.name}.attrs['units']`) and try decoding the time " + "coordinates again." ) return ds @@ -346,9 +354,9 @@ def decode_non_cf_time(dataset: xr.Dataset) -> xr.Dataset: units, ref_date = _split_time_units_attr(units_attr) except ValueError: logger.warning( - f"This dataset's 'units' attribute ('{units_attr}') is not in a " - "supported format ('months since...' or 'years since...'), so the time " - "coordinates could not be decoded." + f"This dataset's time coordinates 'units' attribute ('{units_attr}') is " + "not in a supported format ('months since...' or 'years since...'), so the " + "time coordinates could not be decoded." ) return ds @@ -358,12 +366,16 @@ def decode_non_cf_time(dataset: xr.Dataset) -> xr.Dataset: data=data, dims=time.dims, coords={time.name: data}, - attrs=time.attrs, + # As mentioned in a comment above, the units and calendar attributes are + # popped from the `.attrs` dict. + attrs=time_attrs, ) decoded_time.encoding = { "source": ds.encoding.get("source", "None"), "dtype": time.dtype, "original_shape": time.shape, + # The units and calendar attributes are now saved in the `.encoding` + # dict. "units": units_attr, "calendar": calendar, } diff --git a/xcdat/temporal.py b/xcdat/temporal.py index 0b4fee79..9e9579b1 100644 --- a/xcdat/temporal.py +++ b/xcdat/temporal.py @@ -3,11 +3,11 @@ from typing import Dict, List, Literal, Optional, Tuple, TypedDict, get_args import cf_xarray # noqa: F401 -import cftime import numpy as np import pandas as pd import xarray as xr from dask.array.core import Array +from xarray.coding.cftime_offsets import get_date_type from xarray.core.groupby import DataArrayGroupBy from xcdat import bounds # noqa: F401 @@ -150,6 +150,17 @@ def __init__(self, dataset: xr.Dataset): self._time_bounds = self._dataset.bounds.get_bounds("T").copy() + try: + self.calendar = self._dataset[self._dim].encoding["calendar"] + self.date_type = get_date_type(self.calendar) + except KeyError: + raise KeyError( + "This dataset's time coordinates do not have a 'calendar' encoding " + "attribute set, which might indicate that the time coordinates were not " + "decoded to datetime objects. Ensure that the time coordinates are " + "decoded before performing temporal averaging operations." + ) + def average(self, data_var: str, weighted: bool = True, keep_weights: bool = False): """ Returns a Dataset with the average of a data variable and the time @@ -1306,17 +1317,15 @@ def _drop_obsolete_columns(self, df_season: pd.DataFrame) -> pd.DataFrame: return df_season def _convert_df_to_dt(self, df: pd.DataFrame) -> np.ndarray: - """Converts a DataFrame of datetime components to datetime objects. + """ + Converts a DataFrame of datetime components to cftime datetime + objects. datetime objects require at least a year, month, and day value. However, some modes and time frequencies don't require year, month, and/or day for grouping. For these cases, use default values of 1 in order to meet this datetime requirement. - If the default value of 1 is used for the years, datetime objects - must be created using `cftime.datetime` because year 1 is outside the - Timestamp-valid range. - Parameters ---------- df : pd.DataFrame @@ -1325,11 +1334,12 @@ def _convert_df_to_dt(self, df: pd.DataFrame) -> np.ndarray: Returns ------- np.ndarray - A numpy ndarray of datetime.datetime or cftime.datetime objects. + A numpy ndarray of cftime.datetime objects. Notes ----- Refer to [3]_ and [4]_ for more information on Timestamp-valid range. + We use cftime.datetime objects to avoid these time range issues. References ---------- @@ -1344,18 +1354,12 @@ def _convert_df_to_dt(self, df: pd.DataFrame) -> np.ndarray: if component not in df_new.columns: df_new[component] = default_val - year_is_unused = self._mode in ["climatology", "departures"] or ( - self._mode == "average" and self._freq != "year" - ) - if year_is_unused: - dates = [ - cftime.datetime(year, month, day, hour) - for year, month, day, hour in zip( - df_new.year, df_new.month, df_new.day, df_new.hour - ) - ] - else: - dates = pd.to_datetime(df_new) + dates = [ + self.date_type(year, month, day, hour) + for year, month, day, hour in zip( + df_new.year, df_new.month, df_new.day, df_new.hour + ) + ] return np.array(dates)