diff --git a/esmvalcore/preprocessor/_multimodel.py b/esmvalcore/preprocessor/_multimodel.py index 9ea3798caf..a7e68143c4 100644 --- a/esmvalcore/preprocessor/_multimodel.py +++ b/esmvalcore/preprocessor/_multimodel.py @@ -20,6 +20,8 @@ import iris import numpy as np +from ._time import regrid_time + logger = logging.getLogger(__name__) @@ -109,10 +111,12 @@ def _put_in_cube(template_cube, cube_data, statistic, t_axis): if t_axis is None: times = template_cube.coord('time') else: + unit_name = template_cube.coord('time').units.name + tunits = cf_units.Unit(unit_name, calendar="standard") times = iris.coords.DimCoord( t_axis, standard_name='time', - units=template_cube.coord('time').units) + units=tunits) coord_names = [c.long_name for c in template_cube.coords()] coord_names.extend([c.standard_name for c in template_cube.coords()]) @@ -164,9 +168,8 @@ def _put_in_cube(template_cube, cube_data, statistic, t_axis): def _datetime_to_int_days(cube): """Return list of int(days) converted from cube datetime cells.""" + cube = _align_yearly_axes(cube) time_cells = [cell.point for cell in cube.coord('time').cells()] - time_unit = cube.coord('time').units.name - time_offset = _get_time_offset(time_unit) # extract date info real_dates = [] @@ -174,15 +177,26 @@ def _datetime_to_int_days(cube): # real_date resets the actual data point day # to the 1st of the month so that there are no # wrong overlap indices - # NOTE: this workaround is good only - # for monthly data real_date = datetime(date_obj.year, date_obj.month, 1, 0, 0, 0) real_dates.append(real_date) + # get the number of days starting from the reference unit + time_unit = cube.coord('time').units.name + time_offset = _get_time_offset(time_unit) days = [(date_obj - time_offset).days for date_obj in real_dates] + return days +def _align_yearly_axes(cube): + """Perform a time-regridding operation to align time axes for yr data.""" + years = [cell.point.year for cell in cube.coord('time').cells()] + # be extra sure that the first point is not in the previous year + if 0 not in np.diff(years): + return regrid_time(cube, 'yr') + return cube + + def _get_overlap(cubes): """ Get discrete time overlaps. diff --git a/tests/unit/preprocessor/_multimodel/test_multimodel.py b/tests/unit/preprocessor/_multimodel/test_multimodel.py index d8d440d51a..4cb7533d49 100644 --- a/tests/unit/preprocessor/_multimodel/test_multimodel.py +++ b/tests/unit/preprocessor/_multimodel/test_multimodel.py @@ -48,7 +48,22 @@ def setUp(self): units=Unit( 'days since 1950-01-01', calendar='gregorian')) - + yr_time = iris.coords.DimCoord([15, 410], + standard_name='time', + bounds=[[1., 30.], [395., 425.]], + units=Unit( + 'days since 1950-01-01', + calendar='gregorian')) + yr_time2 = iris.coords.DimCoord([1., 367., 733., 1099.], + standard_name='time', + bounds=[ + [0.5, 1.5], + [366, 368], + [732, 734], + [1098, 1100], ], + units=Unit( + 'days since 1950-01-01', + calendar='gregorian')) zcoord = iris.coords.DimCoord([0.5, 5., 50.], standard_name='air_pressure', long_name='air_pressure', @@ -75,6 +90,14 @@ def setUp(self): coords_spec5 = [(time2, 0), (zcoord, 1), (lats, 2), (lons, 3)] self.cube2 = iris.cube.Cube(data3, dim_coords_and_dims=coords_spec5) + coords_spec4_yr = [(yr_time, 0), (zcoord, 1), (lats, 2), (lons, 3)] + self.cube1_yr = iris.cube.Cube(data2, + dim_coords_and_dims=coords_spec4_yr) + + coords_spec5_yr = [(yr_time2, 0), (zcoord, 1), (lats, 2), (lons, 3)] + self.cube2_yr = iris.cube.Cube(data3, + dim_coords_and_dims=coords_spec5_yr) + def test_get_time_offset(self): """Test time unit.""" result = _get_time_offset("days since 1950-01-01") @@ -91,7 +114,7 @@ def test_compute_statistic(self): self.assert_array_equal(stat_mean, expected_mean) self.assert_array_equal(stat_median, expected_median) - def test_compute_full_statistic_cube(self): + def test_compute_full_statistic_mon_cube(self): data = [self.cube1, self.cube2] stats = multi_model_statistics(data, 'full', ['mean']) expected_full_mean = np.ma.ones((2, 3, 2, 2)) @@ -99,12 +122,26 @@ def test_compute_full_statistic_cube(self): expected_full_mean.mask[1] = True self.assert_array_equal(stats['mean'].data, expected_full_mean) - def test_compute_overlap_statistic_cube(self): + def test_compute_full_statistic_yr_cube(self): + data = [self.cube1_yr, self.cube2_yr] + stats = multi_model_statistics(data, 'full', ['mean']) + expected_full_mean = np.ma.ones((4, 3, 2, 2)) + expected_full_mean.mask = np.zeros((4, 3, 2, 2)) + expected_full_mean.mask[2:4] = True + self.assert_array_equal(stats['mean'].data, expected_full_mean) + + def test_compute_overlap_statistic_mon_cube(self): data = [self.cube1, self.cube1] stats = multi_model_statistics(data, 'overlap', ['mean']) expected_ovlap_mean = np.ma.ones((2, 3, 2, 2)) self.assert_array_equal(stats['mean'].data, expected_ovlap_mean) + def test_compute_overlap_statistic_yr_cube(self): + data = [self.cube1_yr, self.cube1_yr] + stats = multi_model_statistics(data, 'overlap', ['mean']) + expected_ovlap_mean = np.ma.ones((2, 3, 2, 2)) + self.assert_array_equal(stats['mean'].data, expected_ovlap_mean) + def test_compute_std(self): """Test statistic.""" data = [self.cube1.data[0], self.cube2.data[0] * 2] @@ -134,7 +171,7 @@ def test_put_in_cube(self): stat_cube = _put_in_cube(self.cube1, cube_data, "mean", t_axis=None) self.assert_array_equal(stat_cube.data, self.cube1.data) - def test_datetime_to_int_days(self): + def test_datetime_to_int_days_no_overlap(self): """Test _datetime_to_int_days.""" computed_dats = _datetime_to_int_days(self.cube1) expected_dats = [0, 31]