From e52d6e10eba4f64895d632d559eedce55133c587 Mon Sep 17 00:00:00 2001 From: Anna Booton Date: Fri, 27 Dec 2019 16:02:55 +0000 Subject: [PATCH 1/2] Update area weighted regridder to accept data with dims in different orders --- lib/iris/analysis/_area_weighted.py | 68 +++++---- .../test_AreaWeightedRegridder.py | 130 ++++++++++-------- 2 files changed, 100 insertions(+), 98 deletions(-) diff --git a/lib/iris/analysis/_area_weighted.py b/lib/iris/analysis/_area_weighted.py index a6ab8586cb..06f44dc951 100644 --- a/lib/iris/analysis/_area_weighted.py +++ b/lib/iris/analysis/_area_weighted.py @@ -47,15 +47,9 @@ def __init__(self, src_grid_cube, target_grid_cube, mdtol=1): the same cooordinate system. """ - # Snapshot the state of the cubes to ensure that the regridder is + # Snapshot the state of the source cube to ensure that the regridder is # impervious to external changes to the original cubes. self._src_grid = snapshot_grid(src_grid_cube) - self._target_grid = snapshot_grid(target_grid_cube) - - # Store the x_dim and y_dim of the source cube. - x, y = get_xy_dim_coords(src_grid_cube) - self._src_x_dim = src_grid_cube.coord_dims(x) - self._src_y_dim = src_grid_cube.coord_dims(y) # Missing data tolerance. if not (0 <= mdtol <= 1): @@ -63,24 +57,21 @@ def __init__(self, src_grid_cube, target_grid_cube, mdtol=1): raise ValueError(msg.format(mdtol)) self._mdtol = mdtol - # The need for an actual Cube is an implementation quirk caused by the - # current usage of the experimental regrid function. - self._target_grid_cube_cache = None - - self._regrid_info = eregrid._regrid_area_weighted_rectilinear_src_and_grid__prepare( - src_grid_cube, self._target_grid_cube + # Store regridding information + _regrid_info = eregrid._regrid_area_weighted_rectilinear_src_and_grid__prepare( + src_grid_cube, target_grid_cube ) - - @property - def _target_grid_cube(self): - if self._target_grid_cube_cache is None: - x, y = self._target_grid - data = np.empty((y.points.size, x.points.size)) - cube = iris.cube.Cube(data) - cube.add_dim_coord(y, 0) - cube.add_dim_coord(x, 1) - self._target_grid_cube_cache = cube - return self._target_grid_cube_cache + ( + src_x, + src_y, + src_x_dim, + src_y_dim, + self.grid_x, + self.grid_y, + self.meshgrid_x, + self.meshgrid_y, + self.weights_info, + ) = _regrid_info def __call__(self, cube): """ @@ -102,22 +93,25 @@ def __call__(self, cube): area-weighted regridding. """ - if get_xy_dim_coords(cube) != self._src_grid or not ( - _xy_data_dims_are_equal(cube, self._src_x_dim, self._src_y_dim) - ): + src_x, src_y = get_xy_dim_coords(cube) + if (src_x, src_y) != self._src_grid: raise ValueError( "The given cube is not defined on the same " "source grid as this regridder." ) + src_x_dim = cube.coord_dims(src_x)[0] + src_y_dim = cube.coord_dims(src_y)[0] + _regrid_info = ( + src_x, + src_y, + src_x_dim, + src_y_dim, + self.grid_x, + self.grid_y, + self.meshgrid_x, + self.meshgrid_y, + self.weights_info, + ) return eregrid._regrid_area_weighted_rectilinear_src_and_grid__perform( - cube, self._regrid_info, mdtol=self._mdtol + cube, _regrid_info, mdtol=self._mdtol ) - - -def _xy_data_dims_are_equal(cube, x_dim, y_dim): - """ - Return whether the data dimensions of the x and y coordinates on the - the cube are equal to the values ``x_dim`` and ``y_dim``, respectively. - """ - x1, y1 = get_xy_dim_coords(cube) - return cube.coord_dims(x1) == x_dim and cube.coord_dims(y1) == y_dim diff --git a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py index 1e494de259..bd39c0d1f4 100644 --- a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py +++ b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py @@ -11,6 +11,7 @@ # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests +import iris.experimental.regrid as eregrid from unittest import mock @@ -30,11 +31,13 @@ def cube(self, x, y): lon = DimCoord(x, "longitude", units="degrees") cube.add_dim_coord(lat, 0) cube.add_dim_coord(lon, 1) + cube.coord("latitude").guess_bounds() + cube.coord("longitude").guess_bounds() return cube def grids(self): src = self.cube(np.linspace(20, 30, 3), np.linspace(10, 25, 4)) - target = self.cube(np.linspace(6, 18, 8), np.linspace(11, 22, 9)) + target = self.cube(np.linspace(22, 28, 8), np.linspace(11, 22, 9)) return src, target def extract_grid(self, cube): @@ -42,30 +45,39 @@ def extract_grid(self, cube): def check_mdtol(self, mdtol=None): src_grid, target_grid = self.grids() + # Get _regrid_info result + _regrid_info = eregrid._regrid_area_weighted_rectilinear_src_and_grid__prepare( + src_grid, target_grid + ) + self.assertEqual(len(_regrid_info), 9) with mock.patch( "iris.experimental.regrid." - "_regrid_area_weighted_rectilinear_src_and_grid__prepare" + "_regrid_area_weighted_rectilinear_src_and_grid__prepare", + return_value=_regrid_info, ) as prepare: - if mdtol is None: - regridder = AreaWeightedRegridder(src_grid, target_grid) - mdtol = 1 - else: - regridder = AreaWeightedRegridder( - src_grid, target_grid, mdtol=mdtol - ) - - # Make a new cube to regrid with different data so we can - # distinguish between regridding the original src grid - # definition cube and the cube passed to the regridder. - src = src_grid.copy() - src.data += 10 - - with mock.patch( - "iris.experimental.regrid." - "_regrid_area_weighted_rectilinear_src_and_grid__perform", - return_value=mock.sentinel.result, - ) as perform: - result = regridder(src) + with mock.patch( + "iris.experimental.regrid." + "_regrid_area_weighted_rectilinear_src_and_grid__perform", + return_value=mock.sentinel.result, + ) as perform: + # Setup the regridder + if mdtol is None: + regridder = AreaWeightedRegridder(src_grid, target_grid) + mdtol = 1 + else: + regridder = AreaWeightedRegridder( + src_grid, target_grid, mdtol=mdtol + ) + # Now regrid the source cube + src = src_grid + result = regridder(src) + + # Make a new cube to regrid with different data so we can + # distinguish between regridding the original src grid + # definition cube and the cube passed to the regridder. + src = src_grid.copy() + src.data += 10 + result = regridder(src) # Prepare: self.assertEqual(prepare.call_count, 1) @@ -75,8 +87,8 @@ def check_mdtol(self, mdtol=None): ) # Perform: - self.assertEqual(perform.call_count, 1) - _, args, kwargs = perform.mock_calls[0] + self.assertEqual(perform.call_count, 2) + _, args, kwargs = perform.mock_calls[1] self.assertEqual(args[0], src) self.assertEqual(kwargs, {"mdtol": mdtol}) self.assertIs(result, mock.sentinel.result) @@ -113,9 +125,6 @@ def test_mismatched_src_coord_systems(self): def test_src_and_target_are_the_same(self): src = self.cube(np.linspace(20, 30, 3), np.linspace(10, 25, 4)) target = self.cube(np.linspace(20, 30, 3), np.linspace(10, 25, 4)) - for name in ["latitude", "longitude"]: - src.coord(name).guess_bounds() - target.coord(name).guess_bounds() regridder = AreaWeightedRegridder(src, target) result = regridder(src) self.assertArrayAllClose(result.data, target.data) @@ -132,9 +141,6 @@ def test_multiple_src_on_same_grid(self): src1.coord(name).units = None src2.coord(name).coord_system = None src2.coord(name).units = None - # Ensure contiguous bounds exists. - src1.coord(name).guess_bounds() - src2.coord(name).guess_bounds() target = self.cube(np.linspace(20, 32, 2), np.linspace(10, 22, 2)) # Ensure the bounds of the target cover the same range as the @@ -193,37 +199,39 @@ def test_multiple_src_on_same_grid(self): self.assertEqual(result1, reference1) self.assertEqual(result2, reference2) - def test_mismatched_data_dims(self): - coord_names = ["latitude", "longitude"] - x = np.linspace(20, 32, 4) - y = np.linspace(10, 22, 4) - src1 = self.cube(x, y) - - data = np.arange(len(y) * len(x)).reshape(len(x), len(y)) - src2 = Cube(data) - lat = DimCoord(y, "latitude", units="degrees") - lon = DimCoord(x, "longitude", units="degrees") - # Add dim coords in opposite order to self.cube. - src2.add_dim_coord(lat, 1) - src2.add_dim_coord(lon, 0) - for name in coord_names: - # Ensure contiguous bounds exists. - src1.coord(name).guess_bounds() - src2.coord(name).guess_bounds() - - target = self.cube(np.linspace(20, 32, 2), np.linspace(10, 22, 2)) - for name in coord_names: - # Ensure the bounds of the target cover the same range as the - # source. - target.coord(name).bounds = np.column_stack( - ( - src1.coord(name).bounds[[0, 1], [0, 1]], - src1.coord(name).bounds[[2, 3], [0, 1]], - ) - ) - - regridder = AreaWeightedRegridder(src1, target) - self.assertRaises(ValueError, regridder, src2) + def test_src_data_different_dims(self): + src, target = self.grids() + regridder = AreaWeightedRegridder(src, target) + result = regridder(src) + expected_mean, expected_std = 4.772097735195653, 2.211698479817678 + self.assertArrayShapeStats(result, (9, 8), expected_mean, expected_std) + # New source cube with additional "levels" dimension + # Each level has identical x-y data so the mean and std stats remain + # identical when x, y and z dims are reordered + levels = DimCoord(np.arange(5), "model_level_number") + lat = src.coord("latitude") + lon = src.coord("longitude") + data = np.repeat(src.data[np.newaxis, ...], 5, axis=0) + src = Cube(data) + src.add_dim_coord(levels, 0) + src.add_dim_coord(lat, 1) + src.add_dim_coord(lon, 2) + result = regridder(src) + self.assertArrayShapeStats( + result, (5, 9, 8), expected_mean, expected_std + ) + # Check data with dims in different order + # Reshape src so that the coords are ordered [x, z, y], + # the mean and std statistics should be the same + data = np.moveaxis(src.data.copy(), 2, 0) + src = Cube(data) + src.add_dim_coord(lon, 0) + src.add_dim_coord(levels, 1) + src.add_dim_coord(lat, 2) + result = regridder(src) + self.assertArrayShapeStats( + result, (8, 5, 9), expected_mean, expected_std + ) if __name__ == "__main__": From c2ad889259b89341b6121754db4296252f471c91 Mon Sep 17 00:00:00 2001 From: Anna Booton Date: Sun, 5 Jan 2020 19:43:39 +0000 Subject: [PATCH 2/2] Add extra unittest to test_AreaWeightedRegridder --- .../area_weighted/test_AreaWeightedRegridder.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py index bd39c0d1f4..18312737fe 100644 --- a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py +++ b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py @@ -232,6 +232,18 @@ def test_src_data_different_dims(self): self.assertArrayShapeStats( result, (8, 5, 9), expected_mean, expected_std ) + # Check data with dims in different order + # Reshape src so that the coords are ordered [y, x, z], + # the mean and std statistics should be the same + data = np.moveaxis(src.data.copy(), 2, 0) + src = Cube(data) + src.add_dim_coord(lat, 0) + src.add_dim_coord(lon, 1) + src.add_dim_coord(levels, 2) + result = regridder(src) + self.assertArrayShapeStats( + result, (9, 8, 5), expected_mean, expected_std + ) if __name__ == "__main__":