diff --git a/esmf_regrid/experimental/unstructured_scheme.py b/esmf_regrid/experimental/unstructured_scheme.py index 24bba373..f17aadcd 100644 --- a/esmf_regrid/experimental/unstructured_scheme.py +++ b/esmf_regrid/experimental/unstructured_scheme.py @@ -1,5 +1,7 @@ """Provides an iris interface for unstructured regridding.""" +from iris.experimental.ugrid import Mesh + from esmf_regrid.schemes import ( _ESMFRegridder, _get_mask, @@ -281,16 +283,18 @@ def __init__( src_resolution=None, use_src_mask=False, use_tgt_mask=False, + tgt_location=None, ): """ Create regridder for conversions between source grid and target mesh. Parameters ---------- - src_grid_cube : :class:`iris.cube.Cube` + src : :class:`iris.cube.Cube` The rectilinear :class:`~iris.cube.Cube` cube providing the source grid. - target_mesh_cube : :class:`iris.cube.Cube` - The unstructured :class:`~iris.cube.Cube` providing the target mesh. + tgt : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` 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 @@ -322,6 +326,9 @@ def __init__( a boolean value. If True, this array is taken from the mask on the data in ``tgt``. If False, no mask will be taken and all points will be used in weights calculation. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Raises ------ @@ -330,7 +337,7 @@ def __init__( or ``tgt`` respectively are not constant over non-horizontal dimensions. """ - if tgt.mesh is None: + if not isinstance(tgt, Mesh) and tgt.mesh is None: raise ValueError("tgt has no mesh.") super().__init__( src, @@ -341,6 +348,7 @@ def __init__( src_resolution=src_resolution, use_src_mask=use_src_mask, use_tgt_mask=use_tgt_mask, + tgt_location=tgt_location, ) self.resolution = src_resolution self.mesh, self.location = self._tgt diff --git a/esmf_regrid/schemes.py b/esmf_regrid/schemes.py index a5f25872..a9c2528e 100644 --- a/esmf_regrid/schemes.py +++ b/esmf_regrid/schemes.py @@ -9,6 +9,7 @@ import iris.coords import iris.cube from iris.exceptions import CoordinateNotFoundError +from iris.experimental.ugrid import Mesh import numpy as np from esmf_regrid.esmf_regridder import GridInfo, RefinedGridInfo, Regridder @@ -33,39 +34,43 @@ def _get_coord(cube, axis): return coord -def _get_mask(cube, use_mask=True): +def _get_mask(cube_or_mesh, use_mask=True): if use_mask is False: result = None elif use_mask is True: - src_x, src_y = (_get_coord(cube, "x"), _get_coord(cube, "y")) + if isinstance(cube_or_mesh, Mesh): + result = None + else: + cube = cube_or_mesh + src_x, src_y = (_get_coord(cube, "x"), _get_coord(cube, "y")) - horizontal_dims = set(cube.coord_dims(src_x)) | set(cube.coord_dims(src_y)) - other_dims = tuple(set(range(cube.ndim)) - horizontal_dims) + horizontal_dims = set(cube.coord_dims(src_x)) | set(cube.coord_dims(src_y)) + other_dims = tuple(set(range(cube.ndim)) - horizontal_dims) - # Find a representative slice of data that spans both horizontal coords. - if cube.coord_dims(src_x) == cube.coord_dims(src_y): - slices = cube.slices([src_x]) - else: - slices = cube.slices([src_x, src_y]) - data = next(slices).data - if np.ma.is_masked(data): - # Check that the mask is constant along all other dimensions. - full_mask = da.ma.getmaskarray(cube.core_data()) - if not np.array_equal( - da.all(full_mask, axis=other_dims).compute(), - da.any(full_mask, axis=other_dims).compute(), - ): - raise ValueError( - "The mask derived from the cube is not constant over non-horizontal dimensions." - "Consider passing in an explicit mask instead." - ) - mask = np.ma.getmaskarray(data) - # Due to structural reasons, the mask should be transposed for curvilinear grids. - if cube.coord_dims(src_x) != cube.coord_dims(src_y): - mask = mask.T - else: - mask = None - result = mask + # Find a representative slice of data that spans both horizontal coords. + if cube.coord_dims(src_x) == cube.coord_dims(src_y): + slices = cube.slices([src_x]) + else: + slices = cube.slices([src_x, src_y]) + data = next(slices).data + if np.ma.is_masked(data): + # Check that the mask is constant along all other dimensions. + full_mask = da.ma.getmaskarray(cube.core_data()) + if not np.array_equal( + da.all(full_mask, axis=other_dims).compute(), + da.any(full_mask, axis=other_dims).compute(), + ): + raise ValueError( + "The mask derived from the cube is not constant over non-horizontal dimensions." + "Consider passing in an explicit mask instead." + ) + mask = np.ma.getmaskarray(data) + # Due to structural reasons, the mask should be transposed for curvilinear grids. + if cube.coord_dims(src_x) != cube.coord_dims(src_y): + mask = mask.T + else: + mask = None + result = mask else: result = use_mask return result @@ -465,9 +470,12 @@ def _make_gridinfo(cube, method, resolution, mask): return _cube_to_GridInfo(cube, center=center, resolution=resolution, mask=mask) -def _make_meshinfo(cube, method, mask, src_or_tgt): - mesh = cube.mesh - location = cube.location +def _make_meshinfo(cube_or_mesh, method, mask, src_or_tgt, location=None): + if isinstance(cube_or_mesh, Mesh): + mesh = cube_or_mesh + else: + mesh = cube_or_mesh.mesh + location = cube_or_mesh.location if mesh is None: raise ValueError(f"The {src_or_tgt} cube is not defined on a mesh.") if method == "conservative": @@ -660,12 +668,13 @@ def _regrid_unstructured_to_rectilinear__perform(src_cube, regrid_info, mdtol): def _regrid_rectilinear_to_unstructured__prepare( src_grid_cube, - target_mesh_cube, + tgt_cube_or_mesh, method, precomputed_weights=None, src_resolution=None, src_mask=None, tgt_mask=None, + tgt_location=None, ): """ First (setup) part of 'regrid_rectilinear_to_unstructured'. @@ -676,8 +685,12 @@ def _regrid_rectilinear_to_unstructured__prepare( """ grid_x = _get_coord(src_grid_cube, "x") grid_y = _get_coord(src_grid_cube, "y") - mesh = target_mesh_cube.mesh - location = target_mesh_cube.location + if isinstance(tgt_cube_or_mesh, Mesh): + mesh = tgt_cube_or_mesh + location = tgt_location + else: + mesh = tgt_cube_or_mesh.mesh + location = tgt_cube_or_mesh.location if grid_x.ndim == 1: (grid_x_dim,) = src_grid_cube.coord_dims(grid_x) @@ -685,7 +698,9 @@ def _regrid_rectilinear_to_unstructured__prepare( else: grid_y_dim, grid_x_dim = src_grid_cube.coord_dims(grid_x) - meshinfo = _make_meshinfo(target_mesh_cube, method, tgt_mask, "target") + meshinfo = _make_meshinfo( + tgt_cube_or_mesh, method, tgt_mask, "target", location=tgt_location + ) gridinfo = _make_gridinfo(src_grid_cube, method, src_resolution, src_mask) regridder = Regridder( @@ -755,6 +770,7 @@ def _regrid_unstructured_to_unstructured__prepare( target_mesh_cube, method, precomputed_weights=None, + tgt_location=None, ): raise NotImplementedError @@ -832,7 +848,9 @@ class ESMFAreaWeighted: """ - def __init__(self, mdtol=0, use_src_mask=False, use_tgt_mask=False): + def __init__( + self, mdtol=0, use_src_mask=False, use_tgt_mask=False, tgt_location="face" + ): """ Area-weighted scheme for regridding between rectilinear grids. @@ -852,35 +870,55 @@ def __init__(self, mdtol=0, use_src_mask=False, use_tgt_mask=False): use_tgt_mask : bool, default=False If True, derive a mask from target cube which will tell :mod:`esmpy` which points to ignore. + tgt_location : str or None, default="face" + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. """ if not (0 <= mdtol <= 1): msg = "Value for mdtol must be in range 0 - 1, got {}." raise ValueError(msg.format(mdtol)) + if tgt_location is not None and tgt_location != "face": + raise ValueError( + "For area weighted regridding, target location must be 'face'." + ) self.mdtol = mdtol self.use_src_mask = use_src_mask self.use_tgt_mask = use_tgt_mask + self.tgt_location = "face" def __repr__(self): """Return a representation of the class.""" return "ESMFAreaWeighted(mdtol={})".format(self.mdtol) - def regridder(self, src_grid, tgt_grid, use_src_mask=None, use_tgt_mask=None): + def regridder( + self, + src_grid, + tgt_grid, + use_src_mask=None, + use_tgt_mask=None, + tgt_location="face", + ): """ Create regridder to perform regridding from ``src_grid`` to ``tgt_grid``. Parameters ---------- src_grid : :class:`iris.cube.Cube` - The :class:`~iris.cube.Cube` defining the source grid. - tgt_grid : :class:`iris.cube.Cube` - The :class:`~iris.cube.Cube` defining the target grid. + The :class:`~iris.cube.Cube` defining the source. + tgt_grid : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` defining the target. use_src_mask : :obj:`~numpy.typing.ArrayLike` or bool, optional Array describing which elements :mod:`esmpy` will ignore on the src_grid. If True, the mask will be derived from src_grid. use_tgt_mask : :obj:`~numpy.typing.ArrayLike` or bool, optional Array describing which elements :mod:`esmpy` will ignore on the tgt_grid. If True, the mask will be derived from tgt_grid. + tgt_location : str or None, default="face" + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. + Returns ------- @@ -900,12 +938,17 @@ def regridder(self, src_grid, tgt_grid, use_src_mask=None, use_tgt_mask=None): use_src_mask = self.use_src_mask if use_tgt_mask is None: use_tgt_mask = self.use_tgt_mask + if tgt_location is not None and tgt_location != "face": + raise ValueError( + "For area weighted regridding, target location must be 'face'." + ) return ESMFAreaWeightedRegridder( src_grid, tgt_grid, mdtol=self.mdtol, use_src_mask=use_src_mask, use_tgt_mask=use_tgt_mask, + tgt_location="face", ) @@ -918,7 +961,9 @@ class ESMFBilinear: calculations and allows for different coordinate systems. """ - def __init__(self, mdtol=0, use_src_mask=False, use_tgt_mask=False): + def __init__( + self, mdtol=0, use_src_mask=False, use_tgt_mask=False, tgt_location=None + ): """ Area-weighted scheme for regridding between rectilinear grids. @@ -934,6 +979,9 @@ def __init__(self, mdtol=0, use_src_mask=False, use_tgt_mask=False): use_tgt_mask : bool, default=False If True, derive a mask from target cube which will tell :mod:`esmpy` which points to ignore. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. """ if not (0 <= mdtol <= 1): @@ -942,27 +990,39 @@ def __init__(self, mdtol=0, use_src_mask=False, use_tgt_mask=False): self.mdtol = mdtol self.use_src_mask = use_src_mask self.use_tgt_mask = use_tgt_mask + self.tgt_location = tgt_location def __repr__(self): """Return a representation of the class.""" return "ESMFBilinear(mdtol={})".format(self.mdtol) - def regridder(self, src_grid, tgt_grid, use_src_mask=None, use_tgt_mask=None): + def regridder( + self, + src_grid, + tgt_grid, + use_src_mask=None, + use_tgt_mask=None, + tgt_location=None, + ): """ Create regridder to perform regridding from ``src_grid`` to ``tgt_grid``. Parameters ---------- src_grid : :class:`iris.cube.Cube` - The :class:`~iris.cube.Cube` defining the source grid. - tgt_grid : :class:`iris.cube.Cube` - The :class:`~iris.cube.Cube` defining the target grid. + The :class:`~iris.cube.Cube` defining the source. + tgt_grid : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` defining the target. use_src_mask : :obj:`~numpy.typing.ArrayLike` or bool, optional Array describing which elements :mod:`esmpy` will ignore on the src_grid. If True, the mask will be derived from src_grid. use_tgt_mask : :obj:`~numpy.typing.ArrayLike` or bool, optional Array describing which elements :mod:`esmpy` will ignore on the tgt_grid. If True, the mask will be derived from tgt_grid. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Returns ------- @@ -982,12 +1042,15 @@ def regridder(self, src_grid, tgt_grid, use_src_mask=None, use_tgt_mask=None): use_src_mask = self.use_src_mask if use_tgt_mask is None: use_tgt_mask = self.use_tgt_mask + if tgt_location is None: + tgt_location = self.tgt_location return ESMFBilinearRegridder( src_grid, tgt_grid, mdtol=self.mdtol, use_src_mask=use_src_mask, use_tgt_mask=use_tgt_mask, + tgt_location=tgt_location, ) @@ -1017,7 +1080,7 @@ class ESMFNearest: the same equivalent space will behave the same. """ - def __init__(self, use_src_mask=False, use_tgt_mask=False): + def __init__(self, use_src_mask=False, use_tgt_mask=False, tgt_location=None): """ Nearest neighbour scheme for regridding between rectilinear grids. @@ -1029,30 +1092,45 @@ def __init__(self, use_src_mask=False, use_tgt_mask=False): use_tgt_mask : bool, default=False If True, derive a mask from target cube which will tell :mod:`esmpy` which points to ignore. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. """ self.use_src_mask = use_src_mask self.use_tgt_mask = use_tgt_mask + self.tgt_location = tgt_location def __repr__(self): """Return a representation of the class.""" return "ESMFNearest()" - def regridder(self, src_grid, tgt_grid, use_src_mask=None, use_tgt_mask=None): + def regridder( + self, + src_grid, + tgt_grid, + use_src_mask=None, + use_tgt_mask=None, + tgt_location=None, + ): """ Create regridder to perform regridding from ``src_grid`` to ``tgt_grid``. Parameters ---------- src_grid : :class:`iris.cube.Cube` - The :class:`~iris.cube.Cube` defining the source grid. - tgt_grid : :class:`iris.cube.Cube` - The :class:`~iris.cube.Cube` defining the target grid. + The :class:`~iris.cube.Cube` defining the source. + tgt_grid : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` defining the target. use_src_mask : :obj:`~numpy.typing.ArrayLike` or bool, optional Array describing which elements :mod:`esmpy` will ignore on the src_grid. If True, the mask will be derived from src_grid. use_tgt_mask : :obj:`~numpy.typing.ArrayLike` or bool, optional Array describing which elements :mod:`esmpy` will ignore on the tgt_grid. If True, the mask will be derived from tgt_grid. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Returns ------- @@ -1072,11 +1150,14 @@ def regridder(self, src_grid, tgt_grid, use_src_mask=None, use_tgt_mask=None): use_src_mask = self.use_src_mask if use_tgt_mask is None: use_tgt_mask = self.use_tgt_mask + if tgt_location is None: + tgt_location = self.tgt_location return ESMFNearestRegridder( src_grid, tgt_grid, use_src_mask=use_src_mask, use_tgt_mask=use_tgt_mask, + tgt_location=tgt_location, ) @@ -1091,6 +1172,7 @@ def __init__( mdtol=None, use_src_mask=False, use_tgt_mask=False, + tgt_location=None, **kwargs, ): """ @@ -1100,7 +1182,7 @@ def __init__( ---------- src : :class:`iris.cube.Cube` The rectilinear :class:`~iris.cube.Cube` providing the source grid. - tgt : :class:`iris.cube.Cube` + tgt : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` The rectilinear :class:`~iris.cube.Cube` providing the target grid. method : str Either "conservative", "bilinear" or "nearest". Corresponds to the :mod:`esmpy` methods @@ -1119,6 +1201,9 @@ def __init__( a boolean value. If True, this array is taken from the mask on the data in ``src`` or ``tgt``. If False, no mask will be taken and all points will be used in weights calculation. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Raises ------ @@ -1148,15 +1233,17 @@ def __init__( kwargs["tgt_mask"] = self.tgt_mask src_is_mesh = src.mesh is not None - tgt_is_mesh = tgt.mesh is not None + tgt_is_mesh = isinstance(tgt, Mesh) or tgt.mesh is not None if src_is_mesh: if tgt_is_mesh: prepare_func = _regrid_unstructured_to_unstructured__prepare + kwargs["tgt_location"] = tgt_location else: prepare_func = _regrid_unstructured_to_rectilinear__prepare else: if tgt_is_mesh: prepare_func = _regrid_rectilinear_to_unstructured__prepare + kwargs["tgt_location"] = tgt_location else: prepare_func = _regrid_rectilinear_to_rectilinear__prepare regrid_info = prepare_func(src, tgt, method, **kwargs) @@ -1269,6 +1356,7 @@ def __init__( tgt_resolution=None, use_src_mask=False, use_tgt_mask=False, + tgt_location="face", ): """ Create regridder for conversions between ``src`` and ``tgt``. @@ -1276,9 +1364,10 @@ def __init__( Parameters ---------- src : :class:`iris.cube.Cube` - The rectilinear :class:`~iris.cube.Cube` providing the source grid. - tgt : :class:`iris.cube.Cube` - The rectilinear :class:`~iris.cube.Cube` providing the target grid. + The rectilinear :class:`~iris.cube.Cube` providing the source. + tgt : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` defining the target. mdtol : float, default=0 Tolerance of missing data. The value returned in each element of the returned array will be masked if the fraction of masked data @@ -1299,18 +1388,26 @@ def __init__( a boolean value. If True, this array is taken from the mask on the data in ``src`` or ``tgt``. If False, no mask will be taken and all points will be used in weights calculation. + tgt_location : str or None, default="face" + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Raises ------ ValueError If ``use_src_mask`` or ``use_tgt_mask`` are True while the masks on ``src`` - or ``tgt`` respectively are not constant over non-horizontal dimensions. + or ``tgt`` respectively are not constant over non-horizontal dimensions or + if tgt_location is not "face". """ kwargs = dict() if src_resolution is not None: kwargs["src_resolution"] = src_resolution if tgt_resolution is not None: kwargs["tgt_resolution"] = tgt_resolution + if tgt_location is not None and tgt_location != "face": + raise ValueError( + "For area weighted regridding, target location must be 'face'." + ) kwargs["use_src_mask"] = use_src_mask kwargs["use_tgt_mask"] = use_tgt_mask super().__init__( @@ -1319,6 +1416,7 @@ def __init__( "conservative", mdtol=mdtol, precomputed_weights=precomputed_weights, + tgt_location="face", **kwargs, ) @@ -1334,6 +1432,7 @@ def __init__( precomputed_weights=None, use_src_mask=False, use_tgt_mask=False, + tgt_location=None, ): """ Create regridder for conversions between ``src`` and ``tgt``. @@ -1341,9 +1440,10 @@ def __init__( Parameters ---------- src : :class:`iris.cube.Cube` - The rectilinear :class:`~iris.cube.Cube` providing the source grid. - tgt : :class:`iris.cube.Cube` - The rectilinear :class:`~iris.cube.Cube` providing the target grid. + The rectilinear :class:`~iris.cube.Cube` providing the source. + tgt : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` defining the target. mdtol : float, default=0 Tolerance of missing data. The value returned in each element of the returned array will be masked if the fraction of masked data @@ -1359,6 +1459,9 @@ def __init__( a boolean value. If True, this array is taken from the mask on the data in ``src`` or ``tgt``. If False, no mask will be taken and all points will be used in weights calculation. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Raises ------ @@ -1374,6 +1477,7 @@ def __init__( precomputed_weights=precomputed_weights, use_src_mask=use_src_mask, use_tgt_mask=use_tgt_mask, + tgt_location=tgt_location, ) @@ -1387,6 +1491,7 @@ def __init__( precomputed_weights=None, use_src_mask=False, use_tgt_mask=False, + tgt_location=None, ): """ Create regridder for conversions between ``src`` and ``tgt``. @@ -1394,9 +1499,10 @@ def __init__( Parameters ---------- src : :class:`iris.cube.Cube` - The rectilinear :class:`~iris.cube.Cube` providing the source grid. - tgt : :class:`iris.cube.Cube` - The rectilinear :class:`~iris.cube.Cube` providing the target grid. + The rectilinear :class:`~iris.cube.Cube` providing the source. + tgt : :class:`iris.cube.Cube` or :class:`iris.experimental.ugrid.Mesh` + The unstructured :class:`~iris.cube.Cube`or + :class:`~iris.experimental.ugrid.Mesh` defining the target. precomputed_weights : :class:`scipy.sparse.spmatrix`, optional If ``None``, :mod:`esmpy` will be used to calculate regridding weights. Otherwise, :mod:`esmpy` will be bypassed @@ -1406,6 +1512,9 @@ def __init__( a boolean value. If True, this array is taken from the mask on the data in ``src`` or ``tgt``. If False, no mask will be taken and all points will be used in weights calculation. + tgt_location : str or None, default=None + Either "face" or "node". Describes the location for data on the mesh + if the target is not a :class:`~iris.cube.Cube`. Raises ------ @@ -1421,4 +1530,5 @@ def __init__( precomputed_weights=precomputed_weights, use_src_mask=use_src_mask, use_tgt_mask=use_tgt_mask, + tgt_location=tgt_location, ) diff --git a/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py b/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py index f89e2243..1704f9e5 100644 --- a/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py +++ b/esmf_regrid/tests/unit/experimental/unstructured_scheme/test_GridToMeshESMFRegridder.py @@ -434,6 +434,38 @@ def test_curvilinear(): assert result_lazy == result +def test_mesh_target(): + """ + Basic test for :class:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`. + + Tests with a mesh as the target. + """ + n_tgt_lons = 5 + n_tgt_lats = 4 + tgt = _gridlike_mesh(n_tgt_lons, n_tgt_lats) + + 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) + + src = _add_metadata(src) + src.data[:] = 1 # Ensure all data in the source is one. + regridder = GridToMeshESMFRegridder(src, tgt, tgt_location="face") + result = regridder(src) + + expected_data = np.ones([n_tgt_lats * n_tgt_lons]) + expected_cube = _add_metadata(_gridlike_mesh_cube(n_tgt_lons, n_tgt_lats)) + + # Lenient check for data. + assert np.allclose(expected_data, result.data) + + # Check metadata and scalar coords. + expected_cube.data = result.data + assert expected_cube == result + + @pytest.mark.parametrize( "resolution", (None, 2), ids=("no resolution", "with resolution") ) diff --git a/esmf_regrid/tests/unit/schemes/__init__.py b/esmf_regrid/tests/unit/schemes/__init__.py index 4874bad4..7d468fc4 100644 --- a/esmf_regrid/tests/unit/schemes/__init__.py +++ b/esmf_regrid/tests/unit/schemes/__init__.py @@ -6,6 +6,7 @@ from esmf_regrid.tests.unit.schemes.test__cube_to_GridInfo import _grid_cube from esmf_regrid.tests.unit.schemes.test__mesh_to_MeshInfo import ( + _gridlike_mesh, _gridlike_mesh_cube, ) @@ -16,8 +17,12 @@ def _test_cube_regrid(scheme, src_type, tgt_type): Checks that regridding occurs and that mdtol is used correctly. """ - scheme_default = scheme() - scheme_full_mdtol = scheme(mdtol=1) + if tgt_type == "just_mesh": + scheme_default = scheme(tgt_location="face") + scheme_full_mdtol = scheme(mdtol=1, tgt_location="face") + else: + scheme_default = scheme() + scheme_full_mdtol = scheme(mdtol=1) n_lons_src = 6 n_lons_tgt = 3 @@ -40,11 +45,17 @@ def _test_cube_regrid(scheme, src_type, tgt_type): expected_data_default = np.zeros([n_lats_tgt, n_lons_tgt]) expected_mask = np.zeros([n_lats_tgt, n_lons_tgt]) expected_mask[0, 0] = 1 - else: + elif tgt_type == "mesh": tgt = _gridlike_mesh_cube(n_lons_tgt, n_lats_tgt) expected_data_default = np.zeros([n_lats_tgt * n_lons_tgt]) expected_mask = np.zeros([n_lats_tgt * n_lons_tgt]) expected_mask[0] = 1 + elif tgt_type == "just_mesh": + tgt = _gridlike_mesh(n_lons_tgt, n_lats_tgt) + expected_data_default = np.zeros([n_lats_tgt * n_lons_tgt]) + expected_mask = np.zeros([n_lats_tgt * n_lons_tgt]) + expected_mask[0] = 1 + src_data = ma.array(src_data, mask=src_mask) src.data = src_data @@ -53,10 +64,14 @@ def _test_cube_regrid(scheme, src_type, tgt_type): expected_data_full = ma.array(expected_data_default, mask=expected_mask) - expected_cube_default = tgt.copy() + if tgt_type == "just_mesh": + tgt_template = _gridlike_mesh_cube(n_lons_tgt, n_lats_tgt) + else: + tgt_template = tgt + expected_cube_default = tgt_template.copy() expected_cube_default.data = expected_data_default - expected_cube_full = tgt.copy() + expected_cube_full = tgt_template.copy() expected_cube_full.data = expected_data_full assert expected_cube_default == result_default diff --git a/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeighted.py b/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeighted.py index 8c10789e..49cd91c0 100644 --- a/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeighted.py +++ b/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeighted.py @@ -12,7 +12,8 @@ @pytest.mark.parametrize( - "src_type,tgt_type", [("grid", "grid"), ("grid", "mesh"), ("mesh", "grid")] + "src_type,tgt_type", + [("grid", "grid"), ("grid", "mesh"), ("grid", "just_mesh"), ("mesh", "grid")], ) def test_cube_regrid(src_type, tgt_type): """ @@ -50,3 +51,14 @@ def test_mask_from_regridder(mask_keyword): Checks that use_src_mask and use_tgt_mask are passed down correctly. """ _test_mask_from_regridder(ESMFAreaWeighted, mask_keyword) + + +def test_invalid_tgt_location(): + """ + Test initialisation of :class:`esmf_regrid.schemes.ESMFAreaWeighted`. + + Checks that initialisation fails when tgt_location is not "face". + """ + match = "For area weighted regridding, target location must be 'face'." + with pytest.raises(ValueError, match=match): + _ = ESMFAreaWeighted(tgt_location="node") diff --git a/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeightedRegridder.py b/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeightedRegridder.py index 86bcbf8d..c5d77ae9 100644 --- a/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeightedRegridder.py +++ b/esmf_regrid/tests/unit/schemes/test_ESMFAreaWeightedRegridder.py @@ -88,6 +88,24 @@ def test_invalid_mdtol(): _ = ESMFAreaWeightedRegridder(src, tgt, mdtol=-1) +def test_invalid_tgt_location(): + """ + Test initialisation of :class:`esmf_regrid.schemes.ESMFAreaWeightedRegridder`. + + Checks that initialisation fails when tgt_location is not "face". + """ + 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) + tgt = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True) + + match = "For area weighted regridding, target location must be 'face'." + with pytest.raises(ValueError, match=match): + _ = ESMFAreaWeightedRegridder(src, tgt, tgt_location="node") + + def test_curvilinear_equivalence(): """ Test initialisation of :class:`esmf_regrid.schemes.ESMFAreaWeightedRegridder`. diff --git a/esmf_regrid/tests/unit/schemes/test_ESMFBilinear.py b/esmf_regrid/tests/unit/schemes/test_ESMFBilinear.py index ae4f18fc..71af3b66 100644 --- a/esmf_regrid/tests/unit/schemes/test_ESMFBilinear.py +++ b/esmf_regrid/tests/unit/schemes/test_ESMFBilinear.py @@ -12,7 +12,8 @@ @pytest.mark.parametrize( - "src_type,tgt_type", [("grid", "grid"), ("grid", "mesh"), ("mesh", "grid")] + "src_type,tgt_type", + [("grid", "grid"), ("grid", "mesh"), ("grid", "just_mesh"), ("mesh", "grid")], ) def test_cube_regrid(src_type, tgt_type): """ diff --git a/esmf_regrid/tests/unit/schemes/test_ESMFNearest.py b/esmf_regrid/tests/unit/schemes/test_ESMFNearest.py index f0f9f85e..48b01181 100644 --- a/esmf_regrid/tests/unit/schemes/test_ESMFNearest.py +++ b/esmf_regrid/tests/unit/schemes/test_ESMFNearest.py @@ -11,12 +11,14 @@ ) from esmf_regrid.tests.unit.schemes.test__cube_to_GridInfo import _grid_cube from esmf_regrid.tests.unit.schemes.test__mesh_to_MeshInfo import ( + _gridlike_mesh, _gridlike_mesh_cube, ) @pytest.mark.parametrize( - "src_type,tgt_type", [("grid", "grid"), ("grid", "mesh"), ("mesh", "grid")] + "src_type,tgt_type", + [("grid", "grid"), ("grid", "mesh"), ("grid", "just_mesh"), ("mesh", "grid")], ) def test_cube_regrid(src_type, tgt_type): """ @@ -24,7 +26,10 @@ def test_cube_regrid(src_type, tgt_type): Checks that regridding occurs. """ - scheme_default = ESMFNearest() + if tgt_type == "just_mesh": + scheme_default = ESMFNearest(tgt_location="face") + else: + scheme_default = ESMFNearest() n_lons_src = 6 n_lons_tgt = 3 @@ -47,17 +52,25 @@ def test_cube_regrid(src_type, tgt_type): expected_data_default = np.zeros([n_lats_tgt, n_lons_tgt]) expected_mask = np.zeros([n_lats_tgt, n_lons_tgt]) expected_mask[0, 0] = 1 - else: + elif tgt_type == "mesh": tgt = _gridlike_mesh_cube(n_lons_tgt, n_lats_tgt) expected_data_default = np.zeros([n_lats_tgt * n_lons_tgt]) expected_mask = np.zeros([n_lats_tgt * n_lons_tgt]) expected_mask[0] = 1 + elif tgt_type == "just_mesh": + tgt = _gridlike_mesh(n_lons_tgt, n_lats_tgt) + expected_data_default = np.zeros([n_lats_tgt * n_lons_tgt]) + expected_mask = np.zeros([n_lats_tgt * n_lons_tgt]) + expected_mask[0] = 1 src_data = ma.array(src_data, mask=src_mask) src.data = src_data result_default = src.regrid(tgt, scheme_default) - expected_cube_default = tgt.copy() + if tgt_type == "just_mesh": + expected_cube_default = _gridlike_mesh_cube(n_lons_tgt, n_lats_tgt) + else: + expected_cube_default = tgt.copy() expected_cube_default.data = expected_data_default assert expected_cube_default == result_default