Esempio n. 1
0
    def _sample_grid(src_coord_system, grid_x_coord, grid_y_coord):
        """
        Convert the rectilinear grid coordinates to a curvilinear grid in
        the source coordinate system.

        The `grid_x_coord` and `grid_y_coord` must share a common coordinate
        system.

        Args:

        * src_coord_system:
            The :class:`iris.coord_system.CoordSystem` for the grid of the
            source Cube.
        * grid_x_coord:
            The :class:`iris.coords.DimCoord` for the X coordinate.
        * grid_y_coord:
            The :class:`iris.coords.DimCoord` for the Y coordinate.

        Returns:
            A tuple of the X and Y coordinate values as 2-dimensional
            arrays.

        """
        grid_x, grid_y = _meshgrid(grid_x_coord.points, grid_y_coord.points)
        # Skip the CRS transform if we can to avoid precision problems.
        if src_coord_system == grid_x_coord.coord_system:
            sample_grid_x = grid_x
            sample_grid_y = grid_y
        else:
            src_crs = src_coord_system.as_cartopy_crs()
            grid_crs = grid_x_coord.coord_system.as_cartopy_crs()
            sample_xyz = src_crs.transform_points(grid_crs, grid_x, grid_y)
            sample_grid_x = sample_xyz[..., 0]
            sample_grid_y = sample_xyz[..., 1]
        return sample_grid_x, sample_grid_y
Esempio n. 2
0
def get_xy_grids(cube):
    """
    Return 2D X and Y points for a given cube.

    Args:

        * cube - The cube for which to generate 2D X and Y points.

    Example::

        x, y = get_xy_grids(cube)

    """
    x_coord, y_coord = cube.coord(axis="X"), cube.coord(axis="Y")

    x = x_coord.points
    y = y_coord.points

    if x.ndim == y.ndim == 1:
        # Convert to 2D.
        x, y = _meshgrid(x, y)
    elif x.ndim == y.ndim == 2:
        # They are already in the correct shape.
        pass
    else:
        raise ValueError("Expected 1D or 2D XY coords")

    return (x, y)
Esempio n. 3
0
    def _sample_grid(src_coord_system, grid_x_coord, grid_y_coord):
        """
        Convert the rectilinear grid coordinates to a curvilinear grid in
        the source coordinate system.

        The `grid_x_coord` and `grid_y_coord` must share a common coordinate
        system.

        Args:

        * src_coord_system:
            The :class:`iris.coord_system.CoordSystem` for the grid of the
            source Cube.
        * grid_x_coord:
            The :class:`iris.coords.DimCoord` for the X coordinate.
        * grid_y_coord:
            The :class:`iris.coords.DimCoord` for the Y coordinate.

        Returns:
            A tuple of the X and Y coordinate values as 2-dimensional
            arrays.

        """
        grid_x, grid_y = _meshgrid(grid_x_coord.points, grid_y_coord.points)
        # Skip the CRS transform if we can to avoid precision problems.
        if src_coord_system == grid_x_coord.coord_system:
            sample_grid_x = grid_x
            sample_grid_y = grid_y
        else:
            src_crs = src_coord_system.as_cartopy_crs()
            grid_crs = grid_x_coord.coord_system.as_cartopy_crs()
            sample_xyz = src_crs.transform_points(grid_crs, grid_x, grid_y)
            sample_grid_x = sample_xyz[..., 0]
            sample_grid_y = sample_xyz[..., 1]
        return sample_grid_x, sample_grid_y
Esempio n. 4
0
def get_xy_grids(cube):
    """
    Return 2D X and Y points for a given cube.

    Args:

        * cube - The cube for which to generate 2D X and Y points.

    Example::

        x, y = get_xy_grids(cube)

    """
    x_coord, y_coord = cube.coord(axis="X"), cube.coord(axis="Y")

    x = x_coord.points
    y = y_coord.points

    if x.ndim == y.ndim == 1:
        # Convert to 2D.
        x, y = _meshgrid(x, y)
    elif x.ndim == y.ndim == 2:
        # They are already in the correct shape.
        pass
    else:
        raise ValueError("Expected 1D or 2D XY coords")

    return (x, y)
Esempio n. 5
0
    def _regrid(
        src_data,
        xy_dim,
        src_x_coord,
        src_y_coord,
        tgt_x_coord,
        tgt_y_coord,
        projection,
        method,
    ):
        """
        Regrids input data from the source to the target. Calculation is.

        """
        # Transform coordinates into the projection the interpolation will be
        # performed in.
        src_projection = src_x_coord.coord_system.as_cartopy_projection()
        projected_src_points = projection.transform_points(
            src_projection, src_x_coord.points, src_y_coord.points
        )

        tgt_projection = tgt_x_coord.coord_system.as_cartopy_projection()
        tgt_x, tgt_y = _meshgrid(tgt_x_coord.points, tgt_y_coord.points)
        projected_tgt_grid = projection.transform_points(
            tgt_projection, tgt_x, tgt_y
        )

        # Prepare the result data array.
        # XXX TODO: Deal with masked src_data
        (tgt_y_shape,) = tgt_y_coord.shape
        (tgt_x_shape,) = tgt_x_coord.shape
        tgt_shape = (
            src_data.shape[:xy_dim]
            + (tgt_y_shape,)
            + (tgt_x_shape,)
            + src_data.shape[xy_dim + 1 :]
        )
        data = np.empty(tgt_shape, dtype=src_data.dtype)

        iter_shape = list(src_data.shape)
        iter_shape[xy_dim] = 1

        for index in np.ndindex(tuple(iter_shape)):
            src_index = list(index)
            src_index[xy_dim] = slice(None)
            src_subset = src_data[tuple(src_index)]
            tgt_index = (
                index[:xy_dim]
                + (slice(None), slice(None))
                + index[xy_dim + 1 :]
            )
            data[tgt_index] = scipy.interpolate.griddata(
                projected_src_points[..., :2],
                src_subset,
                (projected_tgt_grid[..., 0], projected_tgt_grid[..., 1]),
                method=method,
            )
        data = np.ma.array(data, mask=np.isnan(data))
        return data
Esempio n. 6
0
def get_xy_contiguous_bounded_grids(cube):
    """
    Return 2d arrays for x and y bounds.

    Returns array of shape (n+1, m+1).

    Example::

        xs, ys = get_xy_contiguous_bounded_grids(cube)

    """
    x_coord, y_coord = cube.coord(axis="X"), cube.coord(axis="Y")

    x = x_coord.contiguous_bounds()
    y = y_coord.contiguous_bounds()
    x, y = _meshgrid(x, y)

    return (x, y)
Esempio n. 7
0
def get_xy_contiguous_bounded_grids(cube):
    """
    Return 2d arrays for x and y bounds.

    Returns array of shape (n+1, m+1).

    Example::

        xs, ys = get_xy_contiguous_bounded_grids(cube)

    """
    x_coord, y_coord = cube.coord(axis="X"), cube.coord(axis="Y")

    x = x_coord.contiguous_bounds()
    y = y_coord.contiguous_bounds()
    x, y = _meshgrid(x, y)

    return (x, y)
Esempio n. 8
0
    def __init__(self, src_cube, target_grid_cube):
        """
        A nearest-neighbour regridder to perform regridding from the source
        grid to the target grid.

        This can then be applied to any source data with the same structure as
        the original 'src_cube'.

        Args:

        * src_cube:
            The :class:`~iris.cube.Cube` defining the source grid.
            The X and Y coordinates can have any shape, but must be mapped over
            the same cube dimensions.

        * target_grid_cube:
            A :class:`~iris.cube.Cube`, whose X and Y coordinates specify a
            desired target grid.
            The X and Y coordinates must be one-dimensional dimension
            coordinates, mapped to different dimensions.
            All other cube components are ignored.

        Returns:
            regridder : (object)

            A callable object with the interface:
                `result_cube = regridder(data)`

            where `data` is a cube with the same grid as the original
            `src_cube`, that is to be regridded to the `target_grid_cube`.

        .. Note::

            For latitude-longitude coordinates, the nearest-neighbour distances
            are computed on the sphere, otherwise flat Euclidean distances are
            used.

            The source and target X and Y coordinates must all have the same
            coordinate system, which may also be None.
            If any X and Y coordinates are latitudes or longitudes, they *all*
            must be.  Otherwise, the corresponding X and Y coordinates must
            have the same units in the source and grid cubes.

        """
        # Make a copy of the source cube, so we can convert coordinate units.
        src_cube = src_cube.copy()

        # Snapshot the target grid and check it is a "normal" grid.
        tgt_x_coord, tgt_y_coord = snapshot_grid(target_grid_cube)

        # Check that the source has unique X and Y coords over common dims.
        if not src_cube.coords(axis="x") or not src_cube.coords(axis="y"):
            msg = "Source cube must have X- and Y-axis coordinates."
            raise ValueError(msg)
        src_x_coord = src_cube.coord(axis="x")
        src_y_coord = src_cube.coord(axis="y")
        if src_cube.coord_dims(src_x_coord) != src_cube.coord_dims(
            src_y_coord
        ):
            msg = (
                "Source cube X and Y coordinates must have the same "
                "cube dimensions."
            )
            raise ValueError(msg)

        # Record *copies* of the original grid coords, in the desired
        # dimension order.
        # This lets us convert the actual ones in use to units of "degrees".
        self.src_grid_coords = [src_y_coord.copy(), src_x_coord.copy()]
        self.tgt_grid_coords = [tgt_y_coord.copy(), tgt_x_coord.copy()]

        # Check that all XY coords have suitable coordinate systems and units.
        coords_all = [src_x_coord, src_y_coord, tgt_x_coord, tgt_y_coord]
        cs = coords_all[0].coord_system
        if not all(coord.coord_system == cs for coord in coords_all):
            msg = (
                "Source and target cube X and Y coordinates must all have "
                "the same coordinate system."
            )
            raise ValueError(msg)

        # Check *all* X and Y coords are lats+lons, if any are.
        latlons = [
            "latitude" in coord.name() or "longitude" in coord.name()
            for coord in coords_all
        ]
        if any(latlons) and not all(latlons):
            msg = (
                "If any X and Y coordinates are latitudes/longitudes, "
                "then they all must be."
            )
            raise ValueError(msg)

        self.grid_is_latlon = any(latlons)
        if self.grid_is_latlon:
            # Convert all XY coordinates to units of "degrees".
            # N.B. already copied the target grid, so the result matches that.
            for coord in coords_all:
                try:
                    coord.convert_units("degrees")
                except ValueError:
                    msg = (
                        "Coordinate {!r} has units of {!r}, which does not "
                        'convert to "degrees".'
                    )
                    raise ValueError(
                        msg.format(coord.name(), str(coord.units))
                    )
        else:
            # Check that source and target have the same X and Y units.
            if (
                src_x_coord.units != tgt_x_coord.units
                or src_y_coord.units != tgt_y_coord.units
            ):
                msg = (
                    "Source and target cube X and Y coordinates must "
                    "have the same units."
                )
                raise ValueError(msg)

        # Record the resulting grid shape.
        self.tgt_grid_shape = tgt_y_coord.shape + tgt_x_coord.shape

        # Calculate sample points as 2d arrays, like broadcast (NY,1)*(1,NX).
        x_2d, y_2d = _meshgrid(tgt_x_coord.points, tgt_y_coord.points)
        # Cast as a "trajectory", to suit the method used.
        self.trajectory = (
            (tgt_x_coord.name(), x_2d.flatten()),
            (tgt_y_coord.name(), y_2d.flatten()),
        )
Esempio n. 9
0
def rotate_winds(u_cube, v_cube, target_cs):
    r"""
    Transform wind vectors to a different coordinate system.

    The input cubes contain U and V components parallel to the local X and Y
    directions of the input grid at each point.

    The output cubes contain the same winds, at the same locations, but
    relative to the grid directions of a different coordinate system.
    Thus in vector terms, the magnitudes will always be the same, but the
    angles can be different.

    The outputs retain the original horizontal dimension coordinates, but
    also have two 2-dimensional auxiliary coordinates containing the X and
    Y locations in the target coordinate system.

    Args:

    * u_cube
        An instance of :class:`iris.cube.Cube` that contains the x-component
        of the vector.
    * v_cube
        An instance of :class:`iris.cube.Cube` that contains the y-component
        of the vector.
    * target_cs
        An instance of :class:`iris.coord_systems.CoordSystem` that specifies
        the new grid directions.

    Returns:
        A (u', v') tuple of :class:`iris.cube.Cube` instances that are the u
        and v components in the requested target coordinate system.
        The units are the same as the inputs.

    .. note::

        The U and V values relate to distance, with units such as 'm s-1'.
        These are not the same as coordinate vectors, which transform in a
        different manner.

    .. note::

        The names of the output cubes are those of the inputs, prefixed with
        'transformed\_' (e.g. 'transformed_x_wind').

    .. warning::

        Conversion between rotated-pole and non-rotated systems can be
        expressed analytically.  However, this function always uses a numerical
        approach. In locations where this numerical approach does not preserve
        magnitude to an accuracy of 0.1%, the corresponding elements of the
        returned cubes will be masked.

    """
    # Check u_cube and v_cube have the same shape. We iterate through
    # the u and v cube slices which relies on the shapes matching.
    if u_cube.shape != v_cube.shape:
        msg = ("Expected u and v cubes to have the same shape. "
               "u cube has shape {}, v cube has shape {}.")
        raise ValueError(msg.format(u_cube.shape, v_cube.shape))

    # Check the u_cube and v_cube have the same x and y coords.
    msg = ("Coordinates differ between u and v cubes. Coordinate {!r} from "
           "u cube does not equal coordinate {!r} from v cube.")
    if u_cube.coord(axis="x") != v_cube.coord(axis="x"):
        raise ValueError(
            msg.format(
                u_cube.coord(axis="x").name(),
                v_cube.coord(axis="x").name()))
    if u_cube.coord(axis="y") != v_cube.coord(axis="y"):
        raise ValueError(
            msg.format(
                u_cube.coord(axis="y").name(),
                v_cube.coord(axis="y").name()))

    # Check x and y coords have the same coordinate system.
    x_coord = u_cube.coord(axis="x")
    y_coord = u_cube.coord(axis="y")
    if x_coord.coord_system != y_coord.coord_system:
        msg = ("Coordinate systems of x and y coordinates differ. "
               "Coordinate {!r} has a coord system of {!r}, but coordinate "
               "{!r} has a coord system of {!r}.")
        raise ValueError(
            msg.format(
                x_coord.name(),
                x_coord.coord_system,
                y_coord.name(),
                y_coord.coord_system,
            ))

    # Convert from iris coord systems to cartopy CRSs to access
    # transform functionality. Use projection as cartopy
    # transform_vectors relies on x_limits and y_limits.
    if x_coord.coord_system is not None:
        src_crs = x_coord.coord_system.as_cartopy_projection()
    else:
        # Default to Geodetic (but actually use PlateCarree as a
        # projection is needed).
        src_crs = ccrs.PlateCarree()
    target_crs = target_cs.as_cartopy_projection()

    # Check the number of dimensions of the x and y coords is the same.
    # Subsequent logic assumes either both 1d or both 2d.
    x = x_coord.points
    y = y_coord.points
    if x.ndim != y.ndim or x.ndim > 2 or y.ndim > 2:
        msg = ("x and y coordinates must have the same number of dimensions "
               "and be either 1D or 2D. The number of dimensions are {} and "
               "{}, respectively.".format(x.ndim, y.ndim))
        raise ValueError(msg)

    # Check the dimension mappings match between u_cube and v_cube.
    if u_cube.coord_dims(x_coord) != v_cube.coord_dims(x_coord):
        raise ValueError("Dimension mapping of x coordinate differs "
                         "between u and v cubes.")
    if u_cube.coord_dims(y_coord) != v_cube.coord_dims(y_coord):
        raise ValueError("Dimension mapping of y coordinate differs "
                         "between u and v cubes.")
    x_dims = u_cube.coord_dims(x_coord)
    y_dims = u_cube.coord_dims(y_coord)

    # Convert points to 2D, if not already, and determine dims.
    if x.ndim == y.ndim == 1:
        x, y = _meshgrid(x, y)
        dims = (y_dims[0], x_dims[0])
    else:
        dims = x_dims

    # Transpose x, y 2d arrays to match the order in cube's data
    # array so that x, y and the sliced data all line up.
    if dims[0] > dims[1]:
        x = x.transpose()
        y = y.transpose()

    # Create resulting cubes.
    ut_cube = u_cube.copy()
    vt_cube = v_cube.copy()
    ut_cube.rename("transformed_{}".format(u_cube.name()))
    vt_cube.rename("transformed_{}".format(v_cube.name()))

    # Get distance scalings for source crs.
    ds_dx1, ds_dy1 = _crs_distance_differentials(src_crs, x, y)

    # Get distance scalings for target crs.
    x2, y2 = _transform_xy(src_crs, x, y, target_crs)
    ds_dx2, ds_dy2 = _crs_distance_differentials(target_crs, x2, y2)

    ds = DistanceDifferential(ds_dx1, ds_dy1, ds_dx2, ds_dy2)

    # Calculate coordinate partial differentials from source crs to target crs.
    dx2_dx1, dy2_dx1, dx2_dy1, dy2_dy1 = _inter_crs_differentials(
        src_crs, x, y, target_crs)

    dx2 = PartialDifferential(dx2_dx1, dx2_dy1)
    dy2 = PartialDifferential(dy2_dx1, dy2_dy1)

    # Calculate mask based on preservation of magnitude.
    mask = _transform_distance_vectors_tolerance_mask(src_crs, x, y,
                                                      target_crs, ds, dx2, dy2)
    apply_mask = mask.any()
    if apply_mask:
        # Make masked arrays to accept masking.
        ut_cube.data = ma.asanyarray(ut_cube.data)
        vt_cube.data = ma.asanyarray(vt_cube.data)

    # Project vectors with u, v components one horiz slice at a time and
    # insert into the resulting cubes.
    shape = list(u_cube.shape)
    for dim in dims:
        shape[dim] = 1
    ndindex = np.ndindex(*shape)
    for index in ndindex:
        index = list(index)
        for dim in dims:
            index[dim] = slice(None, None)
        index = tuple(index)
        u = u_cube.data[index]
        v = v_cube.data[index]
        ut, vt = _transform_distance_vectors(u, v, ds, dx2, dy2)
        if apply_mask:
            ut = ma.asanyarray(ut)
            ut[mask] = ma.masked
            vt = ma.asanyarray(vt)
            vt[mask] = ma.masked
        ut_cube.data[index] = ut
        vt_cube.data[index] = vt

    # Calculate new coords of locations in target coordinate system.
    xyz_tran = target_crs.transform_points(src_crs, x, y)
    xt = xyz_tran[..., 0].reshape(x.shape)
    yt = xyz_tran[..., 1].reshape(y.shape)

    # Transpose xt, yt 2d arrays to match the dim order
    # of the original x an y arrays - i.e. undo the earlier
    # transpose (if applied).
    if dims[0] > dims[1]:
        xt = xt.transpose()
        yt = yt.transpose()

    xt_coord = iris.coords.AuxCoord(xt,
                                    standard_name="projection_x_coordinate",
                                    coord_system=target_cs)
    yt_coord = iris.coords.AuxCoord(yt,
                                    standard_name="projection_y_coordinate",
                                    coord_system=target_cs)
    # Set units based on coord_system.
    if isinstance(
            target_cs,
        (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS),
    ):
        xt_coord.units = yt_coord.units = "degrees"
    else:
        xt_coord.units = yt_coord.units = "m"

    ut_cube.add_aux_coord(xt_coord, dims)
    ut_cube.add_aux_coord(yt_coord, dims)
    vt_cube.add_aux_coord(xt_coord.copy(), dims)
    vt_cube.add_aux_coord(yt_coord.copy(), dims)

    return ut_cube, vt_cube
Esempio n. 10
0
def project(cube, target_proj, nx=None, ny=None):
    """
    Nearest neighbour regrid to a specified target projection.

    Return a new cube that is the result of projecting a cube with 1 or 2
    dimensional latitude-longitude coordinates from its coordinate system into
    a specified projection e.g. Robinson or Polar Stereographic.
    This function is intended to be used in cases where the cube's coordinates
    prevent one from directly visualising the data, e.g. when the longitude
    and latitude are two dimensional and do not make up a regular grid.

    Args:
        * cube
            An instance of :class:`iris.cube.Cube`.
        * target_proj
            An instance of the Cartopy Projection class, or an instance of
            :class:`iris.coord_systems.CoordSystem` from which a projection
            will be obtained.
    Kwargs:
        * nx
            Desired number of sample points in the x direction for a domain
            covering the globe.
        * ny
            Desired number of sample points in the y direction for a domain
            covering the globe.

    Returns:
        An instance of :class:`iris.cube.Cube` and a list describing the
        extent of the projection.

    .. note::

        This function assumes global data and will if necessary extrapolate
        beyond the geographical extent of the source cube using a nearest
        neighbour approach. nx and ny then include those points which are
        outside of the target projection.

    .. note::

        Masked arrays are handled by passing their masked status to the
        resulting nearest neighbour values.  If masked, the value in the
        resulting cube is set to 0.

    .. warning::

        This function uses a nearest neighbour approach rather than any form
        of linear/non-linear interpolation to determine the data value of each
        cell in the resulting cube. Consequently it may have an adverse effect
        on the statistics of the data e.g. the mean and standard deviation
        will not be preserved.

    """
    try:
        lon_coord, lat_coord = _get_lon_lat_coords(cube)
    except IndexError:
        raise ValueError("Cannot get latitude/longitude "
                         "coordinates from cube {!r}.".format(cube.name()))

    if lat_coord.coord_system != lon_coord.coord_system:
        raise ValueError("latitude and longitude coords appear to have "
                         "different coordinates systems.")

    if lon_coord.units != "degrees":
        lon_coord = lon_coord.copy()
        lon_coord.convert_units("degrees")
    if lat_coord.units != "degrees":
        lat_coord = lat_coord.copy()
        lat_coord.convert_units("degrees")

    # Determine source coordinate system
    if lat_coord.coord_system is None:
        # Assume WGS84 latlon if unspecified
        warnings.warn("Coordinate system of latitude and longitude "
                      "coordinates is not specified. Assuming WGS84 Geodetic.")
        orig_cs = iris.coord_systems.GeogCS(semi_major_axis=6378137.0,
                                            inverse_flattening=298.257223563)
    else:
        orig_cs = lat_coord.coord_system

    # Convert to cartopy crs
    source_cs = orig_cs.as_cartopy_crs()

    # Obtain coordinate arrays (ignoring bounds) and convert to 2d
    # if not already.
    source_x = lon_coord.points
    source_y = lat_coord.points
    if source_x.ndim != 2 or source_y.ndim != 2:
        source_x, source_y = _meshgrid(source_x, source_y)

    # Calculate target grid
    target_cs = None
    if isinstance(target_proj, iris.coord_systems.CoordSystem):
        target_cs = target_proj
        target_proj = target_proj.as_cartopy_projection()

    # Resolution of new grid
    if nx is None:
        nx = source_x.shape[1]
    if ny is None:
        ny = source_x.shape[0]

    target_x, target_y, extent = cartopy.img_transform.mesh_projection(
        target_proj, nx, ny)

    # Determine dimension mappings - expect either 1d or 2d
    if lat_coord.ndim != lon_coord.ndim:
        raise ValueError("The latitude and longitude coordinates have "
                         "different dimensionality.")

    latlon_ndim = lat_coord.ndim
    lon_dims = cube.coord_dims(lon_coord)
    lat_dims = cube.coord_dims(lat_coord)

    if latlon_ndim == 1:
        xdim = lon_dims[0]
        ydim = lat_dims[0]
    elif latlon_ndim == 2:
        if lon_dims != lat_dims:
            raise ValueError("The 2d latitude and longitude coordinates "
                             "correspond to different dimensions.")
        # If coords are 2d assume that grid is ordered such that x corresponds
        # to the last dimension (shortest stride).
        xdim = lon_dims[1]
        ydim = lon_dims[0]
    else:
        raise ValueError("Expected the latitude and longitude coordinates "
                         "to have 1 or 2 dimensions, got {} and "
                         "{}.".format(lat_coord.ndim, lon_coord.ndim))

    # Create array to store regridded data
    new_shape = list(cube.shape)
    new_shape[xdim] = nx
    new_shape[ydim] = ny
    new_data = ma.zeros(new_shape, cube.data.dtype)

    # Create iterators to step through cube data in lat long slices
    new_shape[xdim] = 1
    new_shape[ydim] = 1
    index_it = np.ndindex(*new_shape)
    if lat_coord.ndim == 1 and lon_coord.ndim == 1:
        slice_it = cube.slices([lat_coord, lon_coord])
    elif lat_coord.ndim == 2 and lon_coord.ndim == 2:
        slice_it = cube.slices(lat_coord)
    else:
        raise ValueError("Expected the latitude and longitude coordinates "
                         "to have 1 or 2 dimensions, got {} and "
                         "{}.".format(lat_coord.ndim, lon_coord.ndim))

    #    # Mask out points outside of extent in source_cs - disabled until
    #    # a way to specify global/limited extent is agreed upon and code
    #    # is generalised to handle -180 to +180, 0 to 360 and >360 longitudes.
    #    source_desired_xy = source_cs.transform_points(target_proj,
    #                                                   target_x.flatten(),
    #                                                   target_y.flatten())
    #    if np.any(source_x < 0.0) and np.any(source_x > 180.0):
    #        raise ValueError('Unable to handle range of longitude.')
    #    # This does not work in all cases e.g. lon > 360
    #    if np.any(source_x > 180.0):
    #        source_desired_x = (source_desired_xy[:, 0].reshape(ny, nx) +
    #                            360.0) % 360.0
    #    else:
    #        source_desired_x = source_desired_xy[:, 0].reshape(ny, nx)
    #    source_desired_y = source_desired_xy[:, 1].reshape(ny, nx)
    #    outof_extent_points = ((source_desired_x < source_x.min()) |
    #                           (source_desired_x > source_x.max()) |
    #                           (source_desired_y < source_y.min()) |
    #                           (source_desired_y > source_y.max()))
    #    # Make array a mask by default (rather than a single bool) to allow mask
    #    # to be assigned to slices.
    #    new_data.mask = np.zeros(new_shape)

    # Step through cube data, regrid onto desired projection and insert results
    # in new_data array
    for index, ll_slice in zip(index_it, slice_it):
        # Regrid source data onto target grid
        index = list(index)
        index[xdim] = slice(None, None)
        index[ydim] = slice(None, None)
        index = tuple(index)  # Numpy>=1.16 : index with tuple, *not* list.
        new_data[index] = cartopy.img_transform.regrid(
            ll_slice.data,
            source_x,
            source_y,
            source_cs,
            target_proj,
            target_x,
            target_y,
        )

    #    # Mask out points beyond extent
    #    new_data[index].mask[outof_extent_points] = True

    # Remove mask if it is unnecessary
    if not np.any(new_data.mask):
        new_data = new_data.data

    # Create new cube
    new_cube = iris.cube.Cube(new_data)

    # Add new grid coords
    x_coord = iris.coords.DimCoord(
        target_x[0, :],
        "projection_x_coordinate",
        units="m",
        coord_system=copy.copy(target_cs),
    )
    y_coord = iris.coords.DimCoord(
        target_y[:, 0],
        "projection_y_coordinate",
        units="m",
        coord_system=copy.copy(target_cs),
    )

    new_cube.add_dim_coord(x_coord, xdim)
    new_cube.add_dim_coord(y_coord, ydim)

    # Add resampled lat/lon in original coord system
    source_desired_xy = source_cs.transform_points(target_proj,
                                                   target_x.flatten(),
                                                   target_y.flatten())
    new_lon_points = source_desired_xy[:, 0].reshape(ny, nx)
    new_lat_points = source_desired_xy[:, 1].reshape(ny, nx)
    new_lon_coord = iris.coords.AuxCoord(
        new_lon_points,
        standard_name="longitude",
        units="degrees",
        coord_system=orig_cs,
    )
    new_lat_coord = iris.coords.AuxCoord(
        new_lat_points,
        standard_name="latitude",
        units="degrees",
        coord_system=orig_cs,
    )
    new_cube.add_aux_coord(new_lon_coord, [ydim, xdim])
    new_cube.add_aux_coord(new_lat_coord, [ydim, xdim])

    coords_to_ignore = set()
    coords_to_ignore.update(cube.coords(contains_dimension=xdim))
    coords_to_ignore.update(cube.coords(contains_dimension=ydim))
    for coord in cube.dim_coords:
        if coord not in coords_to_ignore:
            new_cube.add_dim_coord(coord.copy(), cube.coord_dims(coord))
    for coord in cube.aux_coords:
        if coord not in coords_to_ignore:
            new_cube.add_aux_coord(coord.copy(), cube.coord_dims(coord))
    discarded_coords = coords_to_ignore.difference([lat_coord, lon_coord])
    if discarded_coords:
        warnings.warn("Discarding coordinates that share dimensions with "
                      "{} and {}: {}".format(
                          lat_coord.name(),
                          lon_coord.name(),
                          [coord.name() for coord in discarded_coords],
                      ))

    # TODO handle derived coords/aux_factories

    # Copy metadata across
    new_cube.metadata = cube.metadata

    return new_cube, extent
Esempio n. 11
0
def regrid_area_weighted_rectilinear_src_and_grid(src_cube,
                                                  grid_cube,
                                                  mdtol=0):
    """
    Return a new cube with data values calculated using the area weighted
    mean of data values from src_grid regridded onto the horizontal grid of
    grid_cube.

    This function requires that the horizontal grids of both cubes are
    rectilinear (i.e. expressed in terms of two orthogonal 1D coordinates)
    and that these grids are in the same coordinate system. This function
    also requires that the coordinates describing the horizontal grids
    all have bounds.

    .. note::

        Elements in data array of the returned cube that lie either partially
        or entirely outside of the horizontal extent of the src_cube will
        be masked irrespective of the value of mdtol.

    Args:

    * src_cube:
        An instance of :class:`iris.cube.Cube` that supplies the data,
        metadata and coordinates.
    * grid_cube:
        An instance of :class:`iris.cube.Cube` that supplies the desired
        horizontal grid definition.

    Kwargs:

    * mdtol:
        Tolerance of missing data. The value returned in each element of the
        returned cube's data array will be masked if the fraction of masked
        data in the overlapping cells of the source cube exceeds mdtol. This
        fraction is calculated based on the area of masked cells within each
        target cell. mdtol=0 means no missing data is tolerated while mdtol=1
        will mean the resulting element will be masked if and only if all the
        overlapping cells of the source cube are masked. Defaults to 0.

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

    """
    # Get the 1d monotonic (or scalar) src and grid coordinates.
    src_x, src_y = _get_xy_coords(src_cube)
    grid_x, grid_y = _get_xy_coords(grid_cube)

    # Condition 1: All x and y coordinates must have contiguous bounds to
    # define areas.
    if not src_x.is_contiguous() or not src_y.is_contiguous() or \
            not grid_x.is_contiguous() or not grid_y.is_contiguous():
        raise ValueError("The horizontal grid coordinates of both the source "
                         "and grid cubes must have contiguous bounds.")

    # Condition 2: Everything must have the same coordinate system.
    src_cs = src_x.coord_system
    grid_cs = grid_x.coord_system
    if src_cs != grid_cs:
        raise ValueError("The horizontal grid coordinates of both the source "
                         "and grid cubes must have the same coordinate "
                         "system.")

    # Condition 3: cannot create vector coords from scalars.
    src_x_dims = src_cube.coord_dims(src_x)
    src_x_dim = None
    if src_x_dims:
        src_x_dim = src_x_dims[0]
    src_y_dims = src_cube.coord_dims(src_y)
    src_y_dim = None
    if src_y_dims:
        src_y_dim = src_y_dims[0]
    if src_x_dim is None and grid_x.shape[0] != 1 or \
            src_y_dim is None and grid_y.shape[0] != 1:
        raise ValueError('The horizontal grid coordinates of source cube '
                         'includes scalar coordinates, but the new grid does '
                         'not. The new grid must not require additional data '
                         'dimensions to be created.')

    # Determine whether to calculate flat or spherical areas.
    # Don't only rely on coord system as it may be None.
    spherical = (isinstance(
        src_cs, (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS))
                 or src_x.units == 'degrees' or src_x.units == 'radians')

    # Get src and grid bounds in the same units.
    x_units = cf_units.Unit('radians') if spherical else src_x.units
    y_units = cf_units.Unit('radians') if spherical else src_y.units

    # Operate in highest precision.
    src_dtype = np.promote_types(src_x.bounds.dtype, src_y.bounds.dtype)
    grid_dtype = np.promote_types(grid_x.bounds.dtype, grid_y.bounds.dtype)
    dtype = np.promote_types(src_dtype, grid_dtype)

    src_x_bounds = _get_bounds_in_units(src_x, x_units, dtype)
    src_y_bounds = _get_bounds_in_units(src_y, y_units, dtype)
    grid_x_bounds = _get_bounds_in_units(grid_x, x_units, dtype)
    grid_y_bounds = _get_bounds_in_units(grid_y, y_units, dtype)

    # Determine whether target grid bounds are decreasing. This must
    # be determined prior to wrap_lons being called.
    grid_x_decreasing = grid_x_bounds[-1, 0] < grid_x_bounds[0, 0]
    grid_y_decreasing = grid_y_bounds[-1, 0] < grid_y_bounds[0, 0]

    # Wrapping of longitudes.
    if spherical:
        base = np.min(src_x_bounds)
        modulus = x_units.modulus
        # Only wrap if necessary to avoid introducing floating
        # point errors.
        if np.min(grid_x_bounds) < base or \
                np.max(grid_x_bounds) > (base + modulus):
            grid_x_bounds = iris.analysis.cartography.wrap_lons(
                grid_x_bounds, base, modulus)

    # Determine whether the src_x coord has periodic boundary conditions.
    circular = getattr(src_x, 'circular', False)

    # Use simple cartesian area function or one that takes into
    # account the curved surface if coord system is spherical.
    if spherical:
        area_func = _spherical_area
    else:
        area_func = _cartesian_area

    # Calculate new data array for regridded cube.
    new_data = _regrid_area_weighted_array(src_cube.data, src_x_dim, src_y_dim,
                                           src_x_bounds, src_y_bounds,
                                           grid_x_bounds, grid_y_bounds,
                                           grid_x_decreasing,
                                           grid_y_decreasing, area_func,
                                           circular, mdtol)

    # Wrap up the data as a Cube.
    # Create 2d meshgrids as required by _create_cube func.
    meshgrid_x, meshgrid_y = _meshgrid(grid_x.points, grid_y.points)
    regrid_callback = RectilinearRegridder._regrid
    new_cube = RectilinearRegridder._create_cube(new_data, src_cube, src_x_dim,
                                                 src_y_dim, src_x, src_y,
                                                 grid_x, grid_y, meshgrid_x,
                                                 meshgrid_y, regrid_callback)

    # Slice out any length 1 dimensions.
    indices = [slice(None, None)] * new_data.ndim
    if src_x_dim is not None and new_cube.shape[src_x_dim] == 1:
        indices[src_x_dim] = 0
    if src_y_dim is not None and new_cube.shape[src_y_dim] == 1:
        indices[src_y_dim] = 0
    if 0 in indices:
        new_cube = new_cube[tuple(indices)]

    return new_cube
Esempio n. 12
0
def _map_common(draw_method_name, arg_func, mode, cube, plot_defn, *args,
                **kwargs):
    """
    Draw the given cube on a map using its points or bounds.

    "Mode" parameter will switch functionality between POINT or BOUND plotting.


    """
    # Generate 2d x and 2d y grids.
    y_coord, x_coord = plot_defn.coords
    if mode == iris.coords.POINT_MODE:
        if x_coord.ndim == y_coord.ndim == 1:
            x, y = _meshgrid(x_coord.points, y_coord.points)
        elif x_coord.ndim == y_coord.ndim == 2:
            x = x_coord.points
            y = y_coord.points
        else:
            raise ValueError("Expected 1D or 2D XY coords")
    else:
        try:
            x, y = _meshgrid(x_coord.contiguous_bounds(),
                             y_coord.contiguous_bounds())
        # Exception translation.
        except iris.exceptions.CoordinateMultiDimError:
            raise ValueError("Could not get XY grid from bounds. "
                             "X or Y coordinate not 1D.")
        except ValueError:
            raise ValueError("Could not get XY grid from bounds. "
                             "X or Y coordinate doesn't have 2 bounds "
                             "per point.")

    # Obtain the data array.
    data = cube.data
    if plot_defn.transpose:
        data = data.T

    # If we are global, then append the first column of data the array to the
    # last (and add 360 degrees) NOTE: if it is found that this block of code
    # is useful in anywhere other than this plotting routine, it may be better
    # placed in the CS.
    if getattr(x_coord, 'circular', False):
        _, direction = iris.util.monotonic(x_coord.points,
                                           return_direction=True)
        y = np.append(y, y[:, 0:1], axis=1)
        x = np.append(x, x[:, 0:1] + 360 * direction, axis=1)
        data = ma.concatenate([data, data[:, 0:1]], axis=1)

    # Replace non-cartopy subplot/axes with a cartopy alternative and set the
    # transform keyword.
    kwargs = _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord,
                                                       kwargs)

    if arg_func is not None:
        new_args, kwargs = arg_func(x, y, data, *args, **kwargs)
    else:
        new_args = (x, y, data) + args

    # Draw the contour lines/filled contours.
    axes = kwargs.pop('axes', None)
    plotfn = getattr(axes if axes else plt, draw_method_name)
    return plotfn(*new_args, **kwargs)
Esempio n. 13
0
    def __init__(self, src_cube, target_grid_cube):
        """
        A nearest-neighbour regridder to perform regridding from the source
        grid to the target grid.

        This can then be applied to any source data with the same structure as
        the original 'src_cube'.

        Args:

        * src_cube:
            The :class:`~iris.cube.Cube` defining the source grid.
            The X and Y coordinates can have any shape, but must be mapped over
            the same cube dimensions.

        * target_grid_cube:
            A :class:`~iris.cube.Cube`, whose X and Y coordinates specify a
            desired target grid.
            The X and Y coordinates must be one-dimensional dimension
            coordinates, mapped to different dimensions.
            All other cube components are ignored.

        Returns:
            regridder : (object)

            A callable object with the interface:
                `result_cube = regridder(data)`

            where `data` is a cube with the same grid as the original
            `src_cube`, that is to be regridded to the `target_grid_cube`.

        .. Note::

            For latitude-longitude coordinates, the nearest-neighbour distances
            are computed on the sphere, otherwise flat Euclidean distances are
            used.

            The source and target X and Y coordinates must all have the same
            coordinate system, which may also be None.
            If any X and Y coordinates are latitudes or longitudes, they *all*
            must be.  Otherwise, the corresponding X and Y coordinates must
            have the same units in the source and grid cubes.

        """
        # Make a copy of the source cube, so we can convert coordinate units.
        src_cube = src_cube.copy()

        # Snapshot the target grid and check it is a "normal" grid.
        tgt_x_coord, tgt_y_coord = snapshot_grid(target_grid_cube)

        # Check that the source has unique X and Y coords over common dims.
        if (not src_cube.coords(axis='x') or not src_cube.coords(axis='y')):
            msg = 'Source cube must have X- and Y-axis coordinates.'
            raise ValueError(msg)
        src_x_coord = src_cube.coord(axis='x')
        src_y_coord = src_cube.coord(axis='y')
        if (src_cube.coord_dims(src_x_coord) !=
                src_cube.coord_dims(src_y_coord)):
            msg = ('Source cube X and Y coordinates must have the same '
                   'cube dimensions.')
            raise ValueError(msg)

        # Record *copies* of the original grid coords, in the desired
        # dimension order.
        # This lets us convert the actual ones in use to units of "degrees".
        self.src_grid_coords = [src_y_coord.copy(), src_x_coord.copy()]
        self.tgt_grid_coords = [tgt_y_coord.copy(), tgt_x_coord.copy()]

        # Check that all XY coords have suitable coordinate systems and units.
        coords_all = [src_x_coord, src_y_coord, tgt_x_coord, tgt_y_coord]
        cs = coords_all[0].coord_system
        if not all(coord.coord_system == cs for coord in coords_all):
            msg = ('Source and target cube X and Y coordinates must all have '
                   'the same coordinate system.')
            raise ValueError(msg)

        # Check *all* X and Y coords are lats+lons, if any are.
        latlons = ['latitude' in coord.name() or 'longitude' in coord.name()
                   for coord in coords_all]
        if any(latlons) and not all(latlons):
            msg = ('If any X and Y coordinates are latitudes/longitudes, '
                   'then they all must be.')
            raise ValueError(msg)

        self.grid_is_latlon = any(latlons)
        if self.grid_is_latlon:
            # Convert all XY coordinates to units of "degrees".
            # N.B. already copied the target grid, so the result matches that.
            for coord in coords_all:
                try:
                    coord.convert_units('degrees')
                except ValueError:
                    msg = ('Coordinate {!r} has units of {!r}, which does not '
                           'convert to "degrees".')
                    raise ValueError(msg.format(coord.name(),
                                                str(coord.units)))
        else:
            # Check that source and target have the same X and Y units.
            if (src_x_coord.units != tgt_x_coord.units or
                    src_y_coord.units != tgt_y_coord.units):
                msg = ('Source and target cube X and Y coordinates must '
                       'have the same units.')
                raise ValueError(msg)

        # Record the resulting grid shape.
        self.tgt_grid_shape = tgt_y_coord.shape + tgt_x_coord.shape

        # Calculate sample points as 2d arrays, like broadcast (NY,1)*(1,NX).
        x_2d, y_2d = _meshgrid(tgt_x_coord.points, tgt_y_coord.points)
        # Cast as a "trajectory", to suit the method used.
        self.trajectory = ((tgt_x_coord.name(), x_2d.flatten()),
                           (tgt_y_coord.name(), y_2d.flatten()))
Esempio n. 14
0
def _make_esmpy_field(x_coord, y_coord, ref_name='field',
                      data=None, mask=None):
    """
    Create an ESMPy ESMF.Field on given coordinates.

    Create a ESMF.Grid from the coordinates, defining corners and centre
    positions as lats+lons.
    Add a grid mask if provided.
    Create and return a Field mapped on this Grid, setting data if provided.

    Args:

    * x_coord, y_coord (:class:`iris.coords.Coord`):
        One-dimensional coordinates of shape (nx,) and (ny,).
        Their contiguous bounds define an ESMF.Grid of shape (nx, ny).

    Kwargs:

    * data (:class:`numpy.ndarray`, shape (nx,ny)):
        Set the Field data content.
    * mask (:class:`numpy.ndarray`, boolean, shape (nx,ny)):
        Add a mask item to the grid, assigning it 0/1 where mask=False/True.

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

    # Create a Grid object describing the coordinate cells.
    dims = [len(coord.points) for coord in (x_coord, y_coord)]
    dims = np.array(dims, dtype=np.int32)  # specific type required by ESMF.
    grid = ESMF.Grid(dims)

    # Get all cell corner coordinates as true-lat-lons
    x_bounds, y_bounds = _meshgrid(x_coord.contiguous_bounds(),
                                   y_coord.contiguous_bounds())
    grid_crs = x_coord.coord_system.as_cartopy_crs()
    lon_bounds, lat_bounds = _convert_latlons(grid_crs, x_bounds, y_bounds)

    # Add grid 'coord' element for corners, and fill with corner values.
    grid.add_coords(staggerloc=ESMF.StaggerLoc.CORNER)
    grid_corners_x = grid.get_coords(0, ESMF.StaggerLoc.CORNER)
    grid_corners_x[:] = lon_bounds.T
    grid_corners_y = grid.get_coords(1, ESMF.StaggerLoc.CORNER)
    grid_corners_y[:] = lat_bounds.T

    # calculate the cell centre-points
    # NOTE: we don't care about Iris' idea of where the points 'really' are
    # *but* ESMF requires the data in the CENTER for conservative regrid,
    # according to the documentation :
    #  - http://www.earthsystemmodeling.org/
    #        esmf_releases/public/last/ESMF_refdoc.pdf
    #  - section  22.2.3 : ESMF_REGRIDMETHOD
    #
    # We are currently determining cell centres in native coords, then
    # converting these into true-lat-lons.
    # It is confirmed by experiment that moving these centre location *does*
    # changes the regrid results.
    # TODO: work out why this is needed, and whether these centres are 'right'.

    # Average cell corners in native coordinates, then translate to lats+lons
    # (more costly, but presumably 'more correct' than averaging lats+lons).
    x_centres = x_coord.contiguous_bounds()
    x_centres = 0.5 * (x_centres[:-1] + x_centres[1:])
    y_centres = y_coord.contiguous_bounds()
    y_centres = 0.5 * (y_centres[:-1] + y_centres[1:])
    x_points, y_points = _meshgrid(x_centres, y_centres)
    lon_points, lat_points = _convert_latlons(grid_crs, x_points, y_points)

    # Add grid 'coord' element for centres + fill with centre-points values.
    grid.add_coords(staggerloc=ESMF.StaggerLoc.CENTER)
    grid_centers_x = grid.get_coords(0, ESMF.StaggerLoc.CENTER)
    grid_centers_x[:] = lon_points.T
    grid_centers_y = grid.get_coords(1, ESMF.StaggerLoc.CENTER)
    grid_centers_y[:] = lat_points.T

    # Add a mask item, if requested
    if mask is not None:
        grid.add_item(ESMF.GridItem.MASK,
                      [ESMF.StaggerLoc.CENTER])
        grid_mask = grid.get_item(ESMF.GridItem.MASK)
        grid_mask[:] = np.where(mask, 1, 0)

    # create a Field based on this grid
    field = ESMF.Field(grid, ref_name)

    # assign data content, if provided
    if data is not None:
        field.data[:] = data

    return field
Esempio n. 15
0
def _map_common(draw_method_name, arg_func, mode, cube, plot_defn,
                *args, **kwargs):
    """
    Draw the given cube on a map using its points or bounds.

    "Mode" parameter will switch functionality between POINT or BOUND plotting.


    """
    # Generate 2d x and 2d y grids.
    y_coord, x_coord = plot_defn.coords
    if mode == iris.coords.POINT_MODE:
        if x_coord.ndim == y_coord.ndim == 1:
            x, y = _meshgrid(x_coord.points, y_coord.points)
        elif x_coord.ndim == y_coord.ndim == 2:
            x = x_coord.points
            y = y_coord.points
        else:
            raise ValueError("Expected 1D or 2D XY coords")
    else:
        try:
            x, y = _meshgrid(x_coord.contiguous_bounds(),
                             y_coord.contiguous_bounds())
        # Exception translation.
        except iris.exceptions.CoordinateMultiDimError:
            raise ValueError("Could not get XY grid from bounds. "
                             "X or Y coordinate not 1D.")
        except ValueError:
            raise ValueError("Could not get XY grid from bounds. "
                             "X or Y coordinate doesn't have 2 bounds "
                             "per point.")

    # Obtain the data array.
    data = cube.data
    if plot_defn.transpose:
        data = data.T

    # If we are global, then append the first column of data the array to the
    # last (and add 360 degrees) NOTE: if it is found that this block of code
    # is useful in anywhere other than this plotting routine, it may be better
    # placed in the CS.
    if getattr(x_coord, 'circular', False):
        _, direction = iris.util.monotonic(x_coord.points,
                                           return_direction=True)
        y = np.append(y, y[:, 0:1], axis=1)
        x = np.append(x, x[:, 0:1] + 360 * direction, axis=1)
        data = ma.concatenate([data, data[:, 0:1]], axis=1)

    # Replace non-cartopy subplot/axes with a cartopy alternative and set the
    # transform keyword.
    kwargs = _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord,
                                                       kwargs)

    if arg_func is not None:
        new_args, kwargs = arg_func(x, y, data, *args, **kwargs)
    else:
        new_args = (x, y, data) + args

    # Draw the contour lines/filled contours.
    axes = kwargs.pop('axes', None)
    plotfn = getattr(axes if axes else plt, draw_method_name)
    return plotfn(*new_args, **kwargs)
Esempio n. 16
0
def project(cube, target_proj, nx=None, ny=None):
    """
    Nearest neighbour regrid to a specified target projection.

    Return a new cube that is the result of projecting a cube with 1 or 2
    dimensional latitude-longitude coordinates from its coordinate system into
    a specified projection e.g. Robinson or Polar Stereographic.
    This function is intended to be used in cases where the cube's coordinates
    prevent one from directly visualising the data, e.g. when the longitude
    and latitude are two dimensional and do not make up a regular grid.

    Args:
        * cube
            An instance of :class:`iris.cube.Cube`.
        * target_proj
            An instance of the Cartopy Projection class, or an instance of
            :class:`iris.coord_systems.CoordSystem` from which a projection
            will be obtained.
    Kwargs:
        * nx
            Desired number of sample points in the x direction for a domain
            covering the globe.
        * ny
            Desired number of sample points in the y direction for a domain
            covering the globe.

    Returns:
        An instance of :class:`iris.cube.Cube` and a list describing the
        extent of the projection.

    .. note::

        This function assumes global data and will if necessary extrapolate
        beyond the geographical extent of the source cube using a nearest
        neighbour approach. nx and ny then include those points which are
        outside of the target projection.

    .. note::

        Masked arrays are handled by passing their masked status to the
        resulting nearest neighbour values.  If masked, the value in the
        resulting cube is set to 0.

    .. warning::

        This function uses a nearest neighbour approach rather than any form
        of linear/non-linear interpolation to determine the data value of each
        cell in the resulting cube. Consequently it may have an adverse effect
        on the statistics of the data e.g. the mean and standard deviation
        will not be preserved.

    """
    try:
        lon_coord, lat_coord = _get_lon_lat_coords(cube)
    except IndexError:
        raise ValueError('Cannot get latitude/longitude '
                         'coordinates from cube {!r}.'.format(cube.name()))

    if lat_coord.coord_system != lon_coord.coord_system:
        raise ValueError('latitude and longitude coords appear to have '
                         'different coordinates systems.')

    if lon_coord.units != 'degrees':
        lon_coord = lon_coord.copy()
        lon_coord.convert_units('degrees')
    if lat_coord.units != 'degrees':
        lat_coord = lat_coord.copy()
        lat_coord.convert_units('degrees')

    # Determine source coordinate system
    if lat_coord.coord_system is None:
        # Assume WGS84 latlon if unspecified
        warnings.warn('Coordinate system of latitude and longitude '
                      'coordinates is not specified. Assuming WGS84 Geodetic.')
        orig_cs = iris.coord_systems.GeogCS(semi_major_axis=6378137.0,
                                            inverse_flattening=298.257223563)
    else:
        orig_cs = lat_coord.coord_system

    # Convert to cartopy crs
    source_cs = orig_cs.as_cartopy_crs()

    # Obtain coordinate arrays (ignoring bounds) and convert to 2d
    # if not already.
    source_x = lon_coord.points
    source_y = lat_coord.points
    if source_x.ndim != 2 or source_y.ndim != 2:
        source_x, source_y = _meshgrid(source_x, source_y)

    # Calculate target grid
    target_cs = None
    if isinstance(target_proj, iris.coord_systems.CoordSystem):
        target_cs = target_proj
        target_proj = target_proj.as_cartopy_projection()

    # Resolution of new grid
    if nx is None:
        nx = source_x.shape[1]
    if ny is None:
        ny = source_x.shape[0]

    target_x, target_y, extent = cartopy.img_transform.mesh_projection(
        target_proj, nx, ny)

    # Determine dimension mappings - expect either 1d or 2d
    if lat_coord.ndim != lon_coord.ndim:
        raise ValueError("The latitude and longitude coordinates have "
                         "different dimensionality.")

    latlon_ndim = lat_coord.ndim
    lon_dims = cube.coord_dims(lon_coord)
    lat_dims = cube.coord_dims(lat_coord)

    if latlon_ndim == 1:
        xdim = lon_dims[0]
        ydim = lat_dims[0]
    elif latlon_ndim == 2:
        if lon_dims != lat_dims:
            raise ValueError("The 2d latitude and longitude coordinates "
                             "correspond to different dimensions.")
        # If coords are 2d assume that grid is ordered such that x corresponds
        # to the last dimension (shortest stride).
        xdim = lon_dims[1]
        ydim = lon_dims[0]
    else:
        raise ValueError('Expected the latitude and longitude coordinates '
                         'to have 1 or 2 dimensions, got {} and '
                         '{}.'.format(lat_coord.ndim, lon_coord.ndim))

    # Create array to store regridded data
    new_shape = list(cube.shape)
    new_shape[xdim] = nx
    new_shape[ydim] = ny
    new_data = ma.zeros(new_shape, cube.data.dtype)

    # Create iterators to step through cube data in lat long slices
    new_shape[xdim] = 1
    new_shape[ydim] = 1
    index_it = np.ndindex(*new_shape)
    if lat_coord.ndim == 1 and lon_coord.ndim == 1:
        slice_it = cube.slices([lat_coord, lon_coord])
    elif lat_coord.ndim == 2 and lon_coord.ndim == 2:
        slice_it = cube.slices(lat_coord)
    else:
        raise ValueError('Expected the latitude and longitude coordinates '
                         'to have 1 or 2 dimensions, got {} and '
                         '{}.'.format(lat_coord.ndim, lon_coord.ndim))

#    # Mask out points outside of extent in source_cs - disabled until
#    # a way to specify global/limited extent is agreed upon and code
#    # is generalised to handle -180 to +180, 0 to 360 and >360 longitudes.
#    source_desired_xy = source_cs.transform_points(target_proj,
#                                                   target_x.flatten(),
#                                                   target_y.flatten())
#    if np.any(source_x < 0.0) and np.any(source_x > 180.0):
#        raise ValueError('Unable to handle range of longitude.')
#    # This does not work in all cases e.g. lon > 360
#    if np.any(source_x > 180.0):
#        source_desired_x = (source_desired_xy[:, 0].reshape(ny, nx) +
#                            360.0) % 360.0
#    else:
#        source_desired_x = source_desired_xy[:, 0].reshape(ny, nx)
#    source_desired_y = source_desired_xy[:, 1].reshape(ny, nx)
#    outof_extent_points = ((source_desired_x < source_x.min()) |
#                           (source_desired_x > source_x.max()) |
#                           (source_desired_y < source_y.min()) |
#                           (source_desired_y > source_y.max()))
#    # Make array a mask by default (rather than a single bool) to allow mask
#    # to be assigned to slices.
#    new_data.mask = np.zeros(new_shape)

    # Step through cube data, regrid onto desired projection and insert results
    # in new_data array
    for index, ll_slice in zip(index_it, slice_it):
        # Regrid source data onto target grid
        index = list(index)
        index[xdim] = slice(None, None)
        index[ydim] = slice(None, None)
        new_data[index] = cartopy.img_transform.regrid(ll_slice.data,
                                                       source_x, source_y,
                                                       source_cs,
                                                       target_proj,
                                                       target_x, target_y)

#    # Mask out points beyond extent
#    new_data[index].mask[outof_extent_points] = True

    # Remove mask if it is unnecessary
    if not np.any(new_data.mask):
        new_data = new_data.data

    # Create new cube
    new_cube = iris.cube.Cube(new_data)

    # Add new grid coords
    x_coord = iris.coords.DimCoord(target_x[0, :], 'projection_x_coordinate',
                                   units='m',
                                   coord_system=copy.copy(target_cs))
    y_coord = iris.coords.DimCoord(target_y[:, 0], 'projection_y_coordinate',
                                   units='m',
                                   coord_system=copy.copy(target_cs))

    new_cube.add_dim_coord(x_coord, xdim)
    new_cube.add_dim_coord(y_coord, ydim)

    # Add resampled lat/lon in original coord system
    source_desired_xy = source_cs.transform_points(target_proj,
                                                   target_x.flatten(),
                                                   target_y.flatten())
    new_lon_points = source_desired_xy[:, 0].reshape(ny, nx)
    new_lat_points = source_desired_xy[:, 1].reshape(ny, nx)
    new_lon_coord = iris.coords.AuxCoord(new_lon_points,
                                         standard_name='longitude',
                                         units='degrees',
                                         coord_system=orig_cs)
    new_lat_coord = iris.coords.AuxCoord(new_lat_points,
                                         standard_name='latitude',
                                         units='degrees',
                                         coord_system=orig_cs)
    new_cube.add_aux_coord(new_lon_coord, [ydim, xdim])
    new_cube.add_aux_coord(new_lat_coord, [ydim, xdim])

    coords_to_ignore = set()
    coords_to_ignore.update(cube.coords(contains_dimension=xdim))
    coords_to_ignore.update(cube.coords(contains_dimension=ydim))
    for coord in cube.dim_coords:
        if coord not in coords_to_ignore:
            new_cube.add_dim_coord(coord.copy(), cube.coord_dims(coord))
    for coord in cube.aux_coords:
        if coord not in coords_to_ignore:
            new_cube.add_aux_coord(coord.copy(), cube.coord_dims(coord))
    discarded_coords = coords_to_ignore.difference([lat_coord, lon_coord])
    if discarded_coords:
        warnings.warn('Discarding coordinates that share dimensions with '
                      '{} and {}: {}'.format(lat_coord.name(),
                                             lon_coord.name(),
                                             [coord.name() for
                                              coord in discarded_coords]))

    # TODO handle derived coords/aux_factories

    # Copy metadata across
    new_cube.metadata = cube.metadata

    return new_cube, extent
Esempio n. 17
0
def rotate_winds(u_cube, v_cube, target_cs):
    """
    Transform wind vectors to a different coordinate system.

    The input cubes contain U and V components parallel to the local X and Y
    directions of the input grid at each point.

    The output cubes contain the same winds, at the same locations, but
    relative to the grid directions of a different coordinate system.
    Thus in vector terms, the magnitudes will always be the same, but the
    angles can be different.

    The outputs retain the original horizontal dimension coordinates, but
    also have two 2-dimensional auxiliary coordinates containing the X and
    Y locations in the target coordinate system.

    Args:

    * u_cube
        An instance of :class:`iris.cube.Cube` that contains the x-component
        of the vector.
    * v_cube
        An instance of :class:`iris.cube.Cube` that contains the y-component
        of the vector.
    * target_cs
        An instance of :class:`iris.coord_systems.CoordSystem` that specifies
        the new grid directions.

    Returns:
        A (u', v') tuple of :class:`iris.cube.Cube` instances that are the u
        and v components in the requested target coordinate system.
        The units are the same as the inputs.

    .. note::

        The U and V values relate to distance, with units such as 'm s-1'.
        These are not the same as coordinate vectors, which transform in a
        different manner.

    .. note::

        The names of the output cubes are those of the inputs, prefixed with
        'transformed\_' (e.g. 'transformed_x_wind').

    .. warning::

        Conversion between rotated-pole and non-rotated systems can be
        expressed analytically.  However, this function always uses a numerical
        approach. In locations where this numerical approach does not preserve
        magnitude to an accuracy of 0.1%, the corresponding elements of the
        returned cubes will be masked.

    """
    # Check u_cube and v_cube have the same shape. We iterate through
    # the u and v cube slices which relies on the shapes matching.
    if u_cube.shape != v_cube.shape:
        msg = 'Expected u and v cubes to have the same shape. ' \
              'u cube has shape {}, v cube has shape {}.'
        raise ValueError(msg.format(u_cube.shape, v_cube.shape))

    # Check the u_cube and v_cube have the same x and y coords.
    msg = 'Coordinates differ between u and v cubes. Coordinate {!r} from ' \
          'u cube does not equal coordinate {!r} from v cube.'
    if u_cube.coord(axis='x') != v_cube.coord(axis='x'):
        raise ValueError(msg.format(u_cube.coord(axis='x').name(),
                                    v_cube.coord(axis='x').name()))
    if u_cube.coord(axis='y') != v_cube.coord(axis='y'):
        raise ValueError(msg.format(u_cube.coord(axis='y').name(),
                                    v_cube.coord(axis='y').name()))

    # Check x and y coords have the same coordinate system.
    x_coord = u_cube.coord(axis='x')
    y_coord = u_cube.coord(axis='y')
    if x_coord.coord_system != y_coord.coord_system:
        msg = "Coordinate systems of x and y coordinates differ. " \
              "Coordinate {!r} has a coord system of {!r}, but coordinate " \
              "{!r} has a coord system of {!r}."
        raise ValueError(msg.format(x_coord.name(), x_coord.coord_system,
                                    y_coord.name(), y_coord.coord_system))

    # Convert from iris coord systems to cartopy CRSs to access
    # transform functionality. Use projection as cartopy
    # transform_vectors relies on x_limits and y_limits.
    if x_coord.coord_system is not None:
        src_crs = x_coord.coord_system.as_cartopy_projection()
    else:
        # Default to Geodetic (but actually use PlateCarree as a
        # projection is needed).
        src_crs = ccrs.PlateCarree()
    target_crs = target_cs.as_cartopy_projection()

    # Check the number of dimensions of the x and y coords is the same.
    # Subsequent logic assumes either both 1d or both 2d.
    x = x_coord.points
    y = y_coord.points
    if x.ndim != y.ndim or x.ndim > 2 or y.ndim > 2:
        msg = 'x and y coordinates must have the same number of dimensions ' \
              'and be either 1D or 2D. The number of dimensions are {} and ' \
              '{}, respectively.'.format(x.ndim, y.ndim)
        raise ValueError(msg)

    # Check the dimension mappings match between u_cube and v_cube.
    if u_cube.coord_dims(x_coord) != v_cube.coord_dims(x_coord):
        raise ValueError('Dimension mapping of x coordinate differs '
                         'between u and v cubes.')
    if u_cube.coord_dims(y_coord) != v_cube.coord_dims(y_coord):
        raise ValueError('Dimension mapping of y coordinate differs '
                         'between u and v cubes.')
    x_dims = u_cube.coord_dims(x_coord)
    y_dims = u_cube.coord_dims(y_coord)

    # Convert points to 2D, if not already, and determine dims.
    if x.ndim == y.ndim == 1:
        x, y = _meshgrid(x, y)
        dims = (y_dims[0], x_dims[0])
    else:
        dims = x_dims

    # Transpose x, y 2d arrays to match the order in cube's data
    # array so that x, y and the sliced data all line up.
    if dims[0] > dims[1]:
        x = x.transpose()
        y = y.transpose()

    # Create resulting cubes.
    ut_cube = u_cube.copy()
    vt_cube = v_cube.copy()
    ut_cube.rename('transformed_{}'.format(u_cube.name()))
    vt_cube.rename('transformed_{}'.format(v_cube.name()))

    # Get distance scalings for source crs.
    ds_dx1, ds_dy1 = _crs_distance_differentials(src_crs, x, y)

    # Get distance scalings for target crs.
    x2, y2 = _transform_xy(src_crs, x, y, target_crs)
    ds_dx2, ds_dy2 = _crs_distance_differentials(target_crs, x2, y2)

    ds = DistanceDifferential(ds_dx1, ds_dy1, ds_dx2, ds_dy2)

    # Calculate coordinate partial differentials from source crs to target crs.
    dx2_dx1, dy2_dx1, dx2_dy1, dy2_dy1 = _inter_crs_differentials(src_crs,
                                                                  x, y,
                                                                  target_crs)

    dx2 = PartialDifferential(dx2_dx1, dx2_dy1)
    dy2 = PartialDifferential(dy2_dx1, dy2_dy1)

    # Calculate mask based on preservation of magnitude.
    mask = _transform_distance_vectors_tolerance_mask(src_crs, x, y,
                                                      target_crs,
                                                      ds, dx2, dy2)
    apply_mask = mask.any()
    if apply_mask:
        # Make masked arrays to accept masking.
        ut_cube.data = ma.asanyarray(ut_cube.data)
        vt_cube.data = ma.asanyarray(vt_cube.data)

    # Project vectors with u, v components one horiz slice at a time and
    # insert into the resulting cubes.
    shape = list(u_cube.shape)
    for dim in dims:
        shape[dim] = 1
    ndindex = np.ndindex(*shape)
    for index in ndindex:
        index = list(index)
        for dim in dims:
            index[dim] = slice(None, None)
        index = tuple(index)
        u = u_cube.data[index]
        v = v_cube.data[index]
        ut, vt = _transform_distance_vectors(u, v, ds, dx2, dy2)
        if apply_mask:
            ut = ma.asanyarray(ut)
            ut[mask] = ma.masked
            vt = ma.asanyarray(vt)
            vt[mask] = ma.masked
        ut_cube.data[index] = ut
        vt_cube.data[index] = vt

    # Calculate new coords of locations in target coordinate system.
    xyz_tran = target_crs.transform_points(src_crs, x, y)
    xt = xyz_tran[..., 0].reshape(x.shape)
    yt = xyz_tran[..., 1].reshape(y.shape)

    # Transpose xt, yt 2d arrays to match the dim order
    # of the original x an y arrays - i.e. undo the earlier
    # transpose (if applied).
    if dims[0] > dims[1]:
        xt = xt.transpose()
        yt = yt.transpose()

    xt_coord = iris.coords.AuxCoord(xt,
                                    standard_name='projection_x_coordinate',
                                    coord_system=target_cs)
    yt_coord = iris.coords.AuxCoord(yt,
                                    standard_name='projection_y_coordinate',
                                    coord_system=target_cs)
    # Set units based on coord_system.
    if isinstance(target_cs, (iris.coord_systems.GeogCS,
                              iris.coord_systems.RotatedGeogCS)):
        xt_coord.units = yt_coord.units = 'degrees'
    else:
        xt_coord.units = yt_coord.units = 'm'

    ut_cube.add_aux_coord(xt_coord, dims)
    ut_cube.add_aux_coord(yt_coord, dims)
    vt_cube.add_aux_coord(xt_coord.copy(), dims)
    vt_cube.add_aux_coord(yt_coord.copy(), dims)

    return ut_cube, vt_cube
Esempio n. 18
0
def _regrid_area_weighted_rectilinear_src_and_grid__prepare(
        src_cube, grid_cube):
    """
    First (setup) part of 'regrid_area_weighted_rectilinear_src_and_grid'.

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

    """
    # Get the 1d monotonic (or scalar) src and grid coordinates.
    src_x, src_y = _get_xy_coords(src_cube)
    grid_x, grid_y = _get_xy_coords(grid_cube)

    # Condition 1: All x and y coordinates must have contiguous bounds to
    # define areas.
    if (not src_x.is_contiguous() or not src_y.is_contiguous()
            or not grid_x.is_contiguous() or not grid_y.is_contiguous()):
        raise ValueError("The horizontal grid coordinates of both the source "
                         "and grid cubes must have contiguous bounds.")

    # Condition 2: Everything must have the same coordinate system.
    src_cs = src_x.coord_system
    grid_cs = grid_x.coord_system
    if src_cs != grid_cs:
        raise ValueError("The horizontal grid coordinates of both the source "
                         "and grid cubes must have the same coordinate "
                         "system.")

    # Condition 3: cannot create vector coords from scalars.
    src_x_dims = src_cube.coord_dims(src_x)
    src_x_dim = None
    if src_x_dims:
        src_x_dim = src_x_dims[0]
    src_y_dims = src_cube.coord_dims(src_y)
    src_y_dim = None
    if src_y_dims:
        src_y_dim = src_y_dims[0]
    if (src_x_dim is None and grid_x.shape[0] != 1
            or src_y_dim is None and grid_y.shape[0] != 1):
        raise ValueError("The horizontal grid coordinates of source cube "
                         "includes scalar coordinates, but the new grid does "
                         "not. The new grid must not require additional data "
                         "dimensions to be created.")

    # Determine whether to calculate flat or spherical areas.
    # Don't only rely on coord system as it may be None.
    spherical = (isinstance(
        src_cs,
        (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS),
    ) or src_x.units == "degrees" or src_x.units == "radians")

    # Get src and grid bounds in the same units.
    x_units = cf_units.Unit("radians") if spherical else src_x.units
    y_units = cf_units.Unit("radians") if spherical else src_y.units

    # Operate in highest precision.
    src_dtype = np.promote_types(src_x.bounds.dtype, src_y.bounds.dtype)
    grid_dtype = np.promote_types(grid_x.bounds.dtype, grid_y.bounds.dtype)
    dtype = np.promote_types(src_dtype, grid_dtype)

    src_x_bounds = _get_bounds_in_units(src_x, x_units, dtype)
    src_y_bounds = _get_bounds_in_units(src_y, y_units, dtype)
    grid_x_bounds = _get_bounds_in_units(grid_x, x_units, dtype)
    grid_y_bounds = _get_bounds_in_units(grid_y, y_units, dtype)

    # Create 2d meshgrids as required by _create_cube func.
    meshgrid_x, meshgrid_y = _meshgrid(grid_x.points, grid_y.points)

    # Determine whether target grid bounds are decreasing. This must
    # be determined prior to wrap_lons being called.
    grid_x_decreasing = grid_x_bounds[-1, 0] < grid_x_bounds[0, 0]
    grid_y_decreasing = grid_y_bounds[-1, 0] < grid_y_bounds[0, 0]

    # Wrapping of longitudes.
    if spherical:
        base = np.min(src_x_bounds)
        modulus = x_units.modulus
        # Only wrap if necessary to avoid introducing floating
        # point errors.
        if np.min(grid_x_bounds) < base or np.max(grid_x_bounds) > (base +
                                                                    modulus):
            grid_x_bounds = iris.analysis.cartography.wrap_lons(
                grid_x_bounds, base, modulus)

    # Determine whether the src_x coord has periodic boundary conditions.
    circular = getattr(src_x, "circular", False)

    # Use simple cartesian area function or one that takes into
    # account the curved surface if coord system is spherical.
    if spherical:
        area_func = _spherical_area
    else:
        area_func = _cartesian_area

    def _calculate_regrid_area_weighted_weights(
        src_x_bounds,
        src_y_bounds,
        grid_x_bounds,
        grid_y_bounds,
        grid_x_decreasing,
        grid_y_decreasing,
        area_func,
        circular=False,
    ):
        """
        Compute the area weights used for area-weighted regridding.
        Args:
        * src_x_bounds:
            A NumPy array of bounds along the X axis defining the source grid.
        * src_y_bounds:
            A NumPy array of bounds along the Y axis defining the source grid.
        * grid_x_bounds:
            A NumPy array of bounds along the X axis defining the new grid.
        * grid_y_bounds:
            A NumPy array of bounds along the Y axis defining the new grid.
        * grid_x_decreasing:
            Boolean indicating whether the X coordinate of the new grid is
            in descending order.
        * grid_y_decreasing:
            Boolean indicating whether the Y coordinate of the new grid is
            in descending order.
        * area_func:
            A function that returns an (p, q) array of weights given an (p, 2)
            shaped array of Y bounds and an (q, 2) shaped array of X bounds.
        Kwargs:
        * circular:
            A boolean indicating whether the `src_x_bounds` are periodic.
            Default is False.
        Returns:
            The area weights to be used for area-weighted regridding.
        """
        # Determine which grid bounds are within src extent.
        y_within_bounds = _within_bounds(src_y_bounds, grid_y_bounds,
                                         grid_y_decreasing)
        x_within_bounds = _within_bounds(src_x_bounds, grid_x_bounds,
                                         grid_x_decreasing)

        # Cache which src_bounds are within grid bounds
        cached_x_bounds = []
        cached_x_indices = []
        max_x_indices = 0
        for (x_0, x_1) in grid_x_bounds:
            if grid_x_decreasing:
                x_0, x_1 = x_1, x_0
            x_bounds, x_indices = _cropped_bounds(src_x_bounds, x_0, x_1)
            cached_x_bounds.append(x_bounds)
            cached_x_indices.append(x_indices)
            # Keep record of the largest slice
            if isinstance(x_indices, slice):
                x_indices_size = np.sum(x_indices.stop - x_indices.start)
            else:  # is tuple of indices
                x_indices_size = len(x_indices)
            if x_indices_size > max_x_indices:
                max_x_indices = x_indices_size

        # Cache which y src_bounds areas and weights are within grid bounds
        cached_y_indices = []
        cached_weights = []
        max_y_indices = 0
        for j, (y_0, y_1) in enumerate(grid_y_bounds):
            # Reverse lower and upper if dest grid is decreasing.
            if grid_y_decreasing:
                y_0, y_1 = y_1, y_0
            y_bounds, y_indices = _cropped_bounds(src_y_bounds, y_0, y_1)
            cached_y_indices.append(y_indices)
            # Keep record of the largest slice
            if isinstance(y_indices, slice):
                y_indices_size = np.sum(y_indices.stop - y_indices.start)
            else:  # is tuple of indices
                y_indices_size = len(y_indices)
            if y_indices_size > max_y_indices:
                max_y_indices = y_indices_size

            weights_i = []
            for i, (x_0, x_1) in enumerate(grid_x_bounds):
                # Reverse lower and upper if dest grid is decreasing.
                if grid_x_decreasing:
                    x_0, x_1 = x_1, x_0
                x_bounds = cached_x_bounds[i]
                x_indices = cached_x_indices[i]

                # Determine whether element i, j overlaps with src and hence
                # an area weight should be computed.
                # If x_0 > x_1 then we want [0]->x_1 and x_0->[0] + mod in the case
                # of wrapped longitudes. However if the src grid is not global
                # (i.e. circular) this new cell would include a region outside of
                # the extent of the src grid and thus the weight is therefore
                # invalid.
                outside_extent = x_0 > x_1 and not circular
                if (outside_extent or not y_within_bounds[j]
                        or not x_within_bounds[i]):
                    weights = False
                else:
                    # Calculate weights based on areas of cropped bounds.
                    if isinstance(x_indices, tuple) and isinstance(
                            y_indices, tuple):
                        raise RuntimeError("Cannot handle split bounds "
                                           "in both x and y.")
                    weights = area_func(y_bounds, x_bounds)
                weights_i.append(weights)
            cached_weights.append(weights_i)
        return (
            tuple(cached_x_indices),
            tuple(cached_y_indices),
            max_x_indices,
            max_y_indices,
            tuple(cached_weights),
        )

    (
        cached_x_indices,
        cached_y_indices,
        max_x_indices,
        max_y_indices,
        cached_weights,
    ) = _calculate_regrid_area_weighted_weights(
        src_x_bounds,
        src_y_bounds,
        grid_x_bounds,
        grid_y_bounds,
        grid_x_decreasing,
        grid_y_decreasing,
        area_func,
        circular,
    )

    # Go further, calculating the full weights array that we'll need in the
    # perform step and the indices we'll need to extract from the cube we're
    # regridding (src_data)

    result_y_extent = len(grid_y_bounds)
    result_x_extent = len(grid_x_bounds)

    # Total number of points
    num_target_pts = result_y_extent * result_x_extent

    # Create empty array to hold weights
    src_area_weights = np.zeros(
        list((max_y_indices, max_x_indices, num_target_pts)))

    # Built for the case where the source cube isn't masked
    blank_weights = np.zeros((num_target_pts, ))
    new_data_mask_basis = np.full(
        (len(cached_y_indices), len(cached_x_indices)), False, dtype=np.bool_)

    # To permit fancy indexing, we need to store our data in an array whose
    # first two dimensions represent the indices needed for the target cell.
    # Since target cells can require a different number of indices, the size of
    # these dimensions should be the maximum of this number.
    # This means we need to track whether the data in
    # that array is actually required and build those squared-off arrays
    # TODO: Consider if a proper mask would be better
    src_area_datas_required = np.full(
        (max_y_indices, max_x_indices, num_target_pts), False)
    square_data_indices_y = np.zeros(
        (max_y_indices, max_x_indices, num_target_pts), dtype=int)
    square_data_indices_x = np.zeros(
        (max_y_indices, max_x_indices, num_target_pts), dtype=int)

    # Stack the weights for each target point and build the indices we'll need
    # to extract the src_area_data
    target_pt_ji = -1
    for j, y_indices in enumerate(cached_y_indices):
        for i, x_indices in enumerate(cached_x_indices):
            target_pt_ji += 1
            # Determine whether to mask element i, j based on whether
            # there are valid weights.
            weights = cached_weights[j][i]
            if weights is False:
                # Prepare for the src_data not being masked by storing the
                # information that will let us fill the data with zeros and
                # weights as one. The weighted average result will be the same,
                # but we avoid dividing by zero.
                blank_weights[target_pt_ji] = True
                new_data_mask_basis[j, i] = True
            else:
                # Establish which indices are actually in y_indices and x_indices
                if isinstance(y_indices, slice):
                    y_indices = list(
                        range(
                            y_indices.start,
                            y_indices.stop,
                            y_indices.step or 1,
                        ))
                else:
                    y_indices = list(y_indices)

                if isinstance(x_indices, slice):
                    x_indices = list(
                        range(
                            x_indices.start,
                            x_indices.stop,
                            x_indices.step or 1,
                        ))
                else:
                    x_indices = list(x_indices)

                # For the weights, we just need the lengths of these as we're
                # dropping them into a pre-made array

                len_y = len(y_indices)
                len_x = len(x_indices)

                src_area_weights[0:len_y, 0:len_x, target_pt_ji] = weights

                # To build the indices for the source cube, we need equal
                # shaped array so we pad with 0s and record the need to mask
                # them in src_area_datas_required
                padded_y_indices = y_indices + [0] * (max_y_indices - len_y)
                padded_x_indices = x_indices + [0] * (max_x_indices - len_x)

                square_data_indices_y[..., target_pt_ji] = np.array(
                    padded_y_indices)[:, np.newaxis]
                square_data_indices_x[..., target_pt_ji] = padded_x_indices

                src_area_datas_required[0:len_y, 0:len_x, target_pt_ji] = True

    # Package up the return data

    weights_info = (
        blank_weights,
        src_area_weights,
        new_data_mask_basis,
    )

    index_info = (
        result_x_extent,
        result_y_extent,
        square_data_indices_y,
        square_data_indices_x,
        src_area_datas_required,
    )

    # Now return it

    return (
        src_x,
        src_y,
        src_x_dim,
        src_y_dim,
        grid_x,
        grid_y,
        meshgrid_x,
        meshgrid_y,
        weights_info,
        index_info,
    )