Exemplo n.º 1
0
def create_cube(lon_min, lon_max, bounds=False):
    n_lons = max(lon_min, lon_max) - min(lon_max, lon_min)
    data = np.arange(4 * 3 * n_lons, dtype='f4').reshape(4, 3, n_lons)
    data = biggus.NumpyArrayAdapter(data)
    cube = Cube(data, standard_name='x_wind', units='ms-1')
    cube.add_dim_coord(iris.coords.DimCoord([0, 20, 40, 80],
                                            long_name='level_height',
                                            units='m'), 0)
    cube.add_aux_coord(iris.coords.AuxCoord([1.0, 0.9, 0.8, 0.6],
                                            long_name='sigma'), 0)
    cube.add_dim_coord(iris.coords.DimCoord([-45, 0, 45], 'latitude',
                                            units='degrees'), 1)
    step = 1 if lon_max > lon_min else -1
    circular = (abs(lon_max - lon_min) == 360)
    cube.add_dim_coord(iris.coords.DimCoord(np.arange(lon_min, lon_max, step),
                                            'longitude', units='degrees',
                                            circular=circular), 2)
    if bounds:
        cube.coord('longitude').guess_bounds()
    cube.add_aux_coord(iris.coords.AuxCoord(
        np.arange(3 * n_lons).reshape(3, n_lons) * 10, 'surface_altitude',
        units='m'), [1, 2])
    cube.add_aux_factory(iris.aux_factory.HybridHeightFactory(
        cube.coord('level_height'), cube.coord('sigma'),
        cube.coord('surface_altitude')))
    return cube
 def setUp(self):
     # A (3, 2, 4) cube with a masked element.
     cube = Cube(np.ma.arange(24, dtype=np.int32).reshape((3, 2, 4)))
     cs = GeogCS(6371229)
     coord = DimCoord(points=np.array([-1, 0, 1], dtype=np.int32),
                      standard_name='latitude',
                      units='degrees',
                      coord_system=cs)
     cube.add_dim_coord(coord, 0)
     coord = DimCoord(points=np.array([-1, 0, 1, 2], dtype=np.int32),
                      standard_name='longitude',
                      units='degrees',
                      coord_system=cs)
     cube.add_dim_coord(coord, 2)
     cube.coord('latitude').guess_bounds()
     cube.coord('longitude').guess_bounds()
     cube.data[1, 1, 2] = ma.masked
     self.src_cube = cube
     # Create (7, 2, 9) grid cube.
     self.grid_cube = _resampled_grid(cube, 2.3, 2.4)
Exemplo n.º 3
0
def latlon_from_cube(cube: Cube) -> ndarray:
    """
    Produce an array of latitude-longitude coordinates used by an Iris cube.

    Args:
        cube:
            Cube with spatial coords.

    Returns:
        Latitude-longitude pairs (N x 2).
    """
    lats_name, lons_name = latlon_names(cube)
    lats_data = cube.coord(lats_name).points
    lons_data = cube.coord(lons_name).points
    lats_mesh, lons_mesh = np.meshgrid(lats_data, lons_data, indexing="ij")
    latlon = np.dstack((lats_mesh, lons_mesh)).reshape((-1, 2))
    return latlon
Exemplo n.º 4
0
    def _check_dim_coords(temperature: Cube, lapse_rate: Cube) -> None:
        """Throw an error if the dimension coordinates are not the same for
        temperature and lapse rate cubes

        Args:
            temperature
            lapse_rate
        """
        for crd in temperature.coords(dim_coords=True):
            try:
                if crd != lapse_rate.coord(crd.name()):
                    raise ValueError(
                        'Lapse rate cube coordinate "{}" does not match '
                        "temperature cube coordinate".format(crd.name()))
            except CoordinateNotFoundError:
                raise ValueError("Lapse rate cube has no coordinate "
                                 '"{}"'.format(crd.name()))
Exemplo n.º 5
0
    def _create_output_cube(self, orogenh_data: ndarray,
                            reference_cube: Cube) -> Cube:
        """Creates a cube containing orographic enhancement values in SI units.

        Args:
            orogenh_data:
                Orographic enhancement value in mm h-1
            reference_cube:
                Cube with the correct time and forecast period coordinates on
                the UK standard grid

        Returns:
            Orographic enhancement cube (m s-1)
        """
        # create cube containing high resolution data in mm/h
        x_coord = self.topography.coord(axis="x")
        y_coord = self.topography.coord(axis="y")
        for coord in [x_coord, y_coord]:
            coord.points = coord.points.astype(np.float32)
            if coord.bounds is not None:
                coord.bounds = coord.bounds.astype(np.float32)

        aux_coords = []
        for coord in ["time", "forecast_reference_time", "forecast_period"]:
            aux_coords.append((reference_cube.coord(coord), None))

        attributes = generate_mandatory_attributes([reference_cube])
        attributes["title"] = "unknown"  # remove possible wrong grid info.
        for key in MOSG_GRID_ATTRIBUTES:
            try:
                attributes[key] = self.topography.attributes[key]
            except KeyError:
                pass

        orog_enhance_cube = iris.cube.Cube(
            orogenh_data,
            long_name="orographic_enhancement",
            units="mm h-1",
            attributes=attributes,
            dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)],
            aux_coords_and_dims=aux_coords,
        )
        orog_enhance_cube.convert_units("m s-1")

        return orog_enhance_cube
Exemplo n.º 6
0
    def process(self, cube: Cube, coord_name: str, midpoint: float) -> Cube:
        """Calculate triangular weights for a given cube and coord.

        Args:
            cube:
                Cube to blend across the coord.
            coord_name:
                Name of coordinate in the cube to be blended.
            midpoint:
                The centre point of the triangular function.  This is
                assumed to be provided in the same units as "self.width",
                ie "self.parameter_units" as initialised.

        Returns:
            1D cube of normalised (sum = 1.0) weights matching length
            of input dimension to be blended.

        Raises:
            TypeError : input is not a cube
        """
        if not isinstance(cube, iris.cube.Cube):
            msg = (
                "The first argument must be an instance of "
                "iris.cube.Cube but is"
                " {0:s}".format(str(type(cube)))
            )
            raise TypeError(msg)

        cube_coord = cube.coord(coord_name)
        coord_vals = cube_coord.points
        coord_units = cube_coord.units

        # Rescale width and midpoint if in different units to the coordinate
        if coord_units != self.parameters_units:
            width_in_coord_units = self.parameters_units.convert(
                self.width, coord_units
            )
            midpoint = self.parameters_units.convert(midpoint, coord_units)
        else:
            width_in_coord_units = copy.deepcopy(self.width)

        weights = self.triangular_weights(coord_vals, midpoint, width_in_coord_units)

        weights_cube = WeightsUtilities.build_weights_cube(cube, weights, coord_name)
        return weights_cube
Exemplo n.º 7
0
    def _update_metadata(self, cube: Cube) -> None:
        """Rename the cube and add attributes to the threshold coordinate
        after merging
        """
        threshold_coord = cube.coord(self.threshold_coord_name)
        threshold_coord.attributes.update(
            {"spp__relative_to_threshold": self.comparison_operator.spp_string}
        )
        if cube.cell_methods:
            format_cell_methods_for_probability(cube, self.threshold_coord_name)

        cube.rename(
            "probability_of_{parameter}_{relative_to}_threshold".format(
                parameter=self.threshold_coord_name,
                relative_to=probability_is_above_or_below(cube),
            )
        )
        cube.units = Unit(1)
Exemplo n.º 8
0
    def _gradient_from_diff(diff: Cube, axis: str) -> ndarray:
        """
        Calculate the gradient along the x or y axis from differences between
        adjacent grid squares.

        Args:
            diff:
                Cube containing differences along the x or y axis
            axis:
                Short-hand reference for the x or y coordinate, as allowed by
                iris.util.guess_coord_axis.

        Returns:
            Array of the gradients in the coordinate direction specified.
        """
        grid_spacing = np.diff(diff.coord(axis=axis).points)[0]
        gradient = diff.data / grid_spacing
        return gradient
Exemplo n.º 9
0
    def _run_recursion(
        cube: Cube,
        smoothing_coefficients_x: Cube,
        smoothing_coefficients_y: Cube,
        iterations: int,
    ) -> Cube:
        """
        Method to run the recursive filter.

        Args:
            cube:
                2D cube containing the input data to which the recursive
                filter will be applied.
            smoothing_coefficients_x:
                2D cube containing array of smoothing_coefficient values that
                will be used when applying the recursive filter along the
                x-axis.
            smoothing_coefficients_y:
                2D cube containing array of smoothing_coefficient values that
                will be used when applying the recursive filter along the
                y-axis.
            iterations:
                The number of iterations of the recursive filter

        Returns:
            Cube containing the smoothed field after the recursive filter
            method has been applied to the input cube.
        """
        (x_index, ) = cube.coord_dims(cube.coord(axis="x").name())
        (y_index, ) = cube.coord_dims(cube.coord(axis="y").name())
        output = cube.data

        for _ in range(iterations):
            output = RecursiveFilter._recurse_forward(
                output, smoothing_coefficients_x.data, x_index)
            output = RecursiveFilter._recurse_backward(
                output, smoothing_coefficients_x.data, x_index)
            output = RecursiveFilter._recurse_forward(
                output, smoothing_coefficients_y.data, y_index)
            output = RecursiveFilter._recurse_backward(
                output, smoothing_coefficients_y.data, y_index)
            cube.data = output
        return cube
Exemplo n.º 10
0
    def _fill_timezones(self, input_cube: Cube) -> None:
        """
        Populates the output cube data with data from input_cube. This is done by
        multiplying the inverse of the timezone_cube.data with the input_cube.data and
        summing along the time axis. Because timezone_cube.data is a mask of 1 and 0,
        inverting it gives 1 where we WANT data and 0 where we don't. Summing these up
        produces the result. The same logic can be used for times.
        Modifies self.output_cube and self.time_points.
        Assumes that input_cube and self.timezone_cube have been arranged so that time
        or UTC_offset are the inner-most coord (dim=-1).

        Args:
            input_cube:
                Cube of data to extract timezone-offsets from. Must contain a time
                coord spanning all the timezones.

        Raises:
            TypeError:
                If combining the timezone_cube and input_cube results in float64 data.
                (Hint: timezone_cube should be int8 and input cube should be float32)
        """
        # Get the output_data
        result = input_cube.data * (1 - self.timezone_cube.data)
        self.output_data = result.sum(axis=-1)

        # Check resulting dtype
        enforce_dtype("multiply", [input_cube, self.timezone_cube], result)

        # Sort out the time points
        input_time_points = input_cube.coord("time").points
        # Add scalar coords to allow broadcast to spatial coords.
        times = input_time_points.reshape((1, 1, len(input_time_points))) * (
            1 - self.timezone_cube.data
        )
        self.time_points = times.sum(axis=-1)

        # Sort out the time bounds (if present)
        bounds_offsets = self._get_time_bounds_offset(input_cube)
        if bounds_offsets is not None:
            # Add scalar coords to allow broadcast to spatial coords.
            self.time_bounds = bounds_offsets.reshape(
                (1, 1, 2)
            ) + self.time_points.reshape(list(self.time_points.shape) + [1])
Exemplo n.º 11
0
def to_threshold_inequality(cube: Cube, above: bool = True) -> Cube:
    """Takes a cube and a target relative to threshold inequality; above or not
    above. The function returns probabilities in relation to the threshold values
    with the target inequality.

    The threshold inequality is limited to being above (above=True) or below
    (above=False) a threshold, rather than more specific targets such as
    "greater_than_or_equal_to". It is not possible to flip probabilities from
    e.g. "less_than_or_equal_to" to "greater_than_or_equal_to", only to
    "greater_than". As such the operation will use the valid inversion that
    achieves the broader target inequality.

    Args:
        cube:
            A probability cube with a threshold coordinate.
        above:
            Targets an above (gt, ge) threshold inequality if True, otherwise
            targets a below (lt, le) threshold inequality if False.

    Returns:
        A cube with the probabilities relative to the threshold values with
        the target inequality.

    Raised:
        ValueError: If the input cube has no threshold coordinate.
    """
    try:
        threshold = cube.coord(var_name="threshold")
    except CoordinateNotFoundError:
        raise ValueError(
            "Cube does not have a threshold coordinate, probabilities "
            "cannot be inverted if present.")

    inequality = threshold.attributes["spp__relative_to_threshold"]
    spp_lookup = comparison_operator_dict()
    above_attr = [spp_lookup[ineq].spp_string for ineq in ["ge", "gt"]]
    below_attr = [spp_lookup[ineq].spp_string for ineq in ["le", "lt"]]

    if (inequality in below_attr and above) or (inequality in above_attr
                                                and not above):
        return invert_probabilities(cube)
    return cube
Exemplo n.º 12
0
def check_radius_against_distance(cube: Cube, radius: float) -> None:
    """Check required distance isn't greater than the size of the domain.

    Args:
        cube:
            The cube to check.
        radius:
            The radius, which cannot be more than half of the
            size of the domain.
    """
    axes = []
    for axis in ["x", "y"]:
        coord = cube.coord(axis=axis).copy()
        coord.convert_units("metres")
        axes.append((max(coord.points) - min(coord.points)))

    max_allowed = np.sqrt(axes[0]**2 + axes[1]**2) * 0.5
    if radius > max_allowed:
        raise ValueError(f"Distance of {radius}m exceeds max domain "
                         f"distance of {max_allowed}m")
Exemplo n.º 13
0
def remove_cube_halo(cube: Cube, halo_radius: float) -> Cube:
    """
    Remove halo of halo_radius from a cube.

    This function converts the halo radius into
    the number of grid points in the x and y coordinate
    that need to be removed. It then calls remove_halo_from_cube
    which only acts on a cube with x and y coordinates so we
    need to slice the cube and them merge the cube back together
    ensuring the resulting cube has the same dimension coordinates.

    Args:
        cube:
            Cube on extended grid
        halo_radius:
            Size of border to remove, in metres

    Returns:
        New cube with the halo removed.
    """
    halo_size_x = distance_to_number_of_grid_cells(cube, halo_radius, axis="x")
    halo_size_y = distance_to_number_of_grid_cells(cube, halo_radius, axis="y")

    result_slices = iris.cube.CubeList()
    for cube_slice in cube.slices([cube.coord(axis="y"),
                                   cube.coord(axis="x")]):
        cube_halo = remove_halo_from_cube(cube_slice, halo_size_x, halo_size_y)
        result_slices.append(cube_halo)
    result = result_slices.merge_cube()

    # re-promote any scalar dimensions lost in slice / merge
    req_dims = get_dim_coord_names(cube)
    present_dims = get_dim_coord_names(result)
    for coord in req_dims:
        if coord not in present_dims:
            result = iris.util.new_axis(result, coord)

    # re-order (needed if scalar dimensions have been re-added)
    enforce_coordinate_ordering(result, req_dims)

    return result
Exemplo n.º 14
0
    def create_difference_cube(
        cube: Cube, coord_name: str, diff_along_axis: ndarray
    ) -> Cube:
        """
        Put the difference array into a cube with the appropriate
        metadata.

        Args:
            cube:
                Cube from which the differences have been calculated.
            coord_name:
                The name of the coordinate over which the difference
                have been calculated.
            diff_along_axis:
                Array containing the differences.

        Returns:
            Cube containing the differences calculated along the
            specified axis.
        """
        points = cube.coord(coord_name).points
        mean_points = (points[1:] + points[:-1]) / 2

        # Copy cube metadata and coordinates into a new cube.
        # Create a new coordinate for the coordinate along which the
        # difference has been calculated.
        metadata_dict = copy.deepcopy(cube.metadata._asdict())
        diff_cube = Cube(diff_along_axis, **metadata_dict)

        for coord in cube.dim_coords:
            dims = cube.coord_dims(coord)
            if coord.name() in [coord_name]:
                coord = coord.copy(points=mean_points)
            diff_cube.add_dim_coord(coord.copy(), dims)
        for coord in cube.aux_coords:
            dims = cube.coord_dims(coord)
            diff_cube.add_aux_coord(coord.copy(), dims)
        for coord in cube.derived_coords:
            dims = cube.coord_dims(coord)
            diff_cube.add_aux_coord(coord.copy(), dims)
        return diff_cube
Exemplo n.º 15
0
    def _update_metadata(self, output_cube: Cube,
                         original_units: Unit) -> None:
        """
        Update output cube name and threshold coordinate

        Args:
            output_cube:
                Cube containing new "between_thresholds" probabilities
            original_units:
                Required threshold-type coordinate units
        """
        new_name = self.cube.name().replace(
            "{}_threshold".format(probability_is_above_or_below(self.cube)),
            "between_thresholds",
        )
        output_cube.rename(new_name)

        new_thresh_coord = output_cube.coord(self.thresh_coord.name())
        new_thresh_coord.convert_units(original_units)
        new_thresh_coord.attributes[
            "spp__relative_to_threshold"] = "between_thresholds"
Exemplo n.º 16
0
    def check_cell_methods(self, cube: Cube) -> None:
        """Checks cell methods are permitted and correct"""
        if any([substr in cube.name() for substr in PRECIP_ACCUM_NAMES]):
            msg = f"Expected sum over time cell method for {cube.name()}"
            if not cube.cell_methods:
                self.errors.append(msg)
            else:
                found_cm = False
                for cm in cube.cell_methods:
                    if (
                        cm.method == PRECIP_ACCUM_CM.method
                        and cm.coord_names == PRECIP_ACCUM_CM.coord_names
                    ):
                        found_cm = True
                if not found_cm:
                    self.errors.append(msg)

        for cm in cube.cell_methods:
            if cm.method in COMPLIANT_CM_METHODS:
                self.methods += f" {cm.method} over {cm.coord_names[0]}"
                if self.field_type == self.PROB:
                    if not cm.comments or cm.comments[0] != f"of {self.diagnostic}":
                        self.errors.append(
                            f"Cell method {cm} on probability data should have comment "
                            f"'of {self.diagnostic}'"
                        )
                # check point and bounds on method coordinate
                if "time" in cm.coord_names:
                    if cube.coord("time").bounds is None:
                        self.errors.append(f"Cube of{self.methods} has no time bounds")

            elif cm in NONCOMP_CMS or cm.method in NONCOMP_CM_METHODS:
                self.errors.append(f"Non-standard cell method {cm}")
            else:
                # flag method which might be invalid, but we can't be sure
                self.warnings.append(
                    f"Unexpected cell method {cm}. Please check the standard to "
                    "ensure this is valid"
                )
Exemplo n.º 17
0
def invert_probabilities(cube: Cube) -> Cube:
    """Given a cube with a probability threshold, invert the probabilities
    relative to the existing thresholding inequality. Update the coordinate
    metadata to indicate the new threshold inequality.

    Args:
        cube:
            A probability cube with a threshold coordinate.

    Returns:
        Cube with the probabilities inverted relative to the input thresholding
        inequality.

    Raises:
        ValueError: If no threshold coordinate is found.
    """
    try:
        threshold = cube.coord(var_name="threshold")
    except CoordinateNotFoundError:
        raise ValueError(
            "Cube does not have a threshold coordinate, probabilities "
            "cannot be inverted if present.")

    comparison_operator_lookup = comparison_operator_dict()
    inequality = threshold.attributes["spp__relative_to_threshold"]
    (inverse, ) = set([
        value.inverse for key, value in comparison_operator_lookup.items()
        if value.spp_string == inequality
    ])
    new_inequality = comparison_operator_lookup[inverse].spp_string
    inverted_probabilities = cube.copy(data=(1.0 - cube.data))
    inverted_probabilities.coord(
        threshold).attributes["spp__relative_to_threshold"] = new_inequality

    new_name = (cube.name().replace("above", "below") if "above"
                in cube.name() else cube.name().replace("below", "above"))
    inverted_probabilities.rename(new_name)

    return inverted_probabilities
Exemplo n.º 18
0
    def _update_time(input_time: Coord, advected_cube: Cube,
                     timestep: timedelta) -> None:
        """Increment validity time on the advected cube

        Args:
            input_time:
                Time coordinate from source cube
            advected_cube:
                Cube containing advected data (modified in place)
            timestep:
                Time difference between the advected output and the source
        """
        original_datetime = next(input_time.cells())[0]
        new_datetime = original_datetime + timestep
        new_time = input_time.units.date2num(new_datetime)
        time_coord_name = "time"
        time_coord_spec = TIME_COORDS[time_coord_name]
        time_coord = advected_cube.coord(time_coord_name)
        time_coord.points = new_time
        time_coord.convert_units(time_coord_spec.units)
        time_coord.points = round_close(time_coord.points,
                                        dtype=time_coord_spec.dtype)
Exemplo n.º 19
0
    def extract_coordinates(self, neighbour_cube: Cube) -> Cube:
        """
        Extract the desired set of grid coordinates that correspond to spot
        sites from the neighbour cube.

        Args:
            neighbour_cube:
                A cube containing information about the spot data sites and
                their grid point neighbours.

        Returns:
            A cube containing only the x and y grid coordinates for the
            grid point neighbours given the chosen neighbour selection
            method. The neighbour cube contains the indices stored as
            floating point values, so they are converted to integers
            in this cube.

        Raises:
            ValueError if the neighbour_selection_method expected is not found
            in the neighbour cube.
        """
        method = iris.Constraint(
            neighbour_selection_method_name=self.neighbour_selection_method
        )
        index_constraint = iris.Constraint(grid_attributes_key=["x_index", "y_index"])
        coordinate_cube = neighbour_cube.extract(method & index_constraint)
        if coordinate_cube:
            coordinate_cube.data = np.rint(coordinate_cube.data).astype(int)
            return coordinate_cube

        available_methods = neighbour_cube.coord(
            "neighbour_selection_method_name"
        ).points
        raise ValueError(
            'The requested neighbour_selection_method "{}" is not available in'
            " this neighbour_cube. Available methods are: {}.".format(
                self.neighbour_selection_method, available_methods
            )
        )
Exemplo n.º 20
0
def _get_cycletime_point(cube: Cube, cycletime: str) -> int64:
    """
    For cycle and model blending, establish the current cycletime to set on
    the cube after blending.

    Args:
        blended_cube
        cycletime:
            Current cycletime in YYYYMMDDTHHmmZ format

    Returns:
        Cycle time point in units matching the input cube forecast reference
        time coordinate
    """
    frt_coord = cube.coord("forecast_reference_time")
    frt_units = frt_coord.units.origin
    frt_calendar = frt_coord.units.calendar
    # raises TypeError if cycletime is None
    cycletime_point = cycletime_to_number(cycletime,
                                          time_unit=frt_units,
                                          calendar=frt_calendar)
    return round_close(cycletime_point, dtype=np.int64)
Exemplo n.º 21
0
def find_threshold_coordinate(cube: Cube) -> Coord:
    """Find threshold coordinate in cube.

    Compatible with both the old (cube.coord("threshold")) and new
    (cube.coord.var_name == "threshold") IMPROVER metadata standards.

    Args:
        cube:
            Cube containing thresholded probability data

    Returns:
        Threshold coordinate

    Raises:
        TypeError: If cube is not of type iris.cube.Cube.
        CoordinateNotFoundError: If no threshold coordinate is found.
    """
    if not isinstance(cube, iris.cube.Cube):
        msg = (
            "Expecting data to be an instance of "
            "iris.cube.Cube but is {0}.".format(type(cube))
        )
        raise TypeError(msg)

    threshold_coord = None
    try:
        threshold_coord = cube.coord("threshold")
    except CoordinateNotFoundError:
        for coord in cube.coords():
            if coord.var_name == "threshold":
                threshold_coord = coord
                break

    if threshold_coord is None:
        msg = "No threshold coord found on {0:s} data".format(cube.name())
        raise CoordinateNotFoundError(msg)

    return threshold_coord
Exemplo n.º 22
0
    def _set_time(self, cube: Cube) -> None:
        """
        Set self.time to a datetime object specifying the date and time for
        which the masks should be created. self.time is set in UTC.

        Args:
            cube:
                The cube from which the validity time should be taken if one
                has not been explicitly provided by the user.
        """
        if self.time:
            self.time = datetime.strptime(self.time, "%Y%m%dT%H%MZ")
            self.time = pytz.utc.localize(self.time)
        else:
            try:
                self.time = cube.coord("time").cell(0).point
                self.time = pytz.utc.localize(self.time)
            except CoordinateNotFoundError:
                msg = (
                    "The input cube does not contain a 'time' coordinate. "
                    "As such a time must be provided by the user."
                )
                raise ValueError(msg)
Exemplo n.º 23
0
    def find_heightgrid(self, wind: Cube) -> ndarray:
        """Setup the height grid.

        Setup the height grid either from the 1D or 3D height grid
        that was supplied to the plugin or from the z-axis information
        from the wind grid.

        Args:
            wind:
                3D or 4D - representing the wind data.

        Returns:
            1D or 3D array - representing the height grid.
        """
        if self.height_levels is None:
            hld = wind.coord(self.z_name).points
        else:
            hld = iris.util.squeeze(self.height_levels)
            if np.isnan(hld.data).any() or (hld.data == RMDI).any():
                raise ValueError("height grid contains invalid points")
            if hld.ndim == 3:
                try:
                    xap, yap, zap, _ = self.find_coord_order(hld)
                    hld.transpose([yap, xap, zap])
                except KeyError:
                    raise ValueError("height grid different from wind grid")
            elif hld.ndim == 1:
                try:
                    hld = next(hld.slices([self.z_name]))
                except CoordinateNotFoundError:
                    raise ValueError("height z coordinate differs from wind z")
            else:
                raise ValueError("hld must have a dimension length of "
                                 "either 3 or 1"
                                 "hld.ndim is {}".format(hld.ndim))
            hld = hld.data
        return hld
Exemplo n.º 24
0
    def build_KDTree(self, land_mask: Cube) -> Tuple[cKDTree, ndarray]:
        """
        Build a KDTree for extracting the nearest point or points to a site.
        The tree can be built with a constrained set of grid points, e.g. only
        land points, if required.

        Args:
            land_mask:
                A land mask cube for the model/grid from which grid point
                neighbours are being selected.

        Returns:
            - A KDTree containing the required nodes, built using the
              scipy cKDTree method.
            - An array of shape (n_nodes, 2) that contains the x and y
              indices that correspond to the selected node,
              e.g. node=100 -->  x_coord_index=10, y_coord_index=300,
              index_nodes[100] = [10, 300]
        """
        if self.land_constraint:
            included_points = np.nonzero(land_mask.data)
        else:
            included_points = np.where(np.isfinite(land_mask.data.data))

        x_indices = included_points[0]
        y_indices = included_points[1]
        x_coords = land_mask.coord(axis="x").points[x_indices]
        y_coords = land_mask.coord(axis="y").points[y_indices]

        if self.global_coordinate_system:
            nodes = self.geocentric_cartesian(land_mask, x_coords, y_coords)
        else:
            nodes = list(zip(x_coords, y_coords))

        index_nodes = np.array(list(zip(x_indices, y_indices)))

        return cKDTree(nodes), index_nodes
Exemplo n.º 25
0
def calculate_input_grid_spacing(cube_in: Cube) -> Tuple[float, float]:
    """
    Calculate grid spacing in latitude and logitude.
    Check if input source grid is on even-spacing and ascending lat/lon system.

    Args:
        cube_in:
            Input source cube.

    Returns:
        - Grid spacing in latitude, in degree.
        - Grid spacing in logitude, in degree.

    Raises:
        ValueError:
            If input grid is not on a latitude/longitude system or
            input grid coordinates are not ascending.
    """
    # check if in lat/lon system
    if lat_lon_determine(cube_in) is not None:
        raise ValueError("Input grid is not on a latitude/longitude system")

    # calculate grid spacing
    lon_spacing = calculate_grid_spacing(cube_in,
                                         "degree",
                                         axis="x",
                                         rtol=4.0e-5)
    lat_spacing = calculate_grid_spacing(cube_in,
                                         "degree",
                                         axis="y",
                                         rtol=4.0e-5)

    y_coord = cube_in.coord(axis="y").points
    x_coord = cube_in.coord(axis="x").points
    if x_coord[-1] < x_coord[0] or y_coord[-1] < y_coord[0]:
        raise ValueError("Input grid coordinates are not ascending.")
    return lat_spacing, lon_spacing
Exemplo n.º 26
0
    def apply_circular_kernel(self, cube: Cube, ranges: int) -> Cube:
        """
        Method to apply a circular kernel to the data within the input cube in
        order to smooth the resulting field.

        Args:
            cube:
                Cube containing to array to apply CircularNeighbourhood
                processing to.
            ranges:
                Number of grid cells in the x and y direction used to create
                the kernel.

        Returns:
            Cube containing the smoothed field after the kernel has been
            applied.
        """
        data = cube.data
        full_ranges = np.zeros([np.ndim(data)])
        axes = []
        for axis in ["x", "y"]:
            coord_name = cube.coord(axis=axis).name()
            axes.append(cube.coord_dims(coord_name)[0])

        for axis in axes:
            full_ranges[axis] = ranges
        self.kernel = circular_kernel(full_ranges, ranges, self.weighted_mode)
        # Smooth the data by applying the kernel.
        if self.sum_or_fraction == "sum":
            total_area = 1.0
        else:
            # sum_or_fraction is in fraction mode
            total_area = np.sum(self.kernel)

        cube.data = correlate(data, self.kernel, mode="nearest") / total_area
        return cube
Exemplo n.º 27
0
    def percentile_weighted_mean(self, cube: Cube,
                                 weights: Optional[Cube]) -> Cube:
        """
        Blend percentile data using the weights provided.

        Args:
            cube:
                The cube which is being blended over self.blend_coord. Assumes
                self.blend_coord and percentile are leading coordinates (enforced
                in process).
            weights:
                Cube of blending weights.

        Returns:
            The cube with percentile values blended over self.blend_coord,
            with suitable weightings applied.
        """
        non_perc_slice = next(cube.slices_over(PERC_COORD))
        weights_array = self.get_weights_array(non_perc_slice, weights)
        weights_array = self._normalise_weights(weights_array)

        # Set up aggregator
        PERCENTILE_BLEND = Aggregator(
            "mean",  # Use CF-compliant cell method.
            PercentileBlendingAggregator.aggregate,
        )

        cube_new = collapsed(
            cube,
            self.blend_coord,
            PERCENTILE_BLEND,
            percentiles=cube.coord(PERC_COORD).points,
            arr_weights=weights_array,
        )

        return cube_new
Exemplo n.º 28
0
    def ensure_monotonic_increase_in_chosen_direction(self,
                                                      cube: Cube) -> Cube:
        """Ensure that the chosen coordinate is monotonically increasing in
        the specified direction.

        Args:
            cube:
                The cube containing the coordinate to check.
                Note that the input cube will be modified by this method.

        Returns:
            The cube containing a coordinate that is monotonically
            increasing in the desired direction.
        """
        coord_name = self.coord_name_to_integrate
        increasing_order = np.all(np.diff(cube.coord(coord_name).points) > 0)

        if increasing_order and not self.positive_integration:
            cube = sort_coord_in_cube(cube, coord_name, descending=True)

        if not increasing_order and self.positive_integration:
            cube = sort_coord_in_cube(cube, coord_name)

        return cube
Exemplo n.º 29
0
    def get_weights_array(self, cube: Cube, weights: Optional[Cube]) -> ndarray:
        """
        Given a 1 or multidimensional cube of weights, reshape and broadcast
        these to the shape of the data cube. If no weights are provided, an
        array of weights is returned that equally weights all slices across
        the blending coordinate.

        Args:
            cube:
                Template cube to reshape weights, with a leading blend coordinate
            weights:
                Cube of initial blending weights or None

        Returns:
            An array of weights that matches the template cube shape.
        """
        if weights:
            weights_array = self.shape_weights(cube, weights)
        else:
            (number_of_fields,) = cube.coord(self.blend_coord).shape
            weight = FLOAT_DTYPE(1.0 / number_of_fields)
            weights_array = np.broadcast_to(weight, cube.shape)

        return weights_array
Exemplo n.º 30
0
    def process(self, cube: Cube) -> Tuple[Cube, Cube]:
        """
        Calculate the difference along the x and y axes and return
        the result in separate cubes. The difference along each axis is
        calculated using numpy.diff.

        Args:
            cube:
                Cube from which the differences will be calculated.

        Returns:
            - Cube after the differences have been calculated along the
              x axis.
            - Cube after the differences have been calculated along the
              y axis.
        """
        diffs = []
        for axis in ["x", "y"]:
            coord_name = cube.coord(axis=axis).name()
            diff_cube = self.create_difference_cube(
                cube, coord_name, self.calculate_difference(cube, coord_name))
            self._update_metadata(diff_cube, coord_name, cube.name())
            diffs.append(diff_cube)
        return tuple(diffs)
Exemplo n.º 31
0
    def calc_true_north_offset(reference_cube: Cube) -> ndarray:
        """
        Calculate the angles between grid North and true North, as a
        matrix of values on the grid of the input reference cube.

        Args:
            reference_cube:
                2D cube on grid for which "north" is required.  Provides both
                coordinate system (reference_cube.coord_system()) and template
                spatial grid on which the angle adjustments should be provided.

        Returns:
            Angle in radians by which wind direction wrt true North at
            each point must be rotated to be relative to grid North.
        """
        reference_x_coord = reference_cube.coord(axis="x")
        reference_y_coord = reference_cube.coord(axis="y")

        # find corners of reference_cube grid in lat / lon coordinates
        latlon = [
            GLOBAL_CRS.as_cartopy_crs().transform_point(
                reference_x_coord.points[i],
                reference_y_coord.points[j],
                reference_cube.coord_system().as_cartopy_crs(),
            )
            for i in [0, -1]
            for j in [0, -1]
        ]
        latlon = np.array(latlon).T.tolist()

        # define lat / lon coordinates to cover the reference_cube grid at an
        # equivalent resolution
        lat_points = np.linspace(
            np.floor(min(latlon[1])),
            np.ceil(max(latlon[1])),
            len(reference_y_coord.points),
        )
        lon_points = np.linspace(
            np.floor(min(latlon[0])),
            np.ceil(max(latlon[0])),
            len(reference_x_coord.points),
        )

        lat_coord = DimCoord(
            lat_points, "latitude", units="degrees", coord_system=GLOBAL_CRS
        )
        lon_coord = DimCoord(
            lon_points, "longitude", units="degrees", coord_system=GLOBAL_CRS
        )

        # define a unit vector wind towards true North over the lat / lon grid
        udata = np.zeros(reference_cube.shape, dtype=np.float32)
        vdata = np.ones(reference_cube.shape, dtype=np.float32)

        ucube_truenorth = Cube(
            udata,
            "grid_eastward_wind",
            dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
        )
        vcube_truenorth = Cube(
            vdata,
            "grid_northward_wind",
            dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
        )

        # rotate unit vector onto reference_cube coordinate system
        ucube, vcube = rotate_winds(
            ucube_truenorth, vcube_truenorth, reference_cube.coord_system()
        )

        # unmask and regrid rotated winds onto reference_cube grid
        ucube.data = ucube.data.data
        ucube = ucube.regrid(reference_cube, Linear())
        vcube.data = vcube.data.data
        vcube = vcube.regrid(reference_cube, Linear())

        # ratio of u to v winds is the tangent of the angle which is the
        # true North to grid North rotation
        angle_adjustment = np.arctan2(ucube.data, vcube.data)

        return angle_adjustment
Exemplo n.º 32
0
Arquivo: mock.py Projeto: cedadev/cis
def make_mock_cube(lat_dim_length=5, lon_dim_length=3, lon_range=None, alt_dim_length=0, pres_dim_length=0,
                   time_dim_length=0,
                   horizontal_offset=0, altitude_offset=0, pressure_offset=0, time_offset=0, data_offset=0,
                   surf_pres_offset=0,
                   hybrid_ht_len=0, hybrid_pr_len=0, geopotential_height=False, dim_order=None, mask=False):
    """
    Makes a cube of any shape required, with coordinate offsets from the default available. If no arguments are
    given get a 5x3 cube of the form:
        array([[1,2,3],
               [4,5,6],
               [7,8,9],
               [10,11,12],
               [13,14,15]])
        and coordinates in latitude:
            array([ -10, -5, 0, 5, 10 ])
        longitude:
            array([ -5, 0, 5 ])
    :param lat_dim_length: Latitude grid length
    :param lon_dim_length: Longitude grid length
    :param alt_dim_length: Altitude grid length
    :param pres_dim_length: Pressure grid length
    :param time_dim_length: Time grid length
    :param horizontal_offset: Offset from the default grid, in degrees, in lat and lon
    :param altitude_offset: Offset from the default grid in altitude
    :param pressure_offset: Offset from the default grid in pressure
    :param time_offset: Offset from the default grid in time
    :param data_offset: Offset from the default data values
    :param surf_pres_offset: Offset for the optional surface pressure field
    :param hybrid_ht_len: Hybrid height grid length
    :param hybrid_pr_len: Hybrid pressure grid length
    :param geopotential_height: Include a geopotential height field when calcluting a hybrid pressure? (default False)
    :param dim_order: List of 'lat', 'lon', 'alt', 'pres', 'time' in the order in which the dimensions occur
    :param mask: A mask to apply to the data, this should be either a scalar or the same shape as the data
    :return: A cube with well defined data.
    """
    import iris
    from iris.aux_factory import HybridHeightFactory, HybridPressureFactory

    data_size = 1
    DIM_NAMES = ['lat', 'lon', 'alt', 'pres', 'time', 'hybrid_ht', 'hybrid_pr']
    dim_lengths = [lat_dim_length, lon_dim_length, alt_dim_length, pres_dim_length, time_dim_length, hybrid_ht_len,
                   hybrid_pr_len]
    lon_range = lon_range or (-5., 5.)

    if dim_order is None:
        dim_order = list(DIM_NAMES)

    if any([True for d in dim_order if d not in DIM_NAMES]):
        raise ValueError("dim_order contains unrecognised name")

    for idx, dim in enumerate(DIM_NAMES):
        if dim_lengths[idx] == 0 and dim in dim_order:
            del dim_order[dim_order.index(dim)]

    coord_map = {}
    for idx, dim in enumerate(dim_order):
        coord_map[dim] = dim_order.index(dim)
    coord_list = [None] * len(coord_map)

    if lat_dim_length:
        coord_list[coord_map['lat']] = (DimCoord(np.linspace(-10., 10., lat_dim_length) + horizontal_offset,
                                                 standard_name='latitude', units='degrees', var_name='lat'),
                                        coord_map['lat'])
        data_size *= lat_dim_length

    if lon_dim_length:
        coord_list[coord_map['lon']] = (
            DimCoord(np.linspace(lon_range[0], lon_range[1], lon_dim_length) + horizontal_offset,
                     standard_name='longitude', units='degrees', var_name='lon'), coord_map['lon'])
        data_size *= lon_dim_length

    if alt_dim_length:
        coord_list[coord_map['alt']] = (DimCoord(np.linspace(0., 7., alt_dim_length) + altitude_offset,
                                                 standard_name='altitude', units='metres', var_name='alt'),
                                        coord_map['alt'])
        data_size *= alt_dim_length

    if pres_dim_length:
        coord_list[coord_map['pres']] = (DimCoord(np.linspace(0., 7., pres_dim_length) + pressure_offset,
                                                  standard_name='air_pressure', units='hPa', var_name='pres'),
                                         coord_map['pres'])
        data_size *= pres_dim_length

    if time_dim_length:
        t0 = datetime.datetime(1984, 8, 27)
        times = np.array([t0 + datetime.timedelta(days=d + time_offset) for d in range(time_dim_length)])
        time_nums = convert_datetime_to_std_time(times)
        time_bounds = None
        if time_dim_length == 1:
            time_bounds = convert_datetime_to_std_time(np.array([times[0] - datetime.timedelta(days=0.5),
                                                                       times[0] + datetime.timedelta(days=0.5)]))
        coord_list[coord_map['time']] = (DimCoord(time_nums, standard_name='time',
                                                  units='days since 1600-01-01 00:00:00', var_name='time',
                                                  bounds=time_bounds),
                                         coord_map['time'])
        data_size *= time_dim_length

    if hybrid_ht_len:
        coord_list[coord_map['hybrid_ht']] = (DimCoord(np.arange(hybrid_ht_len, dtype='i8') + 10,
                                                       "model_level_number", units="1"), coord_map['hybrid_ht'])
        data_size *= hybrid_ht_len

    if hybrid_pr_len:
        coord_list[coord_map['hybrid_pr']] = (DimCoord(np.arange(hybrid_pr_len, dtype='i8'),
                                                       "atmosphere_hybrid_sigma_pressure_coordinate", units="1"),
                                              coord_map['hybrid_pr'])
        data_size *= hybrid_pr_len

    data = np.reshape(np.arange(data_size) + data_offset + 1., tuple(len(i[0].points) for i in coord_list))

    if mask:
        data = np.ma.asarray(data)
        data.mask = mask

    return_cube = Cube(data, dim_coords_and_dims=coord_list, var_name='rain', standard_name='rainfall_rate',
                       long_name="TOTAL RAINFALL RATE: LS+CONV KG/M2/S", units="kg m-2 s-1")

    if hybrid_ht_len:
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 40,
                                                       long_name="level_height",
                                                       units="m", var_name='hybrid_ht'), coord_map['hybrid_ht'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 50,
                                                       long_name="sigma", units="1", var_name='sigma'),
                                  coord_map['hybrid_ht'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(
            np.arange(lat_dim_length * lon_dim_length, dtype='i8').reshape(lat_dim_length, lon_dim_length) + 100,
            long_name="surface_altitude",
            units="m"), [coord_map['lat'], coord_map['lon']])

        return_cube.add_aux_factory(HybridHeightFactory(
            delta=return_cube.coord("level_height"),
            sigma=return_cube.coord("sigma"),
            orography=return_cube.coord("surface_altitude")))
    elif hybrid_pr_len:
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='i8') + 40,
                                                       long_name="hybrid A coefficient at layer midpoints",
                                                       units="Pa", var_name='a'), coord_map['hybrid_pr'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='f8') + 50,
                                                       long_name="hybrid B coefficient at layer midpoints", units="1",
                                                       var_name='b'),
                                  coord_map['hybrid_pr'])
        return_cube.add_aux_coord(
            iris.coords.AuxCoord(np.arange(lat_dim_length * lon_dim_length * time_dim_length, dtype='i8')
                                 .reshape(lat_dim_length, lon_dim_length, time_dim_length) * 100000 + surf_pres_offset,
                                 "surface_air_pressure", units="Pa"),
            [coord_map['lat'], coord_map['lon'], coord_map['time']])

        if geopotential_height:
            return_cube.add_aux_coord(iris.coords.AuxCoord(
                np.arange(lat_dim_length * lon_dim_length * time_dim_length * hybrid_pr_len, dtype='i8')
                .reshape(lat_dim_length, lon_dim_length, time_dim_length, hybrid_pr_len) + 10,
                "altitude", long_name="Geopotential height at layer midpoints", units="meter"),
                [coord_map['lat'], coord_map['lon'], coord_map['time'], coord_map['hybrid_pr']])

        return_cube.add_aux_factory(HybridPressureFactory(
            delta=return_cube.coord("hybrid A coefficient at layer midpoints"),
            sigma=return_cube.coord("hybrid B coefficient at layer midpoints"),
            surface_air_pressure=return_cube.coord("surface_air_pressure")))

    for coord in return_cube.coords(dim_coords=True):
        if coord.bounds is None:
            coord.guess_bounds()

    return return_cube
Exemplo n.º 33
0
    def process(self, spot_data_cube: Cube, neighbour_cube: Cube,
                gridded_lapse_rate_cube: Cube) -> Cube:
        """
        Extract lapse rates from the appropriate grid points and apply them to
        the spot extracted temperatures.

        The calculation is::

         lapse_rate_adjusted_temperatures = temperatures + lapse_rate *
         vertical_displacement

        Args:
            spot_data_cube:
                A spot data cube of temperatures for the spot data sites,
                extracted from the gridded temperature field. These
                temperatures will have been extracted using the same
                neighbour_cube and neighbour_selection_method that are being
                used here.
            neighbour_cube:
                The neighbour_cube that contains the grid coordinates at which
                lapse rates should be extracted and the vertical displacement
                between those grid points on the model orography and the spot
                data sites actual altitudes. This cube is only updated when
                a new site is added.
            gridded_lapse_rate_cube:
                A cube of temperature lapse rates on the same grid as that from
                which the spot data temperatures were extracted.

        Returns:
            A copy of the input spot_data_cube with the data modified by
            the lapse rates to give a better representation of the site's
            temperatures.

        Raises:
            ValueError:
                If the lapse rate cube was provided but the diagnostic being
                processed is not air temperature.
            ValueError:
                If the lapse rate cube provided does not have the name
                "air_temperature_lapse_rate"
            ValueError:
                If the lapse rate cube does not contain a single valued height
                coordinate.

        Warns:
            warning:
                If a lapse rate cube was provided, but the height of the
                temperature does not match that of the data used.
        """

        if is_probability(spot_data_cube):
            msg = (
                "Input cube has a probability coordinate which cannot be lapse "
                "rate adjusted. Input data should be in percentile or "
                "deterministic space only.")
            raise ValueError(msg)

        # Check that we are dealing with temperature data.
        if spot_data_cube.name() not in [
                "air_temperature", "feels_like_temperature"
        ]:
            msg = (
                "The diagnostic being processed is not air temperature "
                "or feels like temperature and therefore cannot be adjusted.")
            raise ValueError(msg)

        if not gridded_lapse_rate_cube.name() == "air_temperature_lapse_rate":
            msg = ("A cube has been provided as a lapse rate cube but does "
                   "not have the expected name air_temperature_lapse_rate: "
                   "{}".format(gridded_lapse_rate_cube.name()))
            raise ValueError(msg)

        try:
            lapse_rate_height_coord = gridded_lapse_rate_cube.coord("height")
        except (CoordinateNotFoundError):
            msg = ("Lapse rate cube does not contain a single valued height "
                   "coordinate. This is required to ensure it is applied to "
                   "equivalent temperature data.")
            raise CoordinateNotFoundError(msg)

        # Check the height of the temperature data matches that used to
        # calculate the lapse rates. If so, adjust temperatures using the lapse
        # rate values.
        if not spot_data_cube.coord("height") == lapse_rate_height_coord:
            raise ValueError(
                "A lapse rate cube was provided, but the height of the "
                "temperature data does not match that of the data used "
                "to calculate the lapse rates. As such the temperatures "
                "were not adjusted with the lapse rates.")

        # Check the cubes are compatible.
        check_grid_match(
            [neighbour_cube, spot_data_cube, gridded_lapse_rate_cube])

        # Extract the lapse rates that correspond to the spot sites.
        spot_lapse_rate = SpotExtraction(
            neighbour_selection_method=self.neighbour_selection_method)(
                neighbour_cube, gridded_lapse_rate_cube)

        # Extract vertical displacements between the model orography and sites.
        method_constraint = iris.Constraint(
            neighbour_selection_method_name=self.neighbour_selection_method)
        data_constraint = iris.Constraint(
            grid_attributes_key="vertical_displacement")
        vertical_displacement = neighbour_cube.extract(method_constraint
                                                       & data_constraint)

        # Apply lapse rate adjustment to the temperature at each site.
        new_spot_lapse_rate = iris.util.broadcast_to_shape(
            spot_lapse_rate.data, spot_data_cube.shape, [-1])
        new_temperatures = (
            spot_data_cube.data +
            (new_spot_lapse_rate * vertical_displacement.data)).astype(
                np.float32)
        new_spot_cube = spot_data_cube.copy(data=new_temperatures)
        return new_spot_cube
Exemplo n.º 34
0
    def test_multidim(self):
        # Testing with >2D data to demonstrate correct operation over
        # additional non-XY dimensions (including data masking), which is
        # handled by the PointInCell wrapper class.

        # Define a simple target grid first, in plain latlon coordinates.
        plain_latlon_cs = GeogCS(EARTH_RADIUS)
        grid_x_coord = DimCoord(points=[15.0, 25.0, 35.0],
                                bounds=[[10.0, 20.0],
                                        [20.0, 30.0],
                                        [30.0, 40.0]],
                                standard_name='longitude',
                                units='degrees',
                                coord_system=plain_latlon_cs)
        grid_y_coord = DimCoord(points=[-30.0, -50.0],
                                bounds=[[-20.0, -40.0], [-40.0, -60.0]],
                                standard_name='latitude',
                                units='degrees',
                                coord_system=plain_latlon_cs)
        grid_cube = Cube(np.zeros((2, 3)))
        grid_cube.add_dim_coord(grid_y_coord, 0)
        grid_cube.add_dim_coord(grid_x_coord, 1)

        # Define some key points in true-lat/lon thta have known positions
        # First 3x2 points in the centre of each output cell.
        x_centres, y_centres = np.meshgrid(grid_x_coord.points,
                                           grid_y_coord.points)
        # An extra point also falling in cell 1, 1
        x_in11, y_in11 = 26.3, -48.2
        # An extra point completely outside the target grid
        x_out, y_out = 70.0, -40.0

        # Define a rotated coord system for the source data
        pole_lon, pole_lat = -125.3, 53.4
        src_cs = RotatedGeogCS(grid_north_pole_latitude=pole_lat,
                               grid_north_pole_longitude=pole_lon,
                               ellipsoid=plain_latlon_cs)

        # Concatenate all the testpoints in a flat array, and find the rotated
        # equivalents.
        xx = list(x_centres.flat[:]) + [x_in11, x_out]
        yy = list(y_centres.flat[:]) + [y_in11, y_out]
        xx, yy = rotate_pole(lons=np.array(xx),
                             lats=np.array(yy),
                             pole_lon=pole_lon,
                             pole_lat=pole_lat)
        # Define handy index numbers for all these.
        i00, i01, i02, i10, i11, i12, i_in, i_out = range(8)

        # Build test data in the shape Z,YX = (3, 8)
        data = [[1, 2, 3, 11, 12, 13, 7, 99],
                [1, 2, 3, 11, 12, 13, 7, 99],
                [7, 6, 5, 51, 52, 53, 12, 1]]
        mask = [[0, 0, 0, 0, 0, 0, 0, 0],
                [0, 1, 0, 0, 0, 0, 1, 0],
                [0, 0, 0, 0, 0, 0, 0, 0]]
        src_data = np.ma.array(data, mask=mask, dtype=float)

        # Make the source cube.
        src_cube = Cube(src_data)
        src_x = AuxCoord(xx,
                         standard_name='grid_longitude',
                         units='degrees',
                         coord_system=src_cs)
        src_y = AuxCoord(yy,
                         standard_name='grid_latitude',
                         units='degrees',
                         coord_system=src_cs)
        src_z = DimCoord(np.arange(3), long_name='z')
        src_cube.add_dim_coord(src_z, 0)
        src_cube.add_aux_coord(src_x, 1)
        src_cube.add_aux_coord(src_y, 1)
        # Add in some extra metadata, to ensure it gets copied over.
        src_cube.add_aux_coord(DimCoord([0], long_name='extra_scalar_coord'))
        src_cube.attributes['extra_attr'] = 12.3

        # Define what the expected answers should be, shaped (3, 2, 3).
        expected_result = [
            [[1.0, 2.0, 3.0],
             [11.0, 0.5 * (12 + 7), 13.0]],
            [[1.0, -999, 3.0],
             [11.0, 12.0, 13.0]],
            [[7.0, 6.0, 5.0],
             [51.0, 0.5 * (52 + 12), 53.0]],
            ]
        expected_result = np.ma.masked_less(expected_result, 0)

        # Perform the calculation with the regridder.
        regridder = Regridder(src_cube, grid_cube)

        # Check all is as expected.
        result = regridder(src_cube)
        self.assertEqual(result.coord('z'), src_cube.coord('z'))
        self.assertEqual(result.coord('extra_scalar_coord'),
                         src_cube.coord('extra_scalar_coord'))
        self.assertEqual(result.coord('longitude'),
                         grid_cube.coord('longitude'))
        self.assertEqual(result.coord('latitude'),
                         grid_cube.coord('latitude'))
        self.assertMaskedArrayAlmostEqual(result.data, expected_result)