def test_multiple_coords(self):
        cube = iris.tests.stock.realistic_4d()
        cs = iris.coord_systems.GeogCS(6371229)
        time_coord = cube.coord('time')
        time_dims = cube.coord_dims(time_coord)
        lat_coord = iris.coords.DimCoord(np.arange(time_coord.shape[0]),
                                         standard_name='latitude',
                                         units='degrees',
                                         coord_system=cs)
        cube.remove_coord(time_coord)
        cube.add_dim_coord(lat_coord, time_dims)
        model_level_coord = cube.coord('model_level_number')
        model_level_dims = cube.coord_dims(model_level_coord)
        lon_coord = iris.coords.DimCoord(np.arange(model_level_coord.shape[0]),
                                         standard_name='longitude',
                                         units='degrees',
                                         coord_system=cs)
        cube.remove_coord(model_level_coord)
        cube.add_dim_coord(lon_coord, model_level_dims)

        with self.assertRaises(ValueError):
            get_xy_dim_coords(cube)

        cube.remove_coord('grid_latitude')
        cube.remove_coord('grid_longitude')

        x, y = get_xy_dim_coords(cube)
        self.assertIs(x, lon_coord)
        self.assertIs(y, lat_coord)
    def test_different_coordsystem(self):
        cube = iris.tests.stock.lat_lon_cube()

        lat_cs = copy.copy(cube.coord('latitude').coord_system)
        lat_cs.semi_major_axis = 7000000
        cube.coord('latitude').coord_system = lat_cs

        lon_cs = copy.copy(cube.coord('longitude').coord_system)
        lon_cs.semi_major_axis = 7000001
        cube.coord('longitude').coord_system = lon_cs

        with self.assertRaises(ValueError):
            get_xy_dim_coords(cube)
 def test_projection_coords(self):
     cube = iris.tests.stock.lat_lon_cube()
     cube.coord('longitude').rename('projection_x_coordinate')
     cube.coord('latitude').rename('projection_y_coordinate')
     x, y = get_xy_dim_coords(cube)
     self.assertIs(x, cube.coord('projection_x_coordinate'))
     self.assertIs(y, cube.coord('projection_y_coordinate'))
Пример #4
0
    def __call__(self, cube):
        """
        Regrid this :class:`~iris.cube.Cube` onto the target grid of
        this :class:`AreaWeightedRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`AreaWeightedRegridder`.

        Args:

        * cube:
            A :class:`~iris.cube.Cube` 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.

        """
        if get_xy_dim_coords(cube) != self._src_grid:
            raise ValueError('The given cube is not defined on the same '
                             'source grid as this regridder.')
        return eregrid.regrid_area_weighted_rectilinear_src_and_grid(
            cube, self._target_grid_cube, mdtol=self._mdtol)
 def test_no_coordsystem(self):
     cube = iris.tests.stock.lat_lon_cube()
     for coord in cube.coords():
         coord.coord_system = None
     x, y = get_xy_dim_coords(cube)
     self.assertIs(x, cube.coord('longitude'))
     self.assertIs(y, cube.coord('latitude'))
Пример #6
0
    def __init__(self, src_grid, tgt_grid, mdtol=0):
        """
        Create regridder for conversions between source grid and target grid.

        Parameters
        ----------
        src_grid_cube : cube
            The rectilinear iris cube providing the source grid.
        target_grid_cube : cube
            The rectilinear iris cube providing the target grid.
        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 0.

        """
        if not (0 <= mdtol <= 1):
            msg = "Value for mdtol must be in range 0 - 1, got {}."
            raise ValueError(msg.format(mdtol))
        self.mdtol = mdtol

        regrid_info = _regrid_rectilinear_to_rectilinear__prepare(
            src_grid, tgt_grid)

        # Store regrid info.
        self.grid_x = regrid_info.x_coord
        self.grid_y = regrid_info.y_coord
        self.regridder = regrid_info.regridder

        # Record the source grid.
        self.src_grid = get_xy_dim_coords(src_grid)
Пример #7
0
    def __call__(self, cube):
        """
        Regrid this :class:`~iris.cube.Cube` on to the target grid of
        this :class:`LinearRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`LinearRegridder`.

        Args:

        * cube:
            A :class:`~iris.cube.Cube` 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
            linear interpolation.

        """
        if get_xy_dim_coords(cube) != self._src_grid:
            raise ValueError('The given cube is not defined on the same '
                             'source grid as this regridder.')
        return eregrid.regrid_bilinear_rectilinear_src_and_grid(
            cube, self._target_grid_cube,
            extrapolation_mode=self._extrapolation_mode)
Пример #8
0
    def __init__(self, src_cube, tgt_grid_cube, method, projection=None):
        """
        Create a regridder for conversions between the source
        and target grids.

        Args:

        * src_cube:
            The :class:`~iris.cube.Cube` providing the source points.
        * tgt_grid_cube:
            The :class:`~iris.cube.Cube` providing the target grid.
        * method:
            Either 'linear' or 'nearest'.
        * projection:
            The projection in which the interpolation is performed. If None, a
            PlateCarree projection is used. Defaults to None.

        """
        # Validity checks.
        if not isinstance(src_cube, iris.cube.Cube):
            raise TypeError("'src_cube' must be a Cube")
        if not isinstance(tgt_grid_cube, iris.cube.Cube):
            raise TypeError("'tgt_grid_cube' must be a Cube")

        # Snapshot the state of the target cube to ensure that the regridder
        # is impervious to external changes to the original source cubes.
        self._tgt_grid = snapshot_grid(tgt_grid_cube)

        # Check the target grid units.
        for coord in self._tgt_grid:
            self._check_units(coord)

        # Whether to use linear or nearest-neighbour interpolation.
        if method not in ('linear', 'nearest'):
            msg = 'Regridding method {!r} not supported.'.format(method)
            raise ValueError(msg)
        self._method = method

        src_x_coord, src_y_coord = get_xy_coords(src_cube)
        if src_x_coord.coord_system != src_y_coord.coord_system:
            raise ValueError("'src_cube' lateral geographic coordinates have "
                             "differing coordinate sytems.")
        if src_x_coord.coord_system is None:
            raise ValueError("'src_cube' lateral geographic coordinates have "
                             "no coordinate sytem.")
        tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_grid_cube)
        if tgt_x_coord.coord_system != tgt_y_coord.coord_system:
            raise ValueError("'tgt_grid_cube' lateral geographic coordinates "
                             "have differing coordinate sytems.")
        if tgt_x_coord.coord_system is None:
            raise ValueError("'tgt_grid_cube' lateral geographic coordinates "
                             "have no coordinate sytem.")

        if projection is None:
            globe = src_x_coord.coord_system.as_cartopy_globe()
            projection = ccrs.Sinusoidal(globe=globe)
        self._projection = projection
Пример #9
0
def _regrid_rectilinear_to_rectilinear__prepare(src_grid_cube, tgt_grid_cube):
    tgt_x, tgt_y = get_xy_dim_coords(tgt_grid_cube)
    src_x, src_y = get_xy_dim_coords(src_grid_cube)

    grid_x_dim = src_grid_cube.coord_dims(src_x)[0]
    grid_y_dim = src_grid_cube.coord_dims(src_y)[0]

    srcinfo = _cube_to_GridInfo(src_grid_cube)
    tgtinfo = _cube_to_GridInfo(tgt_grid_cube)

    regridder = Regridder(srcinfo, tgtinfo)

    regrid_info = RegridInfo(
        x_dim=grid_x_dim,
        y_dim=grid_y_dim,
        x_coord=tgt_x,
        y_coord=tgt_y,
        regridder=regridder,
    )

    return regrid_info
Пример #10
0
    def __call__(self, cube):
        """
        Regrid this :class:`~iris.cube.Cube` onto the target grid of
        this :class:`AreaWeightedRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`AreaWeightedRegridder`.

        If the source cube has lazy data, the returned cube will also
        have lazy data.

        Args:

        * cube:
            A :class:`~iris.cube.Cube` 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.

        .. note::

            If the source cube has lazy data,
            `chunks <https://docs.dask.org/en/latest/array-chunks.html>`__
            in the horizontal dimensions will be combined before regridding.

        """
        from iris.experimental.regrid import (
            _regrid_area_weighted_rectilinear_src_and_grid__perform, )

        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 _regrid_area_weighted_rectilinear_src_and_grid__perform(
            cube, _regrid_info, mdtol=self._mdtol)
Пример #11
0
    def __call__(self, cube):
        """
        Regrid this :class:`~iris.cube.Cube` onto the target grid of
        this :class:`AreaWeightedRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`AreaWeightedRegridder`.

        Args:

        * cube:
            A :class:`~iris.cube.Cube` 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.

        """
        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, _regrid_info, mdtol=self._mdtol
        )
Пример #12
0
    def __call__(self, cube):
        """
        Regrid this cube onto the target grid of this regridder instance.

        The given cube must be defined with the same grid as the source
        cube used to create this ESMFAreaWeightedRegridder 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.

        """
        src_x, src_y = get_xy_dim_coords(cube)

        # Check the source grid matches that used in initialisation
        if self.src_grid != (src_x, src_y):
            raise ValueError("The given cube is not defined on the same "
                             "source grid as this regridder.")

        grid_x_dim = cube.coord_dims(src_x)[0]
        grid_y_dim = cube.coord_dims(src_y)[0]

        regrid_info = RegridInfo(
            x_dim=grid_x_dim,
            y_dim=grid_y_dim,
            x_coord=self.grid_x,
            y_coord=self.grid_y,
            regridder=self.regridder,
        )

        return _regrid_rectilinear_to_rectilinear__perform(
            cube, regrid_info, self.mdtol)
Пример #13
0
    def __call__(self, cube):
        """
        Regrid this :class:`~iris.cube.Cube` onto the target grid of
        this :class:`AreaWeightedRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`AreaWeightedRegridder`.

        Args:

        * cube:
            A :class:`~iris.cube.Cube` 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.

        """
        if get_xy_dim_coords(cube) != self._src_grid:
            raise ValueError("The given cube is not defined on the same " "source grid as this regridder.")
        return eregrid.regrid_area_weighted_rectilinear_src_and_grid(cube, self._target_grid_cube, mdtol=self._mdtol)
 def test_lat_lon(self):
     cube = iris.tests.stock.lat_lon_cube()
     x, y = get_xy_dim_coords(cube)
     self.assertIs(x, cube.coord('longitude'))
     self.assertIs(y, cube.coord('latitude'))
 def test_grid_lat_lon(self):
     cube = iris.tests.stock.realistic_4d()
     x, y = get_xy_dim_coords(cube)
     self.assertIs(x, cube.coord('grid_longitude'))
     self.assertIs(y, cube.coord('grid_latitude'))
 def test_one_coordsystem(self):
     cube = iris.tests.stock.lat_lon_cube()
     cube.coord('longitude').coord_system = None
     with self.assertRaises(ValueError):
         get_xy_dim_coords(cube)
Пример #17
0
def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube):
    """
    Return a new cube with the data values calculated using the weighted
    mean of data values from :data:`src_cube` and the weights from
    :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`.

    This function requires that the :data:`src_cube` has a curvilinear
    horizontal grid and the target :data:`grid_cube` is rectilinear
    i.e. expressed in terms of two orthogonal 1D horizontal coordinates.
    Both grids must be in the same coordinate system, and the :data:`grid_cube`
    must have horizontal coordinates that are both bounded and contiguous.

    Note that, for any given target :data:`grid_cube` cell, only the points
    from the :data:`src_cube` that are bound by that cell will contribute to
    the cell result. The bounded extent of the :data:`src_cube` will not be
    considered here.

    A target :data:`grid_cube` cell result will be calculated as,
    :math:`\sum (src\_cube.data_{ij} * weights_{ij}) / \sum weights_{ij}`, for
    all :math:`ij` :data:`src_cube` points that are bound by that cell.

    .. warning::

        * Only 2D cubes are supported.
        * All coordinates that span the :data:`src_cube` that don't define
          the horizontal curvilinear grid will be ignored.
        * The :class:`iris.unit.Unit` of the horizontal grid coordinates
          must be either :data:`degrees` or :data:`radians`.

    Args:

    * src_cube:
        A :class:`iris.cube.Cube` instance that defines the source
        variable grid to be regridded.
    * weights:
        A :class:`numpy.ndarray` instance that defines the weights
        for the source variable grid cells. Must have the same shape
        as the :data:`src_cube.data`.
    * grid_cube:
        A :class:`iris.cube.Cube` instance that defines the target
        rectilinear grid.

    Returns:
        A :class:`iris.cube.Cube` instance.

    """
    if src_cube.shape != weights.shape:
        msg = 'The source cube and weights require the same data shape.'
        raise ValueError(msg)

    if src_cube.aux_factories:
        msg = 'All source cube derived coordinates will be ignored.'
        warnings.warn(msg)

    # Get the source cube x and y 2D auxiliary coordinates.
    sx, sy = src_cube.coord(axis='x'), src_cube.coord(axis='y')
    # Get the target grid cube x and y dimension coordinates.
    tx, ty = get_xy_dim_coords(grid_cube)

    if sx.units.modulus is None or sy.units.modulus is None or \
            sx.units != sy.units:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'have units of degrees or radians.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if tx.units.modulus is None or ty.units.modulus is None or \
            tx.units != ty.units:
        msg = 'The target grid cube x ({!r}) and y ({!r}) coordinates must ' \
            'have units of degrees or radians.'
        raise ValueError(msg.format(tx.name(), ty.name()))

    if sx.units != tx.units:
        msg = 'The source cube and target grid cube must have x and y ' \
            'coordinates with the same units.'
        raise ValueError(msg)

    if sx.ndim != sy.ndim:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'have the same dimensionality.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.ndim != 2:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'be 2D auxiliary coordinates.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.coord_system != sy.coord_system:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'have the same coordinate system.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.coord_system != tx.coord_system and \
            sx.coord_system is not None and \
            tx.coord_system is not None:
        msg = 'The source cube and target grid cube must have the same ' \
            'coordinate system.'
        raise ValueError(msg)

    if not tx.has_bounds() or not tx.is_contiguous():
        msg = 'The target grid cube x ({!r})coordinate requires ' \
            'contiguous bounds.'
        raise ValueError(msg.format(tx.name()))

    if not ty.has_bounds() or not ty.is_contiguous():
        msg = 'The target grid cube y ({!r}) coordinate requires ' \
            'contiguous bounds.'
        raise ValueError(msg.format(ty.name()))

    def _src_align_and_flatten(coord):
        # Return a flattened, unmasked copy of a coordinate's points array that
        # will align with a flattened version of the source cube's data.
        points = coord.points
        if src_cube.coord_dims(coord) == (1, 0):
            points = points.T
        if points.shape != src_cube.shape:
            msg = 'The shape of the points array of !r is not compatible ' \
                'with the shape of !r.'.format(coord.name(), src_cube.name())
            raise ValueError(msg)
        return np.asarray(points.flatten())

    # Align and flatten the coordinate points of the source space.
    sx_points = _src_align_and_flatten(sx)
    sy_points = _src_align_and_flatten(sy)

    # Match the source cube x coordinate range to the target grid
    # cube x coordinate range.
    min_sx, min_tx = np.min(sx.points), np.min(tx.points)
    modulus = sx.units.modulus
    if min_sx < 0 and min_tx >= 0:
        indices = np.where(sx_points < 0)
        sx_points[indices] += modulus
    elif min_sx >= 0 and min_tx < 0:
        indices = np.where(sx_points > (modulus / 2))
        sx_points[indices] -= modulus

    # Create target grid cube x and y cell boundaries.
    tx_depth, ty_depth = tx.points.size, ty.points.size
    tx_dim, = grid_cube.coord_dims(tx)
    ty_dim, = grid_cube.coord_dims(ty)

    tx_cells = np.concatenate((tx.bounds[:, 0], tx.bounds[-1, 1].reshape(1)))
    ty_cells = np.concatenate((ty.bounds[:, 0], ty.bounds[-1, 1].reshape(1)))

    # Determine the target grid cube x and y cells that bound
    # the source cube x and y points.

    def _regrid_indices(cells, depth, points):
        # Calculate the minimum difference in cell extent.
        extent = np.min(np.diff(cells))
        if extent == 0:
            # Detected an dimension coordinate with an invalid
            # zero length cell extent.
            msg = 'The target grid cube {} ({!r}) coordinate contains ' \
                'a zero length cell extent.'
            axis, name = 'x', tx.name()
            if points is sy_points:
                axis, name = 'y', ty.name()
            raise ValueError(msg.format(axis, name))
        elif extent > 0:
            # The cells of the dimension coordinate are in ascending order.
            indices = np.searchsorted(cells, points, side='right') - 1
        else:
            # The cells of the dimension coordinate are in descending order.
            # np.searchsorted() requires ascending order, so we require to
            # account for this restriction.
            cells = cells[::-1]
            right = np.searchsorted(cells, points, side='right')
            left = np.searchsorted(cells, points, side='left')
            indices = depth - right
            # Only those points that exactly match the left-hand cell bound
            # will differ between 'left' and 'right'. Thus their appropriate
            # target cell location requires to be recalculated to give the
            # correct descending [upper, lower) interval cell, source to target
            # regrid behaviour.
            delta = np.where(left != right)[0]
            if delta.size:
                indices[delta] = depth - left[delta]
        return indices

    x_indices = _regrid_indices(tx_cells, tx_depth, sx_points)
    y_indices = _regrid_indices(ty_cells, ty_depth, sy_points)

    # Now construct a sparse M x N matix, where M is the flattened target
    # space, and N is the flattened source space. The sparse matrix will then
    # be populated with those source cube points that contribute to a specific
    # target cube cell.

    # Determine the valid indices and their offsets in M x N space.
    if ma.isMaskedArray(src_cube.data):
        # Calculate the valid M offsets, accounting for the source cube mask.
        mask = ~src_cube.data.mask.flatten()
        cols = np.where((y_indices >= 0) & (y_indices < ty_depth)
                        & (x_indices >= 0) & (x_indices < tx_depth) & mask)[0]
    else:
        # Calculate the valid M offsets.
        cols = np.where((y_indices >= 0) & (y_indices < ty_depth)
                        & (x_indices >= 0) & (x_indices < tx_depth))[0]

    # Reduce the indices to only those that are valid.
    x_indices = x_indices[cols]
    y_indices = y_indices[cols]

    # Calculate the valid N offsets.
    if ty_dim < tx_dim:
        rows = y_indices * tx.points.size + x_indices
    else:
        rows = x_indices * ty.points.size + y_indices

    # Calculate the associated valid weights.
    weights_flat = weights.flatten()
    data = weights_flat[cols]

    # Build our sparse M x N matrix of weights.
    sparse_matrix = csc_matrix((data, (rows, cols)),
                               shape=(grid_cube.data.size, src_cube.data.size))

    # Performing a sparse sum to collapse the matrix to (M, 1).
    sum_weights = sparse_matrix.sum(axis=1).getA()

    # Determine the rows (flattened target indices) that have a
    # contribution from one or more source points.
    rows = np.nonzero(sum_weights)

    # Calculate the numerator of the weighted mean (M, 1).
    numerator = sparse_matrix * src_cube.data.reshape(-1, 1)

    # Calculate the weighted mean payload.
    weighted_mean = ma.masked_all(numerator.shape, dtype=numerator.dtype)
    weighted_mean[rows] = numerator[rows] / sum_weights[rows]

    # Construct the final regridded weighted mean cube.
    dim_coords_and_dims = list(zip((ty.copy(), tx.copy()), (ty_dim, tx_dim)))
    cube = iris.cube.Cube(weighted_mean.reshape(grid_cube.shape),
                          dim_coords_and_dims=dim_coords_and_dims)
    cube.metadata = copy.deepcopy(src_cube.metadata)

    for coord in src_cube.coords(dimensions=()):
        cube.add_aux_coord(coord.copy())

    return cube
Пример #18
0
    def __call__(self, src):
        """
        Regrid this :class:`~iris.cube.Cube` on to the target grid of
        this :class:`RectilinearRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`RectilinearRegridder`.

        If the source cube has lazy data, the returned cube will also
        have lazy data.

        Args:

        * src:
            A :class:`~iris.cube.Cube` 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
            either nearest-neighbour or linear interpolation.

        .. note::

            If the source cube has lazy data,
            `chunks <https://docs.dask.org/en/latest/array-chunks.html>`__
            in the horizontal dimensions will be combined before regridding.

        """
        from iris.cube import Cube

        # Validity checks.
        if not isinstance(src, Cube):
            raise TypeError("'src' must be a Cube")
        if get_xy_dim_coords(src) != self._src_grid:
            raise ValueError("The given cube is not defined on the same "
                             "source grid as this regridder.")

        src_x_coord, src_y_coord = get_xy_dim_coords(src)
        grid_x_coord, grid_y_coord = self._tgt_grid
        src_cs = src_x_coord.coord_system
        grid_cs = grid_x_coord.coord_system

        if src_cs is None and grid_cs is None:
            if not (src_x_coord.is_compatible(grid_x_coord)
                    and src_y_coord.is_compatible(grid_y_coord)):
                raise ValueError("The rectilinear grid coordinates of the "
                                 "given cube and target grid have no "
                                 "coordinate system but they do not have "
                                 "matching coordinate metadata.")
        elif src_cs is None or grid_cs is None:
            raise ValueError("The rectilinear grid coordinates of the given "
                             "cube and target grid must either both have "
                             "coordinate systems or both have no coordinate "
                             "system but with matching coordinate metadata.")

        # Check the source grid units.
        for coord in (src_x_coord, src_y_coord):
            self._check_units(coord)

        # Convert the grid to a 2D sample grid in the src CRS.
        sample_grid = self._sample_grid(src_cs, grid_x_coord, grid_y_coord)
        sample_grid_x, sample_grid_y = sample_grid

        # Compute the interpolated data values.
        x_dim = src.coord_dims(src_x_coord)[0]
        y_dim = src.coord_dims(src_y_coord)[0]

        # Define regrid function
        regrid = functools.partial(
            self._regrid,
            x_dim=x_dim,
            y_dim=y_dim,
            src_x_coord=src_x_coord,
            src_y_coord=src_y_coord,
            sample_grid_x=sample_grid_x,
            sample_grid_y=sample_grid_y,
            method=self._method,
            extrapolation_mode=self._extrapolation_mode,
        )

        data = map_complete_blocks(src, regrid, (y_dim, x_dim),
                                   sample_grid_x.shape)

        # Wrap up the data as a Cube.
        regrid_callback = functools.partial(self._regrid,
                                            method=self._method,
                                            extrapolation_mode="nan")
        result = self._create_cube(
            data,
            src,
            x_dim,
            y_dim,
            src_x_coord,
            src_y_coord,
            grid_x_coord,
            grid_y_coord,
            sample_grid_x,
            sample_grid_y,
            regrid_callback,
        )
        return result
Пример #19
0
def regrid_bilinear_rectilinear_src_and_grid(src, grid,
                                             extrapolation_mode='mask'):
    """
    Return a new Cube that is the result of regridding the source Cube
    onto the grid of the grid Cube using bilinear interpolation.

    Both the source and grid Cubes must be defined on rectilinear grids.

    Auxiliary coordinates which span the grid dimensions are ignored,
    except where they provide a reference surface for an
    :class:`iris.aux_factory.AuxCoordFactory`.

    Args:

    * src:
        The source :class:`iris.cube.Cube` providing the data.
    * grid:
        The :class:`iris.cube.Cube` which defines the new grid.

    Kwargs:

    * extrapolation_mode:
        Must be one of the following strings:

          * 'linear' - The extrapolation points will be calculated by
            extending the gradient of the closest two points.
          * 'nan' - The extrapolation points will be be set to NaN.
          * 'error' - A ValueError exception will be raised, notifying an
            attempt to extrapolate.
          * 'mask' - The extrapolation points will always be masked, even
            if the source data is not a MaskedArray.
          * 'nanmask' - If the source data is a MaskedArray the
            extrapolation points will be masked. Otherwise they will be
            set to NaN.

        The default mode of extrapolation is 'mask'.

    Returns:
        The :class:`iris.cube.Cube` resulting from regridding the source
        data onto the grid defined by the grid Cube.

    """
    # Validity checks
    if not isinstance(src, iris.cube.Cube):
        raise TypeError("'src' must be a Cube")
    if not isinstance(grid, iris.cube.Cube):
        raise TypeError("'grid' must be a Cube")
    src_x_coord, src_y_coord = get_xy_dim_coords(src)
    grid_x_coord, grid_y_coord = get_xy_dim_coords(grid)
    src_cs = src_x_coord.coord_system
    grid_cs = grid_x_coord.coord_system
    if src_cs is None and grid_cs is None:
        if not (src_x_coord.is_compatible(grid_x_coord) and
                src_y_coord.is_compatible(grid_y_coord)):
            raise ValueError("The rectilinear grid coordinates of the 'src' "
                             "and 'grid' Cubes have no coordinate system but "
                             "they do not have matching coordinate metadata.")
    elif src_cs is None or grid_cs is None:
        raise ValueError("The rectilinear grid coordinates of the 'src' and "
                         "'grid' Cubes must either both have coordinate "
                         "systems or both have no coordinate system but with "
                         "matching coordinate metadata.")

    def _check_units(coord):
        if coord.coord_system is None:
            # No restriction on units.
            pass
        elif isinstance(coord.coord_system,
                        (iris.coord_systems.GeogCS,
                         iris.coord_systems.RotatedGeogCS)):
            # Units for lat-lon or rotated pole must be 'degrees'. Note
            # that 'degrees_east' etc. are equal to 'degrees'.
            if coord.units != 'degrees':
                msg = "Unsupported units for coordinate system. " \
                      "Expected 'degrees' got {!r}.".format(coord.units)
                raise ValueError(msg)
        else:
            # Units for other coord systems must be equal to metres.
            if coord.units != 'm':
                msg = "Unsupported units for coordinate system. " \
                      "Expected 'metres' got {!r}.".format(coord.units)
                raise ValueError(msg)

    for coord in (src_x_coord, src_y_coord, grid_x_coord, grid_y_coord):
        _check_units(coord)

    if extrapolation_mode not in EXTRAPOLATION_MODES:
        raise ValueError('Invalid extrapolation mode.')

    # Convert the grid to a 2D sample grid in the src CRS.
    sample_grid = RectilinearRegridder._sample_grid(src_cs,
                                                    grid_x_coord, grid_y_coord)
    sample_grid_x, sample_grid_y = sample_grid

    # Compute the interpolated data values.
    x_dim = src.coord_dims(src_x_coord)[0]
    y_dim = src.coord_dims(src_y_coord)[0]
    data = RectilinearRegridder._regrid_bilinear_array(src.data, x_dim, y_dim,
                                                       src_x_coord,
                                                       src_y_coord,
                                                       sample_grid_x,
                                                       sample_grid_y,
                                                       extrapolation_mode)

    # Wrap up the data as a Cube.
    regrid_callback = partial(RectilinearRegridder._regrid_bilinear_array,
                              extrapolation_mode='nan')
    result = RectilinearRegridder._create_cube(data, src, x_dim, y_dim,
                                               src_x_coord, src_y_coord,
                                               grid_x_coord, grid_y_coord,
                                               sample_grid_x, sample_grid_y,
                                               regrid_callback)
    return result
Пример #20
0
    def __call__(self, src):
        """
        Regrid this :class:`~iris.cube.Cube` on to the target grid of
        this :class:`RectilinearRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`RectilinearRegridder`.

        Args:

        * src:
            A :class:`~iris.cube.Cube` 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
            either nearest-neighbour or linear interpolation.

        """
        # Validity checks.
        if not isinstance(src, iris.cube.Cube):
            raise TypeError("'src' must be a Cube")
        if get_xy_dim_coords(src) != self._src_grid:
            raise ValueError('The given cube is not defined on the same '
                             'source grid as this regridder.')

        src_x_coord, src_y_coord = get_xy_dim_coords(src)
        grid_x_coord, grid_y_coord = self._tgt_grid
        src_cs = src_x_coord.coord_system
        grid_cs = grid_x_coord.coord_system

        if src_cs is None and grid_cs is None:
            if not (src_x_coord.is_compatible(grid_x_coord)
                    and src_y_coord.is_compatible(grid_y_coord)):
                raise ValueError("The rectilinear grid coordinates of the "
                                 "given cube and target grid have no "
                                 "coordinate system but they do not have "
                                 "matching coordinate metadata.")
        elif src_cs is None or grid_cs is None:
            raise ValueError("The rectilinear grid coordinates of the given "
                             "cube and target grid must either both have "
                             "coordinate systems or both have no coordinate "
                             "system but with matching coordinate metadata.")

        # Check the source grid units.
        for coord in (src_x_coord, src_y_coord):
            self._check_units(coord)

        # Convert the grid to a 2D sample grid in the src CRS.
        sample_grid = self._sample_grid(src_cs, grid_x_coord, grid_y_coord)
        sample_grid_x, sample_grid_y = sample_grid

        # Compute the interpolated data values.
        x_dim = src.coord_dims(src_x_coord)[0]
        y_dim = src.coord_dims(src_y_coord)[0]
        data = self._regrid(src.data, x_dim, y_dim, src_x_coord, src_y_coord,
                            sample_grid_x, sample_grid_y, self._method,
                            self._extrapolation_mode)

        # Wrap up the data as a Cube.
        regrid_callback = functools.partial(self._regrid,
                                            method=self._method,
                                            extrapolation_mode='nan')
        result = self._create_cube(data, src, x_dim, y_dim, src_x_coord,
                                   src_y_coord, grid_x_coord, grid_y_coord,
                                   sample_grid_x, sample_grid_y,
                                   regrid_callback)
        return result
 def test_missing_y_coord(self):
     cube = iris.tests.stock.realistic_4d()
     cube.remove_coord('grid_latitude')
     with self.assertRaises(ValueError):
         get_xy_dim_coords(cube)
Пример #22
0
    def __call__(self, src_cube):
        """
        Regrid the provided :class:`~iris.cube.Cube` on to the target grid
        of this :class:`AreaWeightedRegridder`.

        The supplied :class:`~iris.cube.Cube` must be defined with the same
        grid as the source grid used to create this
        :class:`AreaWeightedRegridder`.

        Args:

        * src_cube:
            A :class:`~iris.cube.Cube` to be regridded.

        Returns:
            A :class:`~iris.cube.Cube` defined with the horizontal dimensions
            of the target and the other dimensions from the supplied source
            :class:`~iris.cube.Cube`. The data values of the supplied source
            :class:`~iris.cube.Cube` will be converted to values on the new
            grid using conservative area-weighted regridding.

        """
        # Sanity check the supplied source cube.
        if not isinstance(src_cube, iris.cube.Cube):
            raise TypeError('The source must be a cube.')

        # Get the source cube x and y coordinates.
        sx, sy = get_xy_dim_coords(src_cube)
        if (sx, sy) != self._src_grid:
            emsg = 'The source cube is not defined on the same source grid ' \
                'as this regridder.'
            raise ValueError(emsg)
        if sx.coord_system is None:
            msg = 'The source cube requires a native coordinate system.'
            raise ValueError(msg)

        # Convert the contiguous bounds of the grid to the source crs.
        gxx, gyy = np.meshgrid(self._gx.contiguous_bounds(),
                               self._gy.contiguous_bounds())

        # Now calculate and cache the grid bounds in the source crs.
        if self._gx_bounds is None or self._gy_bounds is None:
            if sx.coord_system == self._gx.coord_system:
                self._gx_bounds, self._gy_bounds = gxx, gyy
            else:
                from_crs = self._gx.coord_system.as_cartopy_crs()
                to_crs = sx.coord_system.as_cartopy_crs()
                xyz = to_crs.transform_points(from_crs, gxx, gyy)
                self._gx_bounds, self._gy_bounds = xyz[..., 0], xyz[..., 1]

        # Calculate and cache the source contiguous bounds.
        if self._sx_bounds is None or self._sy_bounds is None:
            self._sx_bounds = sx.contiguous_bounds()
            self._sy_bounds = sy.contiguous_bounds()

        sx_dim = src_cube.coord_dims(sx)[0]
        sy_dim = src_cube.coord_dims(sy)[0]

        # Perform the regrid.
        result = agg(src_cube.data, sx.points, self._sx_bounds,
                     sy.points, self._sy_bounds, sx_dim, sy_dim,
                     self._gx_bounds, self._gy_bounds)

        #
        # XXX: Need to deal the factories when constructing result cube.
        #
        result_cube = iris.cube.Cube(result)
        result_cube.metadata = copy.deepcopy(src_cube.metadata)
        coord_mapping = {}

        def copy_coords(coords, add_coord):
            for coord in coords:
                dims = src_cube.coord_dims(coord)
                if coord is sx:
                    coord = self._gx
                elif coord is sy:
                    coord = self._gy
                elif sx_dim in dims or sy_dim in dims:
                    continue
                result_coord = coord.copy()
                add_coord(result_coord, dims)
                coord_mapping[id(coord)] = result_coord

        copy_coords(src_cube.dim_coords, result_cube.add_dim_coord)
        copy_coords(src_cube.aux_coords, result_cube.add_aux_coord)

        return result_cube
Пример #23
0
def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube):
    """
    Return a new cube with the data values calculated using the weighted
    mean of data values from :data:`src_cube` and the weights from
    :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`.

    This function requires that the :data:`src_cube` has a curvilinear
    horizontal grid and the target :data:`grid_cube` is rectilinear
    i.e. expressed in terms of two orthogonal 1D horizontal coordinates.
    Both grids must be in the same coordinate system, and the :data:`grid_cube`
    must have horizontal coordinates that are both bounded and contiguous.

    Note that, for any given target :data:`grid_cube` cell, only the points
    from the :data:`src_cube` that are bound by that cell will contribute to
    the cell result. The bounded extent of the :data:`src_cube` will not be
    considered here.

    A target :data:`grid_cube` cell result will be calculated as,
    :math:`\sum (src\_cube.data_{ij} * weights_{ij}) / \sum weights_{ij}`, for
    all :math:`ij` :data:`src_cube` points that are bound by that cell.

    .. warning::

        * Only 2D cubes are supported.
        * All coordinates that span the :data:`src_cube` that don't define
          the horizontal curvilinear grid will be ignored.
        * The :class:`iris.unit.Unit` of the horizontal grid coordinates
          must be either :data:`degrees` or :data:`radians`.

    Args:

    * src_cube:
        A :class:`iris.cube.Cube` instance that defines the source
        variable grid to be regridded.
    * weights:
        A :class:`numpy.ndarray` instance that defines the weights
        for the source variable grid cells. Must have the same shape
        as the :data:`src_cube.data`.
    * grid_cube:
        A :class:`iris.cube.Cube` instance that defines the target
        rectilinear grid.

    Returns:
        A :class:`iris.cube.Cube` instance.

    """
    if src_cube.shape != weights.shape:
        msg = 'The source cube and weights require the same data shape.'
        raise ValueError(msg)

    if src_cube.aux_factories:
        msg = 'All source cube derived coordinates will be ignored.'
        warnings.warn(msg)

    # Get the source cube x and y 2D auxiliary coordinates.
    sx, sy = src_cube.coord(axis='x'), src_cube.coord(axis='y')
    # Get the target grid cube x and y dimension coordinates.
    tx, ty = get_xy_dim_coords(grid_cube)

    if sx.units.modulus is None or sy.units.modulus is None or \
            sx.units != sy.units:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'have units of degrees or radians.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if tx.units.modulus is None or ty.units.modulus is None or \
            tx.units != ty.units:
        msg = 'The target grid cube x ({!r}) and y ({!r}) coordinates must ' \
            'have units of degrees or radians.'
        raise ValueError(msg.format(tx.name(), ty.name()))

    if sx.units != tx.units:
        msg = 'The source cube and target grid cube must have x and y ' \
            'coordinates with the same units.'
        raise ValueError(msg)

    if sx.ndim != sy.ndim:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'have the same dimensionality.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.ndim != 2:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'be 2D auxiliary coordinates.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.coord_system != sy.coord_system:
        msg = 'The source cube x ({!r}) and y ({!r}) coordinates must ' \
            'have the same coordinate system.'
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.coord_system != tx.coord_system and \
            sx.coord_system is not None and \
            tx.coord_system is not None:
        msg = 'The source cube and target grid cube must have the same ' \
            'coordinate system.'
        raise ValueError(msg)

    if not tx.has_bounds() or not tx.is_contiguous():
        msg = 'The target grid cube x ({!r})coordinate requires ' \
            'contiguous bounds.'
        raise ValueError(msg.format(tx.name()))

    if not ty.has_bounds() or not ty.is_contiguous():
        msg = 'The target grid cube y ({!r}) coordinate requires ' \
            'contiguous bounds.'
        raise ValueError(msg.format(ty.name()))

    def _src_align_and_flatten(coord):
        # Return a flattened, unmasked copy of a coordinate's points array that
        # will align with a flattened version of the source cube's data.
        points = coord.points
        if src_cube.coord_dims(coord) == (1, 0):
            points = points.T
        if points.shape != src_cube.shape:
            msg = 'The shape of the points array of !r is not compatible ' \
                'with the shape of !r.'.format(coord.name(), src_cube.name())
            raise ValueError(msg)
        return np.asarray(points.flatten())

    # Align and flatten the coordinate points of the source space.
    sx_points = _src_align_and_flatten(sx)
    sy_points = _src_align_and_flatten(sy)

    # Match the source cube x coordinate range to the target grid
    # cube x coordinate range.
    min_sx, min_tx = np.min(sx.points), np.min(tx.points)
    modulus = sx.units.modulus
    if min_sx < 0 and min_tx >= 0:
        indices = np.where(sx_points < 0)
        sx_points[indices] += modulus
    elif min_sx >= 0 and min_tx < 0:
        indices = np.where(sx_points > (modulus / 2))
        sx_points[indices] -= modulus

    # Create target grid cube x and y cell boundaries.
    tx_depth, ty_depth = tx.points.size, ty.points.size
    tx_dim, = grid_cube.coord_dims(tx)
    ty_dim, = grid_cube.coord_dims(ty)

    tx_cells = np.concatenate((tx.bounds[:, 0],
                               tx.bounds[-1, 1].reshape(1)))
    ty_cells = np.concatenate((ty.bounds[:, 0],
                               ty.bounds[-1, 1].reshape(1)))

    # Determine the target grid cube x and y cells that bound
    # the source cube x and y points.

    def _regrid_indices(cells, depth, points):
        # Calculate the minimum difference in cell extent.
        extent = np.min(np.diff(cells))
        if extent == 0:
            # Detected an dimension coordinate with an invalid
            # zero length cell extent.
            msg = 'The target grid cube {} ({!r}) coordinate contains ' \
                'a zero length cell extent.'
            axis, name = 'x', tx.name()
            if points is sy_points:
                axis, name = 'y', ty.name()
            raise ValueError(msg.format(axis, name))
        elif extent > 0:
            # The cells of the dimension coordinate are in ascending order.
            indices = np.searchsorted(cells, points, side='right') - 1
        else:
            # The cells of the dimension coordinate are in descending order.
            # np.searchsorted() requires ascending order, so we require to
            # account for this restriction.
            cells = cells[::-1]
            right = np.searchsorted(cells, points, side='right')
            left = np.searchsorted(cells, points, side='left')
            indices = depth - right
            # Only those points that exactly match the left-hand cell bound
            # will differ between 'left' and 'right'. Thus their appropriate
            # target cell location requires to be recalculated to give the
            # correct descending [upper, lower) interval cell, source to target
            # regrid behaviour.
            delta = np.where(left != right)[0]
            if delta.size:
                indices[delta] = depth - left[delta]
        return indices

    x_indices = _regrid_indices(tx_cells, tx_depth, sx_points)
    y_indices = _regrid_indices(ty_cells, ty_depth, sy_points)

    # Now construct a sparse M x N matix, where M is the flattened target
    # space, and N is the flattened source space. The sparse matrix will then
    # be populated with those source cube points that contribute to a specific
    # target cube cell.

    # Determine the valid indices and their offsets in M x N space.
    if ma.isMaskedArray(src_cube.data):
        # Calculate the valid M offsets, accounting for the source cube mask.
        mask = ~src_cube.data.mask.flatten()
        cols = np.where((y_indices >= 0) & (y_indices < ty_depth) &
                        (x_indices >= 0) & (x_indices < tx_depth) &
                        mask)[0]
    else:
        # Calculate the valid M offsets.
        cols = np.where((y_indices >= 0) & (y_indices < ty_depth) &
                        (x_indices >= 0) & (x_indices < tx_depth))[0]

    # Reduce the indices to only those that are valid.
    x_indices = x_indices[cols]
    y_indices = y_indices[cols]

    # Calculate the valid N offsets.
    if ty_dim < tx_dim:
        rows = y_indices * tx.points.size + x_indices
    else:
        rows = x_indices * ty.points.size + y_indices

    # Calculate the associated valid weights.
    weights_flat = weights.flatten()
    data = weights_flat[cols]

    # Build our sparse M x N matrix of weights.
    sparse_matrix = csc_matrix((data, (rows, cols)),
                               shape=(grid_cube.data.size, src_cube.data.size))

    # Performing a sparse sum to collapse the matrix to (M, 1).
    sum_weights = sparse_matrix.sum(axis=1).getA()

    # Determine the rows (flattened target indices) that have a
    # contribution from one or more source points.
    rows = np.nonzero(sum_weights)

    # Calculate the numerator of the weighted mean (M, 1).
    numerator = sparse_matrix * src_cube.data.reshape(-1, 1)

    # Calculate the weighted mean payload.
    weighted_mean = ma.masked_all(numerator.shape, dtype=numerator.dtype)
    weighted_mean[rows] = numerator[rows] / sum_weights[rows]

    # Construct the final regridded weighted mean cube.
    dim_coords_and_dims = list(zip((ty.copy(), tx.copy()), (ty_dim, tx_dim)))
    cube = iris.cube.Cube(weighted_mean.reshape(grid_cube.shape),
                          dim_coords_and_dims=dim_coords_and_dims)
    cube.metadata = copy.deepcopy(src_cube.metadata)

    for coord in src_cube.coords(dimensions=()):
        cube.add_aux_coord(coord.copy())

    return cube
Пример #24
0
def regrid_conservative_via_esmpy(source_cube, grid_cube):
    """
    Perform a conservative regridding with ESMPy.

    Regrids the data of a source cube onto a new grid defined by a destination
    cube.

    Args:

    * source_cube (:class:`iris.cube.Cube`):
        Source data.  Must have two identifiable horizontal dimension
        coordinates.
    * grid_cube (:class:`iris.cube.Cube`):
        Define the target horizontal grid:  Only the horizontal dimension
        coordinates are actually used.

    Returns:
        A new cube derived from source_cube, regridded onto the specified
        horizontal grid.

    Any additional coordinates which map onto the horizontal dimensions are
    removed, while all other metadata is retained.
    If there are coordinate factories with 2d horizontal reference surfaces,
    the reference surfaces are also regridded, using ordinary bilinear
    interpolation.

    .. note::

        Both source and destination cubes must have two dimension coordinates
        identified with axes 'X' and 'Y' which share a coord_system with a
        Cartopy CRS.
        The grids are defined by :meth:`iris.coords.Coord.contiguous_bounds` of
        these.

    .. note::

        Initialises the ESMF Manager, if it was not already called.
        This implements default Manager operations (e.g. logging).

        To alter this, make a prior call to ESMF.Manager().

    """
    # Lazy import so we can build the docs with no ESMF.
    import ESMF

    # Get source + target XY coordinate pairs and check they are suitable.
    src_coords = get_xy_dim_coords(source_cube)
    dst_coords = get_xy_dim_coords(grid_cube)
    src_cs = src_coords[0].coord_system
    grid_cs = dst_coords[0].coord_system
    if src_cs is None or grid_cs is None:
        raise ValueError("Both 'src' and 'grid' Cubes must have a"
                         " coordinate system for their rectilinear grid"
                         " coordinates.")

    if src_cs.as_cartopy_crs() is None or grid_cs.as_cartopy_crs() is None:
        raise ValueError("Both 'src' and 'grid' Cubes coord_systems must have "
                         "a valid associated Cartopy CRS.")

    def _valid_units(coord):
        if isinstance(coord.coord_system, (iris.coord_systems.GeogCS,
                                           iris.coord_systems.RotatedGeogCS)):
            valid_units = 'degrees'
        else:
            valid_units = 'm'
        return coord.units == valid_units

    if not all(_valid_units(coord) for coord in src_coords + dst_coords):
        raise ValueError("Unsupported units: must be 'degrees' or 'm'.")

    # Initialise the ESMF manager in case it was not already done.
    ESMF.Manager()

    # Create a data array for the output cube.
    src_dims_xy = [source_cube.coord_dims(coord)[0] for coord in src_coords]
    # Size matches source, except for X+Y dimensions
    dst_shape = np.array(source_cube.shape)
    dst_shape[src_dims_xy] = [coord.shape[0] for coord in dst_coords]
    # NOTE: result array is masked -- fix this afterward if all unmasked
    fullcube_data = np.ma.zeros(dst_shape)

    # Iterate 2d slices over all possible indices of the 'other' dimensions
    all_other_dims = [i_dim for i_dim in range(source_cube.ndim)
                      if i_dim not in src_dims_xy]
    all_combinations_of_other_inds = np.ndindex(*dst_shape[all_other_dims])
    for other_indices in all_combinations_of_other_inds:
        # Construct a tuple of slices to address the 2d xy field
        slice_indices_array = np.array([slice(None)] * source_cube.ndim)
        slice_indices_array[all_other_dims] = other_indices
        slice_indices_tuple = tuple(slice_indices_array)

        # Get the source data, reformed into the right dimension order, (x,y).
        src_data_2d = source_cube.data[slice_indices_tuple]
        if (src_dims_xy[0] > src_dims_xy[1]):
            src_data_2d = src_data_2d.transpose()

        # Work out whether we have missing data to define a source grid mask.
        if np.ma.is_masked(src_data_2d):
            srcdata_mask = np.ma.getmask(src_data_2d)
        else:
            srcdata_mask = None

        # Construct ESMF Field objects on source and destination grids.
        src_field = _make_esmpy_field(src_coords[0], src_coords[1],
                                      data=src_data_2d, mask=srcdata_mask)
        dst_field = _make_esmpy_field(dst_coords[0], dst_coords[1])

        # Make Field for destination coverage fraction (for missing data calc).
        coverage_field = ESMF.Field(dst_field.grid, 'validmask_dst')

        # Do the actual regrid with ESMF.
        mask_flag_values = np.array([1], dtype=np.int32)
        regrid_method = ESMF.Regrid(src_field, dst_field,
                                    src_mask_values=mask_flag_values,
                                    regrid_method=ESMF.RegridMethod.CONSERVE,
                                    unmapped_action=ESMF.UnmappedAction.IGNORE,
                                    dst_frac_field=coverage_field)
        regrid_method(src_field, dst_field)
        data = np.ma.masked_array(dst_field.data)

        # Convert destination 'coverage fraction' into a missing-data mask.
        # Set = wherever part of cell goes outside source grid, or overlaps a
        # masked source cell.
        coverage_tolerance_threshold = 1.0 - 1.0e-8
        data.mask = coverage_field.data < coverage_tolerance_threshold

        # Transpose ESMF result dims (X,Y) back to the order of the source
        if (src_dims_xy[0] > src_dims_xy[1]):
            data = data.transpose()

        # Paste regridded slice back into parent array
        fullcube_data[slice_indices_tuple] = data

    # Remove the data mask if completely unused.
    if not np.ma.is_masked(fullcube_data):
        fullcube_data = np.array(fullcube_data)

    # Generate a full 2d sample grid, as required for regridding orography
    # NOTE: as seen in "regrid_bilinear_rectilinear_src_and_grid"
    # TODO: can this not also be wound into the _create_cube method ?
    src_cs = src_coords[0].coord_system
    sample_grid_x, sample_grid_y = RectilinearRegridder._sample_grid(
        src_cs, dst_coords[0], dst_coords[1])

    # Return result as a new cube based on the source.
    # TODO: please tidy this interface !!!
    return RectilinearRegridder._create_cube(
        fullcube_data,
        src=source_cube,
        x_dim=src_dims_xy[0],
        y_dim=src_dims_xy[1],
        src_x_coord=src_coords[0],
        src_y_coord=src_coords[1],
        grid_x_coord=dst_coords[0],
        grid_y_coord=dst_coords[1],
        sample_grid_x=sample_grid_x,
        sample_grid_y=sample_grid_y,
        regrid_callback=RectilinearRegridder._regrid)
Пример #25
0
def regrid_conservative_via_esmpy(source_cube, grid_cube):
    """
    Perform a conservative regridding with ESMPy.

    Regrids the data of a source cube onto a new grid defined by a destination
    cube.

    Args:

    * source_cube (:class:`iris.cube.Cube`):
        Source data.  Must have two identifiable horizontal dimension
        coordinates.
    * grid_cube (:class:`iris.cube.Cube`):
        Define the target horizontal grid:  Only the horizontal dimension
        coordinates are actually used.

    Returns:
        A new cube derived from source_cube, regridded onto the specified
        horizontal grid.

    Any additional coordinates which map onto the horizontal dimensions are
    removed, while all other metadata is retained.
    If there are coordinate factories with 2d horizontal reference surfaces,
    the reference surfaces are also regridded, using ordinary bilinear
    interpolation.

    .. note::

        Both source and destination cubes must have two dimension coordinates
        identified with axes 'X' and 'Y' which share a coord_system with a
        Cartopy CRS.
        The grids are defined by :meth:`iris.coords.Coord.contiguous_bounds` of
        these.

    .. note::

        Initialises the ESMF Manager, if it was not already called.
        This implements default Manager operations (e.g. logging).

        To alter this, make a prior call to ESMF.Manager().

    """
    # Lazy import so we can build the docs with no ESMF.
    import ESMF

    # Get source + target XY coordinate pairs and check they are suitable.
    src_coords = get_xy_dim_coords(source_cube)
    dst_coords = get_xy_dim_coords(grid_cube)
    src_cs = src_coords[0].coord_system
    grid_cs = dst_coords[0].coord_system
    if src_cs is None or grid_cs is None:
        raise ValueError("Both 'src' and 'grid' Cubes must have a"
                         " coordinate system for their rectilinear grid"
                         " coordinates.")

    if src_cs.as_cartopy_crs() is None or grid_cs.as_cartopy_crs() is None:
        raise ValueError("Both 'src' and 'grid' Cubes coord_systems must have "
                         "a valid associated Cartopy CRS.")

    def _valid_units(coord):
        if isinstance(coord.coord_system, (iris.coord_systems.GeogCS,
                                           iris.coord_systems.RotatedGeogCS)):
            valid_units = 'degrees'
        else:
            valid_units = 'm'
        return coord.units == valid_units

    if not all(_valid_units(coord) for coord in src_coords + dst_coords):
        raise ValueError("Unsupported units: must be 'degrees' or 'm'.")

    # Initialise the ESMF manager in case it was not already done.
    ESMF.Manager()

    # Create a data array for the output cube.
    src_dims_xy = [source_cube.coord_dims(coord)[0] for coord in src_coords]
    # Size matches source, except for X+Y dimensions
    dst_shape = np.array(source_cube.shape)
    dst_shape[src_dims_xy] = [coord.shape[0] for coord in dst_coords]
    # NOTE: result array is masked -- fix this afterward if all unmasked
    fullcube_data = np.ma.zeros(dst_shape)

    # Iterate 2d slices over all possible indices of the 'other' dimensions
    all_other_dims = [i_dim for i_dim in range(source_cube.ndim)
                      if i_dim not in src_dims_xy]
    all_combinations_of_other_inds = np.ndindex(*dst_shape[all_other_dims])
    for other_indices in all_combinations_of_other_inds:
        # Construct a tuple of slices to address the 2d xy field
        slice_indices_array = np.array([slice(None)] * source_cube.ndim)
        slice_indices_array[all_other_dims] = other_indices
        slice_indices_tuple = tuple(slice_indices_array)

        # Get the source data, reformed into the right dimension order, (x,y).
        src_data_2d = source_cube.data[slice_indices_tuple]
        if (src_dims_xy[0] > src_dims_xy[1]):
            src_data_2d = src_data_2d.transpose()

        # Work out whether we have missing data to define a source grid mask.
        if np.ma.is_masked(src_data_2d):
            srcdata_mask = np.ma.getmask(src_data_2d)
        else:
            srcdata_mask = None

        # Construct ESMF Field objects on source and destination grids.
        src_field = _make_esmpy_field(src_coords[0], src_coords[1],
                                      data=src_data_2d, mask=srcdata_mask)
        dst_field = _make_esmpy_field(dst_coords[0], dst_coords[1])

        # Make Field for destination coverage fraction (for missing data calc).
        coverage_field = ESMF.Field(dst_field.grid, 'validmask_dst')

        # Do the actual regrid with ESMF.
        mask_flag_values = np.array([1], dtype=np.int32)
        regrid_method = ESMF.Regrid(src_field, dst_field,
                                    src_mask_values=mask_flag_values,
                                    regrid_method=ESMF.RegridMethod.CONSERVE,
                                    unmapped_action=ESMF.UnmappedAction.IGNORE,
                                    dst_frac_field=coverage_field)
        regrid_method(src_field, dst_field)
        data = dst_field.data

        # Convert destination 'coverage fraction' into a missing-data mask.
        # Set = wherever part of cell goes outside source grid, or overlaps a
        # masked source cell.
        coverage_tolerance_threshold = 1.0 - 1.0e-8
        data.mask = coverage_field.data < coverage_tolerance_threshold

        # Transpose ESMF result dims (X,Y) back to the order of the source
        if (src_dims_xy[0] > src_dims_xy[1]):
            data = data.transpose()

        # Paste regridded slice back into parent array
        fullcube_data[slice_indices_tuple] = data

    # Remove the data mask if completely unused.
    if not np.ma.is_masked(fullcube_data):
        fullcube_data = np.array(fullcube_data)

    # Generate a full 2d sample grid, as required for regridding orography
    # NOTE: as seen in "regrid_bilinear_rectilinear_src_and_grid"
    # TODO: can this not also be wound into the _create_cube method ?
    src_cs = src_coords[0].coord_system
    sample_grid_x, sample_grid_y = RectilinearRegridder._sample_grid(
        src_cs, dst_coords[0], dst_coords[1])

    # Return result as a new cube based on the source.
    # TODO: please tidy this interface !!!
    return RectilinearRegridder._create_cube(
        fullcube_data,
        src=source_cube,
        x_dim=src_dims_xy[0],
        y_dim=src_dims_xy[1],
        src_x_coord=src_coords[0],
        src_y_coord=src_coords[1],
        grid_x_coord=dst_coords[0],
        grid_y_coord=dst_coords[1],
        sample_grid_x=sample_grid_x,
        sample_grid_y=sample_grid_y,
        regrid_callback=RectilinearRegridder._regrid)
Пример #26
0
    def __call__(self, src_cube):
        """
        Regrid the provided :class:`~iris.cube.Cube` on to the target grid
        of this :class:`AreaWeightedRegridder`.

        The supplied :class:`~iris.cube.Cube` must be defined with the same
        grid as the source grid used to create this
        :class:`AreaWeightedRegridder`.

        Args:

        * src_cube:
            A :class:`~iris.cube.Cube` to be regridded.

        Returns:
            A :class:`~iris.cube.Cube` defined with the horizontal dimensions
            of the target and the other dimensions from the supplied source
            :class:`~iris.cube.Cube`. The data values of the supplied source
            :class:`~iris.cube.Cube` will be converted to values on the new
            grid using conservative area-weighted regridding.

        """
        # Sanity check the supplied source cube.
        if not isinstance(src_cube, iris.cube.Cube):
            raise TypeError('The source must be a cube.')

        # Get the source cube x and y coordinates.
        sx, sy = get_xy_dim_coords(src_cube)
        if (sx, sy) != self._src_grid:
            emsg = 'The source cube is not defined on the same source grid ' \
                'as this regridder.'
            raise ValueError(emsg)
        if sx.coord_system is None:
            msg = 'The source cube requires a native coordinate system.'
            raise ValueError(msg)

        # Convert the contiguous bounds of the grid to the source crs.
        gxx, gyy = np.meshgrid(self._gx.contiguous_bounds(),
                               self._gy.contiguous_bounds())

        # Now calculate and cache the grid bounds in the source crs.
        if self._gx_bounds is None or self._gy_bounds is None:
            if sx.coord_system == self._gx.coord_system:
                self._gx_bounds, self._gy_bounds = gxx, gyy
            else:
                from_crs = self._gx.coord_system.as_cartopy_crs()
                to_crs = sx.coord_system.as_cartopy_crs()
                xyz = to_crs.transform_points(from_crs, gxx, gyy)
                self._gx_bounds, self._gy_bounds = xyz[..., 0], xyz[..., 1]

        # Calculate and cache the source contiguous bounds.
        if self._sx_bounds is None or self._sy_bounds is None:
            self._sx_bounds = sx.contiguous_bounds()
            self._sy_bounds = sy.contiguous_bounds()

        sx_dim = src_cube.coord_dims(sx)[0]
        sy_dim = src_cube.coord_dims(sy)[0]

        # Perform the regrid.
        result = agg(src_cube.data, sx.points, self._sx_bounds, sy.points,
                     self._sy_bounds, sx_dim, sy_dim, self._gx_bounds,
                     self._gy_bounds)

        #
        # XXX: Need to deal the factories when constructing result cube.
        #
        result_cube = iris.cube.Cube(result)
        result_cube.metadata = copy.deepcopy(src_cube.metadata)
        coord_mapping = {}

        def copy_coords(coords, add_coord):
            for coord in coords:
                dims = src_cube.coord_dims(coord)
                if coord is sx:
                    coord = self._gx
                elif coord is sy:
                    coord = self._gy
                elif sx_dim in dims or sy_dim in dims:
                    continue
                result_coord = coord.copy()
                add_coord(result_coord, dims)
                coord_mapping[id(coord)] = result_coord

        copy_coords(src_cube.dim_coords, result_cube.add_dim_coord)
        copy_coords(src_cube.aux_coords, result_cube.add_aux_coord)

        return result_cube
Пример #27
0
def _regrid_weighted_curvilinear_to_rectilinear__prepare(
        src_cube, weights, grid_cube):
    """
    First (setup) part of 'regrid_weighted_curvilinear_to_rectilinear'.

    Check inputs and calculate the sparse regrid matrix and related info.
    The 'regrid info' returned can be re-used over many 2d slices.

    """
    if src_cube.aux_factories:
        msg = "All source cube derived coordinates will be ignored."
        warnings.warn(msg)

    # Get the source cube x and y 2D auxiliary coordinates.
    sx, sy = src_cube.coord(axis="x"), src_cube.coord(axis="y")
    # Get the target grid cube x and y dimension coordinates.
    tx, ty = get_xy_dim_coords(grid_cube)

    if sx.units != sy.units:
        msg = ("The source cube x ({!r}) and y ({!r}) coordinates must "
               "have the same units.")
        raise ValueError(msg.format(sx.name(), sy.name()))

    if src_cube.coord_dims(sx) != src_cube.coord_dims(sy):
        msg = ("The source cube x ({!r}) and y ({!r}) coordinates must "
               "map onto the same cube dimensions.")
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.coord_system != sy.coord_system:
        msg = ("The source cube x ({!r}) and y ({!r}) coordinates must "
               "have the same coordinate system.")
        raise ValueError(msg.format(sx.name(), sy.name()))

    if sx.coord_system is None:
        msg = ("The source X and Y coordinates must have a defined "
               "coordinate system.")
        raise ValueError(msg)

    if tx.units != ty.units:
        msg = ("The target grid cube x ({!r}) and y ({!r}) coordinates must "
               "have the same units.")
        raise ValueError(msg.format(tx.name(), ty.name()))

    if tx.coord_system is None:
        msg = ("The target X and Y coordinates must have a defined "
               "coordinate system.")
        raise ValueError(msg)

    if tx.coord_system != ty.coord_system:
        msg = ("The target grid cube x ({!r}) and y ({!r}) coordinates must "
               "have the same coordinate system.")
        raise ValueError(msg.format(tx.name(), ty.name()))

    if weights is None:
        weights = np.ones(sx.shape)
    if weights.shape != sx.shape:
        msg = ("Provided weights must have the same shape as the X and Y "
               "coordinates.")
        raise ValueError(msg)

    if not tx.has_bounds() or not tx.is_contiguous():
        msg = ("The target grid cube x ({!r})coordinate requires "
               "contiguous bounds.")
        raise ValueError(msg.format(tx.name()))

    if not ty.has_bounds() or not ty.is_contiguous():
        msg = ("The target grid cube y ({!r}) coordinate requires "
               "contiguous bounds.")
        raise ValueError(msg.format(ty.name()))

    def _src_align_and_flatten(coord):
        # Return a flattened, unmasked copy of a coordinate's points array that
        # will align with a flattened version of the source cube's data.
        #
        # PP-TODO: Should work with any cube dimensions for X and Y coords.
        #  Probably needs fixing anyway?
        #
        points = coord.points
        if src_cube.coord_dims(coord) == (1, 0):
            points = points.T
        if points.shape != src_cube.shape:
            msg = ("The shape of the points array of {!r} is not compatible "
                   "with the shape of {!r}.")
            raise ValueError(msg.format(coord.name(), src_cube.name()))
        return np.asarray(points.flatten())

    # Align and flatten the coordinate points of the source space.
    sx_points = _src_align_and_flatten(sx)
    sy_points = _src_align_and_flatten(sy)

    # Transform source X and Y points into the target coord-system, if needed.
    if sx.coord_system != tx.coord_system:
        src_crs = sx.coord_system.as_cartopy_projection()
        tgt_crs = tx.coord_system.as_cartopy_projection()
        sx_points, sy_points = _transform_xy_arrays(src_crs, sx_points,
                                                    sy_points, tgt_crs)
    #
    # TODO: how does this work with scaled units ??
    #  e.g. if crs is latlon, units could be degrees OR radians ?
    #

    # Wrap modular values (e.g. longitudes) if required.
    modulus = sx.units.modulus
    if modulus is not None:
        # Match the source cube x coordinate range to the target grid
        # cube x coordinate range.
        min_sx, min_tx = np.min(sx.points), np.min(tx.points)
        if min_sx < 0 and min_tx >= 0:
            indices = np.where(sx_points < 0)
            # Ensure += doesn't raise a TypeError
            if not np.can_cast(modulus, sx_points.dtype):
                sx_points = sx_points.astype(type(modulus), casting="safe")
            sx_points[indices] += modulus
        elif min_sx >= 0 and min_tx < 0:
            indices = np.where(sx_points > (modulus / 2))
            # Ensure -= doesn't raise a TypeError
            if not np.can_cast(modulus, sx_points.dtype):
                sx_points = sx_points.astype(type(modulus), casting="safe")
            sx_points[indices] -= modulus

    # Create target grid cube x and y cell boundaries.
    tx_depth, ty_depth = tx.points.size, ty.points.size
    (tx_dim, ) = grid_cube.coord_dims(tx)
    (ty_dim, ) = grid_cube.coord_dims(ty)

    tx_cells = np.concatenate((tx.bounds[:, 0], tx.bounds[-1, 1].reshape(1)))
    ty_cells = np.concatenate((ty.bounds[:, 0], ty.bounds[-1, 1].reshape(1)))

    # Determine the target grid cube x and y cells that bound
    # the source cube x and y points.

    def _regrid_indices(cells, depth, points):
        # Calculate the minimum difference in cell extent.
        extent = np.min(np.diff(cells))
        if extent == 0:
            # Detected an dimension coordinate with an invalid
            # zero length cell extent.
            msg = ("The target grid cube {} ({!r}) coordinate contains "
                   "a zero length cell extent.")
            axis, name = "x", tx.name()
            if points is sy_points:
                axis, name = "y", ty.name()
            raise ValueError(msg.format(axis, name))
        elif extent > 0:
            # The cells of the dimension coordinate are in ascending order.
            indices = np.searchsorted(cells, points, side="right") - 1
        else:
            # The cells of the dimension coordinate are in descending order.
            # np.searchsorted() requires ascending order, so we require to
            # account for this restriction.
            cells = cells[::-1]
            right = np.searchsorted(cells, points, side="right")
            left = np.searchsorted(cells, points, side="left")
            indices = depth - right
            # Only those points that exactly match the left-hand cell bound
            # will differ between 'left' and 'right'. Thus their appropriate
            # target cell location requires to be recalculated to give the
            # correct descending [upper, lower) interval cell, source to target
            # regrid behaviour.
            delta = np.where(left != right)[0]
            if delta.size:
                indices[delta] = depth - left[delta]
        return indices

    x_indices = _regrid_indices(tx_cells, tx_depth, sx_points)
    y_indices = _regrid_indices(ty_cells, ty_depth, sy_points)

    # Now construct a sparse M x N matix, where M is the flattened target
    # space, and N is the flattened source space. The sparse matrix will then
    # be populated with those source cube points that contribute to a specific
    # target cube cell.

    # Determine the valid indices and their offsets in M x N space.
    # Calculate the valid M offsets.
    cols = np.where((y_indices >= 0)
                    & (y_indices < ty_depth)
                    & (x_indices >= 0)
                    & (x_indices < tx_depth))[0]

    # Reduce the indices to only those that are valid.
    x_indices = x_indices[cols]
    y_indices = y_indices[cols]

    # Calculate the valid N offsets.
    if ty_dim < tx_dim:
        rows = y_indices * tx.points.size + x_indices
    else:
        rows = x_indices * ty.points.size + y_indices

    # Calculate the associated valid weights.
    weights_flat = weights.flatten()
    data = weights_flat[cols]

    # Build our sparse M x N matrix of weights.
    sparse_matrix = csc_matrix((data, (rows, cols)),
                               shape=(grid_cube.data.size, src_cube.data.size))

    # Performing a sparse sum to collapse the matrix to (M, 1).
    sum_weights = sparse_matrix.sum(axis=1).getA()

    # Determine the rows (flattened target indices) that have a
    # contribution from one or more source points.
    rows = np.nonzero(sum_weights)

    # NOTE: when source points are masked, this 'sum_weights' is possibly
    # incorrect and needs re-calculating.  Likewise 'rows' may cover target
    # cells which happen to get no data.  This is dealt with by adjusting as
    # required in the '__perform' function, below.

    regrid_info = (sparse_matrix, sum_weights, rows, grid_cube)
    return regrid_info
Пример #28
0
    def __call__(self, src):
        """
        Regrid this :class:`~iris.cube.Cube` on to the target grid of
        this :class:`RectilinearRegridder`.

        The given cube must be defined with the same grid as the source
        grid used to create this :class:`RectilinearRegridder`.

        Args:

        * src:
            A :class:`~iris.cube.Cube` 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
            either nearest-neighbour or linear interpolation.

        """
        # Validity checks.
        if not isinstance(src, iris.cube.Cube):
            raise TypeError("'src' must be a Cube")
        if get_xy_dim_coords(src) != self._src_grid:
            raise ValueError('The given cube is not defined on the same '
                             'source grid as this regridder.')

        src_x_coord, src_y_coord = get_xy_dim_coords(src)
        grid_x_coord, grid_y_coord = self._tgt_grid
        src_cs = src_x_coord.coord_system
        grid_cs = grid_x_coord.coord_system

        if src_cs is None and grid_cs is None:
            if not (src_x_coord.is_compatible(grid_x_coord) and
                    src_y_coord.is_compatible(grid_y_coord)):
                raise ValueError("The rectilinear grid coordinates of the "
                                 "given cube and target grid have no "
                                 "coordinate system but they do not have "
                                 "matching coordinate metadata.")
        elif src_cs is None or grid_cs is None:
            raise ValueError("The rectilinear grid coordinates of the given "
                             "cube and target grid must either both have "
                             "coordinate systems or both have no coordinate "
                             "system but with matching coordinate metadata.")

        # Check the source grid units.
        for coord in (src_x_coord, src_y_coord):
            self._check_units(coord)

        # Convert the grid to a 2D sample grid in the src CRS.
        sample_grid = self._sample_grid(src_cs, grid_x_coord, grid_y_coord)
        sample_grid_x, sample_grid_y = sample_grid

        # Compute the interpolated data values.
        x_dim = src.coord_dims(src_x_coord)[0]
        y_dim = src.coord_dims(src_y_coord)[0]
        data = self._regrid(src.data, x_dim, y_dim,
                            src_x_coord, src_y_coord,
                            sample_grid_x, sample_grid_y,
                            self._method, self._extrapolation_mode)

        # Wrap up the data as a Cube.
        regrid_callback = functools.partial(self._regrid,
                                            method=self._method,
                                            extrapolation_mode='nan')
        result = self._create_cube(data, src, x_dim, y_dim,
                                   src_x_coord, src_y_coord,
                                   grid_x_coord, grid_y_coord,
                                   sample_grid_x, sample_grid_y,
                                   regrid_callback)
        return result
Пример #29
0
def spatial_chunked_regrid(
        src_cube,
        tgt_cube,
        scheme,
        min_src_chunk_size=2,
        max_src_chunk_size=dask.config.get("array.chunk-size"),
        min_tgt_chunk_size=None,
        max_tgt_chunk_size=dask.config.get("array.chunk-size"),
        tol=1e-16,
):
    """Spatially chunked regridding using dask.

    Only the y-coordinate is chunked. This is done because the x-coordinate may be
    circular (global), which may require additional logic to be implemented.

    Args:
        src_cube (iris.cube.Cube): Cube to be regridded onto the coordinate system
            defined by the target cube.
        tgt_cube (iris.cube.Cube): Target cube. This is solely required to specify the
            target coordinate system and may contain dummy data.
        scheme: The type of regridding to use to regrid the source cube onto the
            target grid, e.g. `iris.analysis.Linear`, `iris.analysis.Nearest`, and
            `iris.analysis.AreaWeighted`.
        min_src_chunk_size (None, int): Minimum source cube chunk size along the
            y-dimension, specified in terms of the number of elements per chunk along
            this axis. Note that some regridders, e.g. `iris.analysis.Linear()`
            require at least a chunk size of 2 here.
        max_src_chunk_size (None, int, str): The maximum size of chunks along the
            source cube's y-dimension. Can be given in bytes, e.g. '10MB' or '100KB'.
            If None is given, the chunks will be as large as possible.
        min_tgt_chunk_size (None, int): Analogous to `min_src_chunk_size` for the
            target cube.
        max_tgt_chunk_size (None, int, str): Analogous to `max_src_chunk_size` for the
            target cube.

    Raises:
        TypeError: If `src_cube` does not have lazy data.
        ValueError: If the source cube is not 2D.
        ValueError: If the source or target cube do not define x and y coordinates.
        ValueError: If source and target cubes do not define their x and y coordinates
            along the same dimensions.
        ValueError: If any of the x, y coordinates are not monotonic.
        ValueError: If the given maximum chunk sizes are smaller than required for the
            regridding of a single data chunk.

    """
    if not src_cube.has_lazy_data():
        raise TypeError("Source cube needs to have lazy data.")
    if src_cube.core_data().ndim != 2:
        raise ValueError("Source cube data needs to be 2D.")

    coord_err = "{name} cube needs to define x and y coordinates."
    try:
        src_x_coord, src_y_coord = get_xy_dim_coords(src_cube)
    except Exception as exc:
        raise ValueError(coord_err.format("Source")) from exc
    try:
        tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_cube)
    except Exception as exc:
        raise ValueError(coord_err.format("Target")) from exc

    y_dim = src_y_dim = src_cube.coord_dims(src_y_coord)[0]
    x_dim = src_x_dim = src_cube.coord_dims(src_x_coord)[0]

    tgt_y_dim = tgt_cube.coord_dims(tgt_y_coord)[0]
    tgt_x_dim = tgt_cube.coord_dims(tgt_x_coord)[0]

    if (src_y_dim, src_x_dim) != (tgt_y_dim, tgt_x_dim):
        raise ValueError("Coordinates are not aligned.")

    monotonic_err_msg = "{:}-coordinate needs to be monotonic."

    src_x_coord_monotonic, src_x_coord_direction = iris.util.monotonic(
        src_x_coord.points, return_direction=True)
    if not src_x_coord_monotonic:
        raise ValueError(monotonic_err_msg.format("Source x"))
    if src_x_coord_direction < 0:
        # Coordinate is monotonically decreasing, so we need to invert it.
        flip_slice = [slice(None)] * src_cube.ndim
        flip_slice[src_x_dim] = slice(None, None, -1)
        src_cube = src_cube[tuple(flip_slice)]
        src_x_coord, src_y_coord = get_xy_dim_coords(src_cube)
        src_x_coord.bounds = src_x_coord.bounds[:, ::-1]

    src_y_coord_monotonic, src_y_coord_direction = iris.util.monotonic(
        src_y_coord.points, return_direction=True)
    if not src_y_coord_monotonic:
        raise ValueError(monotonic_err_msg.format("Source y"))
    if src_y_coord_direction < 0:
        # Coordinate is monotonically decreasing, so we need to invert it.
        flip_slice = [slice(None)] * src_cube.ndim
        flip_slice[src_y_dim] = slice(None, None, -1)
        src_cube = src_cube[tuple(flip_slice)]
        src_x_coord, src_y_coord = get_xy_dim_coords(src_cube)
        src_y_coord.bounds = src_y_coord.bounds[:, ::-1]

    tgt_x_coord_monotonic, tgt_x_coord_direction = iris.util.monotonic(
        tgt_x_coord.points, return_direction=True)
    if not tgt_x_coord_monotonic:
        raise ValueError(monotonic_err_msg.format("Target x"))
    if tgt_x_coord_direction < 0:
        # Coordinate is monotonically decreasing, so we need to invert it.
        flip_slice = [slice(None)] * tgt_cube.ndim
        flip_slice[tgt_x_dim] = slice(None, None, -1)
        tgt_cube = tgt_cube[tuple(flip_slice)]
        tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_cube)
        tgt_x_coord.bounds = tgt_x_coord.bounds[:, ::-1]

    tgt_y_coord_monotonic, tgt_y_coord_direction = iris.util.monotonic(
        tgt_y_coord.points, return_direction=True)
    if not tgt_y_coord_monotonic:
        raise ValueError(monotonic_err_msg.format("Target y"))
    if tgt_y_coord_direction < 0:
        # Coordinate is monotonically decreasing, so we need to invert it.
        flip_slice = [slice(None)] * tgt_cube.ndim
        flip_slice[tgt_y_dim] = slice(None, None, -1)
        tgt_cube = tgt_cube[tuple(flip_slice)]
        tgt_x_coord, tgt_y_coord = get_xy_dim_coords(tgt_cube)
        tgt_y_coord.bounds = tgt_y_coord.bounds[:, ::-1]

    max_src_chunk_size = convert_chunk_size(
        max_src_chunk_size,
        # The number of elements along the non-chunked dimension.
        factor=src_cube.shape[x_dim],
        dtype=src_cube.dtype,
        masked=isinstance(src_cube.core_data()._meta, np.ma.MaskedArray),
    )
    max_tgt_chunk_size = convert_chunk_size(
        max_tgt_chunk_size,
        # The number of elements along the non-chunked dimension.
        factor=tgt_cube.shape[x_dim],
        # NOTE: Is this true?
        dtype=src_cube.dtype,
        # Set masked to True here since we will add a mask later in all cases.
        masked=True,
    )
    max_chunk_msg = (
        "Maximum {:} chunk size was smaller than the minimum required for a single "
        "chunk.")
    if max_src_chunk_size == 0:
        raise ValueError(max_chunk_msg.format("source"))
    if max_tgt_chunk_size == 0:
        raise ValueError(max_chunk_msg.format("target"))

    # Calculate all possible chunks along the y dimension.
    overlap_indices, valid_src_y_slice, valid_tgt_y_slice = get_overlapping(
        src_y_coord.contiguous_bounds(),
        tgt_y_coord.contiguous_bounds(),
        tol=tol,
    )
    # Some regridding methods care about cells with points outside of overlapping
    # bounds, like `iris.analysis.Linear()`. Include an additional source cell on
    # either end if possible to account for this.
    # NOTE: These additions may be superfluous, but would require re-writing
    # `get_overlapping()` to determine.
    if valid_src_y_slice.start > 0:
        overlap_indices[valid_tgt_y_slice][0].insert(
            0, overlap_indices[valid_tgt_y_slice][0][0] - 1)
    if valid_src_y_slice.stop < src_y_coord.shape[0]:
        overlap_indices[valid_tgt_y_slice][-1].append(
            overlap_indices[valid_tgt_y_slice][-1][-1] + 1)
    valid_src_y_slice = slice(
        max(0, valid_src_y_slice.start - 1),
        min(src_y_coord.shape[0], valid_src_y_slice.stop + 1),
    )

    cell_numbers, overlap_y = get_cell_numbers(overlap_indices)
    cell_mapping = get_valid_cell_mapping(cell_numbers[valid_tgt_y_slice])

    tgt_y_slices = []
    src_y_chunks = []
    tgt_y_chunks = []

    for tgt_cells, src_cells in cell_mapping.items():
        tgt_y_slices.append(slice(tgt_cells[0], tgt_cells[-1] + 1))
        src_y_chunks.append(len(src_cells))
        tgt_y_chunks.append(len(tgt_cells))

    # XXX: This override is sometimes needed due to floating point errors, e.g. for
    # test_regrid case
    # 100_200-50_120--90_90--90_90--180_180--180_180-AreaWeighted_1-Block-20KB
    # where tgt_cube.coord('latitude')[24].bounds[0][1] is 3.55e-15 instead of 0,
    # causing src_cube.coord('latitude')[50] (with bounds [0, 1.8] to be required to
    # match the masking behaviour in Iris regrid even though this is not expected to
    # influence the final result to the small overlap. Another solution is to decrease
    # the `tol` parameter of `get_overlapping()`, in this case below 3.55e-16
    # (e.g. 1e-16).
    # overlap_y = True

    src_y_chunks, tgt_y_slices = calculate_blocks(
        src_y_chunks,
        tgt_y_chunks,
        tgt_y_slices,
        min_src_chunk_size,
        max_src_chunk_size,
        min_tgt_chunk_size,
        max_tgt_chunk_size,
    )

    valid_src_slice = [slice(None)] * src_cube.ndim
    valid_src_slice[src_y_dim] = valid_src_y_slice
    valid_src_slice = tuple(valid_src_slice)

    # Re-chunk the data and coordinate along the y-dimension.
    block_src_data = src_cube.core_data()[valid_src_slice].rechunk((
        src_y_chunks,
        -1,
    ))
    # 2D arrays are created here to enable consistent map_overlap behaviour.
    block_src_y_pnts = (da.from_array(
        src_y_coord.points[valid_src_y_slice]).rechunk(
            (src_y_chunks, )).reshape(-1, 1))
    block_src_low_y_bnds = (da.from_array(
        src_y_coord.bounds[valid_src_y_slice, 0]).rechunk(
            (src_y_chunks, )).reshape(-1, 1))
    block_src_upp_y_bnds = (da.from_array(
        src_y_coord.bounds[valid_src_y_slice, 1]).rechunk(
            (src_y_chunks, )).reshape(-1, 1))

    chunks_spec = [None] * 2
    chunks_spec[x_dim] = tgt_x_coord.shape[0]
    chunks_spec[y_dim] = tuple(slice_len(s) for s in tgt_y_slices)
    chunks_spec = tuple(chunks_spec)

    output = da.map_overlap(
        regrid_chunk,
        block_src_data,
        block_src_y_pnts,
        block_src_low_y_bnds,
        block_src_upp_y_bnds,
        # Store metadata for the y-coordinate for which points and bounds will be
        # filled in during the course of blocked regridding.
        src_y_coord_metadata=src_y_coord.metadata,
        src_x_coord=src_x_coord,
        src_cube_metadata=src_cube.metadata,
        tgt_y_coord=tgt_y_coord[valid_tgt_y_slice],
        tgt_x_coord=tgt_x_coord,
        tgt_y_slices=tgt_y_slices,
        tgt_cube_metadata=tgt_cube.metadata,
        y_dim=y_dim,
        x_dim=x_dim,
        scheme=scheme,
        depth={
            # The y-coordinate may need to be overlapped.
            y_dim: 1 if overlap_y else 0,
            # The x-coordinate will not be overlapped as it is never chunked.
            x_dim: 0,
        },
        boundary="none",
        trim=False,
        dtype=np.float64,
        chunks=chunks_spec,
        meta=np.array([], dtype=np.float64),
    )
    if not isinstance(output._meta, np.ma.MaskedArray):
        # XXX: Ideally this should not be needed, but the mask appears to vanish in
        # some cases.
        output = da.ma.masked_array(output, mask=False)

    if slice_len(valid_tgt_y_slice) < tgt_y_coord.shape[0]:
        # Embed the output data calculated above into the final target cube by padding
        # with masked data as necessary.
        seq = []
        if valid_tgt_y_slice.start > 0:
            # Pad at the start.
            start_pad_shape = [tgt_x_coord.shape[0]] * 2
            start_pad_shape[y_dim] = valid_tgt_y_slice.start
            seq.append(
                da.ma.masked_array(
                    da.zeros(start_pad_shape),
                    mask=True,
                ))
        seq.append(output)
        if valid_tgt_y_slice.stop < tgt_y_coord.shape[0]:
            # Pad at the end.
            end_pad_shape = [tgt_x_coord.shape[0]] * 2
            end_pad_shape[
                y_dim] = tgt_y_coord.shape[0] - valid_tgt_y_slice.stop
            seq.append(da.ma.masked_array(
                da.zeros(end_pad_shape),
                mask=True,
            ))
        output = da.concatenate(seq, axis=y_dim)

    return tgt_cube.copy(data=output)