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)
def test_invalid_method():
    """
    Test initialisation of :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`.

    Checks that an error is raised when the method is invalid.
    """
    n_lons = 6
    n_lats = 5
    lon_bounds = (-180, 180)
    lat_bounds = (-90, 90)
    face_tgt = _gridlike_mesh_cube(n_lons, n_lats, location="face")
    edge_tgt = _gridlike_mesh_cube(n_lons, n_lats, location="edge")
    node_tgt = _gridlike_mesh_cube(n_lons, n_lats, location="node")
    src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True)

    with pytest.raises(ValueError):
        _ = GridToMeshESMFRegridder(src, face_tgt, method="other")
    with pytest.raises(ValueError) as excinfo:
        _ = GridToMeshESMFRegridder(src, node_tgt, method="conservative")
    expected_message = (
        "Conservative regridding requires a target cube located on "
        "the face of a cube, target cube had the node location.")
    assert expected_message in str(excinfo.value)
    with pytest.raises(ValueError) as excinfo:
        _ = GridToMeshESMFRegridder(src, edge_tgt, method="bilinear")
    expected_message = (
        "Bilinear regridding requires a target cube with a node "
        "or face location, target cube had the edge location.")
    assert expected_message in str(excinfo.value)
def test_invalid_resolution():
    """
    Test initialisation of :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`.

    Checks that an error is raised when the resolution is invalid.
    """
    n_lons = 6
    n_lats = 5
    lon_bounds = (-180, 180)
    lat_bounds = (-90, 90)
    tgt = _gridlike_mesh_cube(n_lons, n_lats, location="face")
    src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True)

    with pytest.raises(ValueError) as excinfo:
        _ = GridToMeshESMFRegridder(src,
                                    tgt,
                                    method="conservative",
                                    resolution=-1)
    expected_message = "resolution must be a positive integer."
    assert expected_message in str(excinfo.value)

    with pytest.raises(ValueError) as excinfo:
        _ = GridToMeshESMFRegridder(src, tgt, method="bilinear", resolution=4)
    expected_message = "resolution can only be set for conservative regridding."
    assert expected_message in str(excinfo.value)
def test_laziness():
    """Test that regridding is lazy when source data is lazy."""
    n_lons = 12
    n_lats = 10
    h = 4
    lon_bounds = (-180, 180)
    lat_bounds = (-90, 90)

    mesh = _gridlike_mesh(n_lons, n_lats)

    src_data = np.arange(n_lats * n_lons * h).reshape([n_lats, n_lons, h])
    src_data = da.from_array(src_data, chunks=[3, 5, 2])
    src = Cube(src_data)
    grid = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True)
    src.add_dim_coord(grid.coord("latitude"), 0)
    src.add_dim_coord(grid.coord("longitude"), 1)

    mesh_coord_x, mesh_coord_y = mesh.to_MeshCoords("face")
    tgt_data = np.zeros([n_lats * n_lons])
    tgt = Cube(tgt_data)
    tgt.add_aux_coord(mesh_coord_x, 0)
    tgt.add_aux_coord(mesh_coord_y, 0)

    rg = GridToMeshESMFRegridder(src, tgt)

    assert src.has_lazy_data()
    result = rg(src)
    assert result.has_lazy_data()
    out_chunks = result.lazy_data().chunks
    expected_chunks = ((120, ), (2, 2))
    assert out_chunks == expected_chunks
    assert np.allclose(result.data, src_data.reshape([-1, h]))
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 setup_cache(self):
        SYNTH_DATA_DIR = Path().cwd() / "tmp_data"
        SYNTH_DATA_DIR.mkdir(exist_ok=True)
        file = str(SYNTH_DATA_DIR.joinpath("chunked_cube.nc"))
        lon_bounds = (-180, 180)
        lat_bounds = (-90, 90)
        n_lons_src = 100
        n_lats_src = 200
        n_lons_tgt = 20
        n_lats_tgt = 40
        h = 2000
        grid = _grid_cube(n_lons_src, n_lats_src, lon_bounds, lat_bounds)

        chunk_size = [n_lats_src, n_lons_src, 10]
        src_data = da.ones([n_lats_src, n_lons_src, h], chunks=chunk_size)
        src = Cube(src_data)
        src.add_dim_coord(grid.coord("latitude"), 0)
        src.add_dim_coord(grid.coord("longitude"), 1)
        tgt = _gridlike_mesh_cube(n_lons_tgt, n_lats_tgt)
        iris.save(src, file, chunksizes=chunk_size)
        # Construct regridder with a loaded version of the grid for consistency.
        loaded_src = iris.load_cube(file)
        regridder = GridToMeshESMFRegridder(loaded_src, tgt)

        return regridder, file
def _make_grid_to_mesh_regridder(method="conservative",
                                 resolution=None,
                                 grid_dims=1,
                                 circular=True):
    src_lons = 3
    src_lats = 4
    tgt_lons = 5
    tgt_lats = 6
    if circular:
        lon_bounds = (-180, 180)
    else:
        lon_bounds = (-180, 170)
    lat_bounds = (-90, 90)
    if grid_dims == 1:
        src = _grid_cube(src_lons,
                         src_lats,
                         lon_bounds,
                         lat_bounds,
                         circular=circular)
    else:
        src = _curvilinear_cube(src_lons, src_lats, lon_bounds, lat_bounds)
    src.coord("longitude").var_name = "longitude"
    src.coord("latitude").var_name = "latitude"
    if method == "bilinear":
        location = "node"
    else:
        location = "face"
    tgt = _gridlike_mesh_cube(tgt_lons, tgt_lats, location=location)

    rg = GridToMeshESMFRegridder(src,
                                 tgt,
                                 method=method,
                                 mdtol=0.5,
                                 resolution=resolution)
    return rg, src
def test_default_mdtol():
    """
    Test initialisation of :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`.

    Checks that default mdtol values are as expected.
    """
    n_lons = 6
    n_lats = 5
    lon_bounds = (-180, 180)
    lat_bounds = (-90, 90)
    tgt = _gridlike_mesh_cube(n_lons, n_lats)
    src = _grid_cube(n_lons, n_lats, lon_bounds, lat_bounds, circular=True)

    rg_con = GridToMeshESMFRegridder(src, tgt, method="conservative")
    assert rg_con.mdtol == 1
    rg_bi = GridToMeshESMFRegridder(src, tgt, method="bilinear")
    assert rg_bi.mdtol == 0
    def setup_cache(self):
        from esmf_regrid.experimental.io import save_regridder

        SYNTH_DATA_DIR = Path().cwd() / "tmp_data"
        SYNTH_DATA_DIR.mkdir(exist_ok=True)

        destination_file = str(SYNTH_DATA_DIR.joinpath("destination.nc"))
        file_dict = {"destination": destination_file}

        for tp in self.params[0]:
            (
                lon_bounds,
                lat_bounds,
                n_lons_src,
                n_lats_src,
                n_lons_tgt,
                n_lats_tgt,
                _,
                alt_coord_system,
            ) = self.get_args(tp)
            src_grid = _grid_cube(
                n_lons_src,
                n_lats_src,
                lon_bounds,
                lat_bounds,
                alt_coord_system=alt_coord_system,
            )
            tgt_grid = _grid_cube(
                n_lons_tgt,
                n_lats_tgt,
                lon_bounds,
                lat_bounds,
                alt_coord_system=alt_coord_system,
            )
            src_mesh_cube = _gridlike_mesh_cube(
                n_lons_src,
                n_lats_src,
            )
            tgt_mesh_cube = _gridlike_mesh_cube(
                n_lons_tgt,
                n_lats_tgt,
            )

            rg_dict = {}
            rg_dict["mesh_to_grid"] = MeshToGridESMFRegridder(
                src_mesh_cube, tgt_grid)
            rg_dict["grid_to_mesh"] = GridToMeshESMFRegridder(
                src_grid, tgt_mesh_cube)

            for rgt in self.params[1]:
                regridder = rg_dict[rgt]
                source_file = str(
                    SYNTH_DATA_DIR.joinpath(f"source_{tp}_{rgt}.nc"))

                save_regridder(regridder, source_file)

                file_dict[(tp, rgt)] = source_file
        return file_dict
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_bilinear():
    """
    Basic test for :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`.

    Tests with method="bilinear".
    """
    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)
    face_tgt = _gridlike_mesh_cube(n_lons, n_lats, location="face")
    node_tgt = _gridlike_mesh_cube(n_lons, n_lats, location="node")

    src = _add_metadata(src)
    src.data[:] = 1  # Ensure all data in the source is one.
    face_regridder = GridToMeshESMFRegridder(src, face_tgt, method="bilinear")
    node_regridder = GridToMeshESMFRegridder(src, node_tgt, method="bilinear")

    assert face_regridder.regridder.method == "bilinear"
    assert node_regridder.regridder.method == "bilinear"

    face_expected_data = np.ones_like(face_tgt.data)
    node_expected_data = np.ones_like(node_tgt.data)
    face_result = face_regridder(src)
    node_result = node_regridder(src)

    # Lenient check for data.
    assert np.allclose(face_expected_data, face_result.data)
    assert np.allclose(node_expected_data, node_result.data)

    # Check metadata and scalar coords.
    face_expected_cube = _add_metadata(face_tgt)
    node_expected_cube = _add_metadata(node_tgt)
    face_expected_cube.data = face_result.data
    node_expected_cube.data = node_result.data
    assert face_expected_cube == face_result
    assert node_expected_cube == node_result
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_resolution():
    """
    Test for :func:`esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`.

    Tests for the resolution keyword.
    """
    tgt = _flat_mesh_cube()
    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)

    resolution = 8

    result = GridToMeshESMFRegridder(grid, tgt, resolution=resolution)
    assert result.resolution == resolution
    assert result.regridder.src.resolution == resolution
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

    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