Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 31 additions & 37 deletions lib/iris/analysis/_area_weighted.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,40 +47,31 @@ 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):
msg = "Value for mdtol must be in range 0 - 1, got {}."
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):
"""
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -30,42 +31,53 @@ 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alter this so that we are passing data through the tests. It is ok to do this because all the tests already using it abort with an error and are independent of the data used.


def extract_grid(self, cube):
return cube.coord("latitude"), cube.coord("longitude")

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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -193,37 +199,51 @@ 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
)
# 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__":
Expand Down