From eecbc91b070c939d330bc40800b1975e5dc63c65 Mon Sep 17 00:00:00 2001 From: stephenworsley <49274989+stephenworsley@users.noreply.github.com> Date: Tue, 27 Jul 2021 16:38:58 +0100 Subject: [PATCH] Add grid to mesh scheme (#96) * add grid to mesh regridder * add tests * add tests * lint fixes * update comments * extend tests with extra dimensions * add multidimensional coords to tests * fix test * fix test * fix test * address some review comments * add error handling and test * lint fix * test mask handling * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix test * lint fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../experimental/unstructured_scheme.py | 252 +++++++++++++++++- .../test_GridToMeshESMFRegridder.py | 198 ++++++++++++++ ...test_regrid_rectilinear_to_unstructured.py | 164 ++++++++++++ 3 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py create mode 100644 esmf_regrid/tests/unit/experimental/unstructured_scheme/test_regrid_rectilinear_to_unstructured.py diff --git a/esmf_regrid/experimental/unstructured_scheme.py b/esmf_regrid/experimental/unstructured_scheme.py index ad3b2128..737227fd 100644 --- a/esmf_regrid/experimental/unstructured_scheme.py +++ b/esmf_regrid/experimental/unstructured_scheme.py @@ -86,7 +86,7 @@ def _create_cube(data, src_cube, mesh_dim, grid_x, grid_y): The regridded data as an N-dimensional NumPy array. src_cube : cube The source Cube. - mes_dim : int + mesh_dim : int The dimension of the mesh within the source Cube. grid_x : DimCoord The :class:`iris.coords.DimCoord` for the new grid's X @@ -187,7 +187,7 @@ def _regrid_unstructured_to_rectilinear__perform(src_cube, regrid_info, mdtol): """ Second (regrid) part of 'regrid_unstructured_to_rectilinear'. - Perform the prepared regrid calculation on a single 2d cube. + Perform the prepared regrid calculation on a single cube. """ mesh_dim, grid_x, grid_y, regridder = regrid_info @@ -324,3 +324,251 @@ def __call__(self, cube): return _regrid_unstructured_to_rectilinear__perform( cube, regrid_info, self.mdtol ) + + +def _regrid_along_grid_dims(regridder, data, grid_x_dim, grid_y_dim, mdtol): + # The mesh will be assigned to the first dimension associated with the + # grid, whether that is associated with the x or y coordinate. + tgt_mesh_dim = min(grid_x_dim, grid_y_dim) + data = np.moveaxis(data, [grid_x_dim, grid_y_dim], [-1, -2]) + result = regridder.regrid(data, mdtol=mdtol) + + result = np.moveaxis(result, -1, tgt_mesh_dim) + return result + + +def _create_mesh_cube(data, src_cube, grid_x_dim, grid_y_dim, mesh): + """ + Return a new cube for the result of regridding. + + Returned cube represents the result of regridding the source cube + onto the new mesh. + All the metadata and coordinates of the result cube are copied from + the source cube, with two exceptions: + - Grid dimension coordinates are copied from the grid cube. + - Auxiliary coordinates which span the mesh dimension are + ignored. + + Parameters + ---------- + data : array + The regridded data as an N-dimensional NumPy array. + src_cube : cube + The source Cube. + grid_x_dim : int + The dimension of the x dimension on the source Cube. + grid_y_dim : int + The dimension of the y dimension on the source Cube. + mesh : Mesh + The :class:`iris.experimental.ugrid.Mesh` for the new + Cube. + + Returns + ------- + cube + A new iris.cube.Cube instance. + + """ + new_cube = iris.cube.Cube(data) + + min_grid_dim = min(grid_x_dim, grid_y_dim) + max_grid_dim = max(grid_x_dim, grid_y_dim) + for coord in mesh.to_MeshCoords("face"): + new_cube.add_aux_coord(coord, min_grid_dim) + + new_cube.metadata = copy.deepcopy(src_cube.metadata) + + def copy_coords(src_coords, add_method): + for coord in src_coords: + dims = src_cube.coord_dims(coord) + if grid_x_dim in dims or grid_y_dim in dims: + continue + # Since the 2D grid will be replaced by a 1D mesh, dims which are + # beyond the max_grid_dim are decreased by one. + dims = [dim if dim < max_grid_dim else dim - 1 for dim in dims] + result_coord = coord.copy() + # Add result_coord to the owner of add_method. + add_method(result_coord, dims) + + copy_coords(src_cube.dim_coords, new_cube.add_dim_coord) + copy_coords(src_cube.aux_coords, new_cube.add_aux_coord) + + return new_cube + + +def _regrid_rectilinear_to_unstructured__prepare(src_grid_cube, target_mesh_cube): + """ + First (setup) part of 'regrid_rectilinear_to_unstructured'. + + Check inputs and calculate the sparse regrid matrix and related info. + The 'regrid info' returned can be re-used over many 2d slices. + + """ + grid_x, grid_y = get_xy_dim_coords(src_grid_cube) + mesh = target_mesh_cube.mesh + # TODO: Improve the checking of mesh validity. Check the mesh location and + # raise appropriate error messages. + assert mesh is not None + grid_x_dim = src_grid_cube.coord_dims(grid_x)[0] + grid_y_dim = src_grid_cube.coord_dims(grid_y)[0] + + meshinfo = _mesh_to_MeshInfo(mesh) + gridinfo = _cube_to_GridInfo(src_grid_cube) + + regridder = Regridder(gridinfo, meshinfo) + + regrid_info = (grid_x_dim, grid_y_dim, grid_x, grid_y, mesh, regridder) + + return regrid_info + + +def _regrid_rectilinear_to_unstructured__perform(src_cube, regrid_info, mdtol): + """ + Second (regrid) part of 'regrid_rectilinear_to_unstructured'. + + Perform the prepared regrid calculation on a single cube. + + """ + grid_x_dim, grid_y_dim, grid_x, grid_y, mesh, regridder = regrid_info + + # Perform regridding with realised data for the moment. This may be changed + # in future to handle src_cube.lazy_data. + new_data = _regrid_along_grid_dims( + regridder, src_cube.data, grid_x_dim, grid_y_dim, mdtol + ) + + new_cube = _create_mesh_cube( + new_data, + src_cube, + grid_x_dim, + grid_y_dim, + mesh, + ) + return new_cube + + +def regrid_rectilinear_to_unstructured(src_cube, mesh_cube, mdtol=0): + """ + Regrid rectilinear cube onto unstructured mesh. + + Return a new cube with data values calculated using the area weighted + mean of data values from rectilinear cube src_cube regridded onto the + horizontal mesh of mesh_cube. The dimensions on the cube associated + with the grid will replaced by a dimension associated with the mesh. + That dimension will be the the first of the grid dimensions, whether + it is associated with the x or y coordinate. Since two dimensions are + being replaced by one, coordinates associated with dimensions after + the grid will become associated with dimensions one lower. + This function requires that the horizontal dimension of mesh_cube is + described by a 2D mesh with data located on the faces of that mesh. + This function requires that the horizontal grid of src_cube is + rectilinear (i.e. expressed in terms of two orthogonal 1D coordinates). + This function also requires that the coordinates describing the + horizontal grid have bounds. + + Parameters + ---------- + src_cube : cube + A rectilinear instance of iris.cube.Cube that supplies the data, + metadata and coordinates. + mesh_cube : cube + An unstructured instance of iris.cube.Cube that supplies the desired + horizontal mesh definition. + mdtol : float, optional + Tolerance of missing data. The value returned in each element of the + returned cube's data array will be masked if the fraction of masked + data in the overlapping cells of the source cube exceeds mdtol. This + fraction is calculated based on the area of masked cells within each + target cell. mdtol=0 means no missing data is tolerated while mdtol=1 + will mean the resulting element will be masked if and only if all the + overlapping cells of the source cube are masked. Defaults to 0. + + Returns + ------- + cube + A new iris.cube.Cube instance. + + """ + regrid_info = _regrid_rectilinear_to_unstructured__prepare(src_cube, mesh_cube) + result = _regrid_rectilinear_to_unstructured__perform(src_cube, regrid_info, mdtol) + return result + + +class GridToMeshESMFRegridder: + """Regridder class for rectilinear to unstructured cubes.""" + + def __init__(self, src_mesh_cube, target_grid_cube, mdtol=1): + """ + Create regridder for conversions between source grid and target mesh. + + Parameters + ---------- + src_grid_cube : cube + The unstructured iris cube providing the source grid. + target_grid_cube : cube + The rectilinear iris cube providing the target mesh. + mdtol : float, optional + Tolerance of missing data. The value returned in each element of + the returned array will be masked if the fraction of masked data + exceeds mdtol. mdtol=0 means no missing data is tolerated while + mdtol=1 will mean the resulting element will be masked if and only + if all the contributing elements of data are masked. + Defaults to 1. + + """ + # Missing data tolerance. + # Code directly copied from iris. + if not (0 <= mdtol <= 1): + msg = "Value for mdtol must be in range 0 - 1, got {}." + raise ValueError(msg.format(mdtol)) + self.mdtol = mdtol + + partial_regrid_info = _regrid_rectilinear_to_unstructured__prepare( + src_mesh_cube, target_grid_cube + ) + + # Store regrid info. + _, _, self.grid_x, self.grid_y, self.mesh, self.regridder = partial_regrid_info + + def __call__(self, cube): + """ + Regrid this cube onto the target mesh of this regridder instance. + + The given cube must be defined with the same grid as the source + cube used to create this MeshToGridESMFRegridder instance. + + Parameters + ---------- + cube : cube + A iris.cube.Cube instance to be regridded. + + Returns + ------- + A cube defined with the horizontal dimensions of the target + and the other dimensions from this cube. The data values of + this cube will be converted to values on the new grid using + area-weighted regridding via ESMF generated weights. + + """ + grid_x, grid_y = get_xy_dim_coords(cube) + if (grid_x != self.grid_x) or (grid_y != self.grid_y): + raise ValueError( + "The given cube is not defined on the same " + "source grid as this regridder." + ) + + grid_x_dim = cube.coord_dims(grid_x)[0] + grid_y_dim = cube.coord_dims(grid_y)[0] + + regrid_info = ( + grid_x_dim, + grid_y_dim, + self.grid_x, + self.grid_y, + self.mesh, + self.regridder, + ) + + return _regrid_rectilinear_to_unstructured__perform( + cube, regrid_info, self.mdtol + ) diff --git a/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py b/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py new file mode 100644 index 00000000..7bb756ad --- /dev/null +++ b/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py @@ -0,0 +1,198 @@ +"""Unit tests for :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`.""" + +from iris.coords import AuxCoord, DimCoord +from iris.cube import Cube +import numpy as np +from numpy import ma +import pytest + +from esmf_regrid.experimental.unstructured_scheme import ( + GridToMeshESMFRegridder, +) +from esmf_regrid.tests.unit.experimental.unstructured_scheme.test__cube_to_GridInfo import ( + _grid_cube, +) +from esmf_regrid.tests.unit.experimental.unstructured_scheme.test__regrid_unstructured_to_rectilinear__prepare import ( + _flat_mesh_cube, +) + + +def test_flat_cubes(): + """ + Basic test for :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`. + + Tests with flat cubes as input (a 2D grid cube and a 1D mesh cube). + """ + tgt = _flat_mesh_cube() + + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + # Ensure data in the target grid is different to the expected data. + # i.e. target grid data is all zero, expected data is all one + tgt.data[:] = 0 + + def _add_metadata(cube): + result = cube.copy() + result.units = "K" + result.attributes = {"a": 1} + result.standard_name = "air_temperature" + scalar_height = AuxCoord([5], units="m", standard_name="height") + scalar_time = DimCoord([10], units="s", standard_name="time") + result.add_aux_coord(scalar_height) + result.add_aux_coord(scalar_time) + return result + + src = _add_metadata(src) + src.data[:] = 1 # Ensure all data in the source is one. + regridder = GridToMeshESMFRegridder(src, tgt) + result = regridder(src) + src_T = src.copy() + src_T.transpose() + result_transposed = regridder(src_T) + + expected_data = np.ones([n_lats, n_lons]) + expected_cube = _add_metadata(tgt) + + # Lenient check for data. + assert np.allclose(expected_data, result.data) + assert np.allclose(expected_data, result_transposed.data) + + # Check metadata and scalar coords. + expected_cube.data = result.data + assert expected_cube == result + expected_cube.data = result_transposed.data + assert expected_cube == result_transposed + + +def test_multidim_cubes(): + """ + Test for :func:`esmf_regrid.experimental.unstructured_scheme.regrid_rectilinear_to_unstructured`. + + Tests with multidimensional cubes. The source cube contains + coordinates on the dimensions before and after the grid dimensions. + """ + tgt = _flat_mesh_cube() + mesh = tgt.mesh + mesh_length = mesh.connectivity(contains_face=True).shape[0] + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + grid = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + h = 2 + t = 3 + height = DimCoord(np.arange(h), standard_name="height") + time = DimCoord(np.arange(t), standard_name="time") + + src_data = np.empty([t, n_lats, n_lons, h]) + src_data[:] = np.arange(t * h).reshape([t, h])[:, np.newaxis, np.newaxis, :] + cube = Cube(src_data) + cube.add_dim_coord(grid.coord("latitude"), 1) + cube.add_dim_coord(grid.coord("longitude"), 2) + cube.add_dim_coord(time, 0) + cube.add_dim_coord(height, 3) + + regridder = GridToMeshESMFRegridder(grid, tgt) + result = regridder(cube) + + # Lenient check for data. + expected_data = np.empty([t, mesh_length, h]) + expected_data[:] = np.arange(t * h).reshape(t, h)[:, np.newaxis, :] + assert np.allclose(expected_data, result.data) + + mesh_coord_x, mesh_coord_y = mesh.to_MeshCoords("face") + expected_cube = Cube(expected_data) + expected_cube.add_dim_coord(time, 0) + expected_cube.add_aux_coord(mesh_coord_x, 1) + expected_cube.add_aux_coord(mesh_coord_y, 1) + expected_cube.add_dim_coord(height, 2) + + # Check metadata and scalar coords. + result.data = expected_data + assert expected_cube == result + + +def test_invalid_mdtol(): + """ + Test initialisation of :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`. + + Checks that an error is raised when mdtol is out of range. + """ + tgt = _flat_mesh_cube() + + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + with pytest.raises(ValueError): + _ = GridToMeshESMFRegridder(src, tgt, mdtol=2) + with pytest.raises(ValueError): + _ = GridToMeshESMFRegridder(src, tgt, mdtol=-1) + + +def test_mismatched_grids(): + """ + Test error handling in calling of :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`. + + Checks that an error is raised when the regridder is called with a + cube whose grid does not match with the one used when initialising + the regridder. + """ + tgt = _flat_mesh_cube() + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + regridder = GridToMeshESMFRegridder(src, tgt) + + n_lons_other = 3 + n_lats_other = 10 + src_other = _grid_cube( + n_lons_other, n_lats_other, lon_bounds, lat_bounds, circular=True + ) + with pytest.raises(ValueError): + _ = regridder(src_other) + + +def test_mask_handling(): + """ + Test masked data handling for :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`. + + Tests masked data handling for multiple valid values for mdtol. + """ + tgt = _flat_mesh_cube() + + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + data = np.ones([n_lats, n_lons]) + mask = np.zeros([n_lats, n_lons]) + mask[0, 0] = 1 + masked_data = ma.array(data, mask=mask) + src.data = masked_data + regridder_0 = GridToMeshESMFRegridder(src, tgt, mdtol=0) + regridder_05 = GridToMeshESMFRegridder(src, tgt, mdtol=0.05) + regridder_1 = GridToMeshESMFRegridder(src, tgt, mdtol=1) + result_0 = regridder_0(src) + result_05 = regridder_05(src) + result_1 = regridder_1(src) + + expected_data = np.ones(tgt.shape) + expected_0 = ma.array(expected_data) + expected_05 = ma.array(expected_data, mask=[0, 0, 1, 0, 0, 0]) + expected_1 = ma.array(expected_data, mask=[1, 0, 1, 0, 0, 0]) + + assert ma.allclose(expected_0, result_0.data) + assert ma.allclose(expected_05, result_05.data) + assert ma.allclose(expected_1, result_1.data) diff --git a/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_regrid_rectilinear_to_unstructured.py b/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_regrid_rectilinear_to_unstructured.py new file mode 100644 index 00000000..d0406e62 --- /dev/null +++ b/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_regrid_rectilinear_to_unstructured.py @@ -0,0 +1,164 @@ +"""Unit tests for :func:`esmf_regrid.experimental.unstructured_scheme.regrid_rectilinear_to_unstructured`.""" + +from iris.coords import AuxCoord, DimCoord +from iris.cube import Cube +import numpy as np +from numpy import ma + +from esmf_regrid.experimental.unstructured_scheme import ( + regrid_rectilinear_to_unstructured, +) +from esmf_regrid.tests.unit.experimental.unstructured_scheme.test__cube_to_GridInfo import ( + _grid_cube, +) +from esmf_regrid.tests.unit.experimental.unstructured_scheme.test__regrid_unstructured_to_rectilinear__prepare import ( + _flat_mesh_cube, +) + + +def test_flat_cubes(): + """ + Basic test for :func:`esmf_regrid.experimental.unstructured_scheme.regrid_rectilinear_to_unstructured`. + + Tests with flat cubes as input (a 2D grid cube and a 1D mesh cube). + """ + tgt = _flat_mesh_cube() + + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + # Ensure data in the target grid is different to the expected data. + # i.e. target grid data is all zero, expected data is all one + tgt.data[:] = 0 + + def _add_metadata(cube): + result = cube.copy() + result.units = "K" + result.attributes = {"a": 1} + result.standard_name = "air_temperature" + scalar_height = AuxCoord([5], units="m", standard_name="height") + scalar_time = DimCoord([10], units="s", standard_name="time") + result.add_aux_coord(scalar_height) + result.add_aux_coord(scalar_time) + return result + + src = _add_metadata(src) + src.data[:] = 1 # Ensure all data in the source is one. + result = regrid_rectilinear_to_unstructured(src, tgt) + src_T = src.copy() + src_T.transpose() + result_transposed = regrid_rectilinear_to_unstructured(src_T, tgt) + + expected_data = np.ones([n_lats, n_lons]) + expected_cube = _add_metadata(tgt) + + # Lenient check for data. + assert np.allclose(expected_data, result.data) + assert np.allclose(expected_data, result_transposed.data) + + # Check metadata and scalar coords. + expected_cube.data = result.data + assert expected_cube == result + expected_cube.data = result_transposed.data + assert expected_cube == result_transposed + + +def test_multidim_cubes(): + """ + Test for :func:`esmf_regrid.experimental.unstructured_scheme.regrid_rectilinear_to_unstructured`. + + Tests with multidimensional cubes. The source cube contains + coordinates on the dimensions before and after the grid dimensions. + """ + tgt = _flat_mesh_cube() + mesh = tgt.mesh + mesh_length = mesh.connectivity(contains_face=True).shape[0] + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + grid = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + h = 2 + p = 4 + t = 3 + height = DimCoord(np.arange(h), standard_name="height") + pressure = DimCoord(np.arange(p), standard_name="air_pressure") + time = DimCoord(np.arange(t), standard_name="time") + spanning = AuxCoord(np.ones([t, p, h]), long_name="spanning dim") + ignore = AuxCoord(np.ones([n_lats, h]), long_name="ignore") + + src_data = np.empty([t, n_lats, p, n_lons, h]) + src_data[:] = np.arange(t * p * h).reshape([t, p, h])[ + :, np.newaxis, :, np.newaxis, : + ] + cube = Cube(src_data) + cube.add_dim_coord(grid.coord("latitude"), 1) + cube.add_dim_coord(grid.coord("longitude"), 3) + cube.add_dim_coord(time, 0) + cube.add_dim_coord(pressure, 2) + cube.add_dim_coord(height, 4) + cube.add_aux_coord(spanning, [0, 2, 4]) + cube.add_aux_coord(ignore, [1, 4]) + + result = regrid_rectilinear_to_unstructured(cube, tgt) + + cube_transposed = cube.copy() + cube_transposed.transpose([0, 3, 2, 1, 4]) + result_transposed = regrid_rectilinear_to_unstructured(cube_transposed, tgt) + + # Lenient check for data. + expected_data = np.empty([t, mesh_length, p, h]) + expected_data[:] = np.arange(t * p * h).reshape(t, p, h)[:, np.newaxis, :, :] + assert np.allclose(expected_data, result.data) + assert np.allclose(expected_data, result_transposed.data) + + mesh_coord_x, mesh_coord_y = mesh.to_MeshCoords("face") + expected_cube = Cube(expected_data) + expected_cube.add_dim_coord(time, 0) + expected_cube.add_aux_coord(mesh_coord_x, 1) + expected_cube.add_aux_coord(mesh_coord_y, 1) + expected_cube.add_dim_coord(pressure, 2) + expected_cube.add_dim_coord(height, 3) + expected_cube.add_aux_coord(spanning, [0, 2, 3]) + + # Check metadata and scalar coords. + result.data = expected_data + assert expected_cube == result + result_transposed.data = expected_data + assert expected_cube == result_transposed + + +def test_mask_handling(): + """ + Test masked data handling for :func:`esmf_regrid.experimental.unstructured_scheme.regrid_rectilinear_to_unstructured`. + + Tests masked data handling for multiple valid values for mdtol. + """ + tgt = _flat_mesh_cube() + + n_lons = 6 + n_lats = 5 + lon_bounds = (-180, 180) + lat_bounds = (-90, 90) + src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + data = np.ones([n_lats, n_lons]) + mask = np.zeros([n_lats, n_lons]) + mask[0, 0] = 1 + masked_data = ma.array(data, mask=mask) + src.data = masked_data + result_0 = regrid_rectilinear_to_unstructured(src, tgt, mdtol=0) + result_05 = regrid_rectilinear_to_unstructured(src, tgt, mdtol=0.05) + result_1 = regrid_rectilinear_to_unstructured(src, tgt, mdtol=1) + + expected_data = np.ones(tgt.shape) + expected_0 = ma.array(expected_data) + expected_05 = ma.array(expected_data, mask=[0, 0, 1, 0, 0, 0]) + expected_1 = ma.array(expected_data, mask=[1, 0, 1, 0, 0, 0]) + + assert ma.allclose(expected_0, result_0.data) + assert ma.allclose(expected_05, result_05.data) + assert ma.allclose(expected_1, result_1.data)