Example #1
0
def create_cube_with_halo(cube, halo_radius):
    """
    Create a template cube defining a new grid by adding a fixed width halo
    on all sides to the input cube grid.  The cube contains no meaningful
    data.

    Args:
        cube (iris.cube.Cube):
            Cube on original grid
        halo_radius (float):
            Size of border to pad original grid, in metres

    Returns:
        iris.cube.Cube:
            New cube defining the halo-padded grid (data set to zero)
    """
    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")

    # create padded x- and y- coordinates
    x_coord = pad_coord(cube.coord(axis="x"), halo_size_x, "add")
    y_coord = pad_coord(cube.coord(axis="y"), halo_size_y, "add")

    halo_cube = iris.cube.Cube(
        np.zeros((len(y_coord.points), len(x_coord.points)), dtype=np.float32),
        long_name="grid_with_halo",
        units=Unit("no_unit"),
        dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)],
    )

    return halo_cube
Example #2
0
 def test_single_point_range_0(self):
     """Test behaviour with zero range."""
     cube = self.cube
     radius = 0.0
     msg = "Please specify a positive distance in metres"
     with self.assertRaisesRegex(ValueError, msg):
         distance_to_number_of_grid_cells(cube, radius)
Example #3
0
    def run(self,
            cube: Cube,
            radius: float,
            mask_cube: Optional[Cube] = None) -> Cube:
        """
        Call the methods required to calculate and apply a circular
        neighbourhood.

        Args:
            cube:
                Cube containing to array to apply CircularNeighbourhood
                processing to.
            radius:
                Radius in metres for use in specifying the number of
                grid cells used to create a circular neighbourhood.
            mask_cube:
                Cube containing the array to be used as a mask.

        Returns:
            Cube containing the smoothed field after the kernel has been
            applied.
        """
        if mask_cube is not None:
            msg = ("The use of a mask cube with a circular kernel is not "
                   "yet implemented.")
            raise NotImplementedError(msg)

        # Check that the cube has an equal area grid.
        check_if_grid_is_equal_area(cube)
        grid_cells = distance_to_number_of_grid_cells(cube, radius)
        cube = self.apply_circular_kernel(cube, grid_cells)
        return cube
Example #4
0
    def _update_spatial_weights(self, cube, weights, fuzzy_length):
        """
        Update weights using spatial information

        Args:
            cube (iris.cube.Cube):
                Cube of input data to be blended
            weights (iris.cube.Cube):
                Initial 1D cube of weights scaled by self.weighting_coord
            fuzzy_length (float):
                Distance (in metres) over which to smooth weights at domain
                boundaries

        Returns:
            iris.cube.Cube:
                Updated 3D cube of spatially-varying weights
        """
        check_if_grid_is_equal_area(cube)
        grid_cells = distance_to_number_of_grid_cells(
            cube, fuzzy_length, return_int=False
        )
        plugin = SpatiallyVaryingWeightsFromMask(
            self.blend_coord, fuzzy_length=grid_cells
        )
        weights = plugin(cube, weights)
        return weights
Example #5
0
 def test_max_distance(self):
     """
     Test the distance in metres to grid cell conversion within a maximum
     distance in grid cells.
     """
     result = distance_to_number_of_grid_cells(self.cube, self.DISTANCE)
     self.assertEqual(result, 3)
Example #6
0
    def run(self, cube, radius, mask_cube=None):
        """
        Call the methods required to apply a square neighbourhood
        method to a cube.

        The steps undertaken are:

        1. Set up cubes by determining, if the arrays are masked.
        2. Pad the input array with a halo and then calculate the neighbourhood
           of the haloed array.
        3. Remove the halo from the neighbourhooded array and deal with a mask,
           if required.

        Args:
            cube (iris.cube.Cube):
                Cube containing the array to which the square neighbourhood
                will be applied.
            radius (float):
                Radius in metres for use in specifying the number of
                grid cells used to create a square neighbourhood.
            mask_cube (iris.cube.Cube):
                Cube containing the array to be used as a mask.

        Returns:
            iris.cube.Cube:
                Cube containing the smoothed field after the square
                neighbourhood method has been applied.
        """
        # If the data is masked, the mask will be processed as well as the
        # original_data * mask array.
        check_radius_against_distance(cube, radius)
        original_attributes = cube.attributes
        original_methods = cube.cell_methods
        grid_cells = distance_to_number_of_grid_cells(cube, radius)
        nb_size = 2 * grid_cells + 1
        try:
            mask_cube_data = mask_cube.data
        except AttributeError:
            mask_cube_data = None

        result_slices = iris.cube.CubeList()
        for cube_slice in cube.slices([cube.coord(axis="y"), cube.coord(axis="x")]):
            cube_slice.data = self._calculate_neighbourhood(
                cube_slice.data,
                mask_cube_data,
                nb_size,
                self.sum_or_fraction == "sum",
                self.re_mask,
            )
            result_slices.append(cube_slice)
        neighbourhood_averaged_cube = result_slices.merge_cube()

        neighbourhood_averaged_cube.cell_methods = original_methods
        neighbourhood_averaged_cube.attributes = original_attributes

        neighbourhood_averaged_cube = check_cube_coordinates(
            cube, neighbourhood_averaged_cube
        )
        return neighbourhood_averaged_cube
Example #7
0
 def test_distance_to_grid_cells_other_axis(self):
     """Test the distance in metres to grid cell conversion along the
     y-axis."""
     self.cube.coord(
         axis="y").points = 0.5 * self.cube.coord(axis="y").points
     result = distance_to_number_of_grid_cells(self.cube,
                                               self.DISTANCE,
                                               axis="y")
     self.assertEqual(result, 6)
Example #8
0
    def run(self, cube, radius, mask_cube=None):
        """
        Call the methods required to apply a square neighbourhood
        method to a cube.

        The steps undertaken are:

        1. Set up cubes by determining, if the arrays are masked.
        2. Pad the input array with a halo and then calculate the neighbourhood
           of the haloed array.
        3. Remove the halo from the neighbourhooded array and deal with a mask,
           if required.

        Args:
            cube (iris.cube.Cube):
                Cube containing the array to which the square neighbourhood
                will be applied.
            radius (float):
                Radius in metres for use in specifying the number of
                grid cells used to create a square neighbourhood.
            mask_cube (iris.cube.Cube):
                Cube containing the array to be used as a mask.

        Returns:
            iris.cube.Cube:
                Cube containing the smoothed field after the square
                neighbourhood method has been applied.
        """
        # If the data is masked, the mask will be processed as well as the
        # original_data * mask array.
        check_radius_against_distance(cube, radius)
        original_attributes = cube.attributes
        original_methods = cube.cell_methods
        grid_cells = distance_to_number_of_grid_cells(cube, radius)

        result_slices = iris.cube.CubeList()
        for cube_slice in cube.slices(
            [cube.coord(axis='y'), cube.coord(axis='x')]):
            (cube_slice, mask,
             nan_array) = (self.set_up_cubes_to_be_neighbourhooded(
                 cube_slice, mask_cube))
            neighbourhood_averaged_cube = (
                self._pad_and_calculate_neighbourhood(cube_slice, mask,
                                                      grid_cells))
            neighbourhood_averaged_cube = (self._remove_padding_and_mask(
                neighbourhood_averaged_cube, cube_slice, mask, grid_cells))
            neighbourhood_averaged_cube.data[nan_array.astype(bool)] = np.nan
            result_slices.append(neighbourhood_averaged_cube)

        neighbourhood_averaged_cube = result_slices.merge_cube()

        neighbourhood_averaged_cube.cell_methods = original_methods
        neighbourhood_averaged_cube.attributes = original_attributes

        neighbourhood_averaged_cube = check_cube_coordinates(
            cube, neighbourhood_averaged_cube)
        return neighbourhood_averaged_cube
Example #9
0
def remove_cube_halo(cube, halo_radius):
    """
    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 (iris.cube.Cube):
            Cube on extended grid
        halo_radius (float):
            Size of border to remove, in metres

    Returns:
        iris.cube.Cube:
            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
Example #10
0
    def process(self, cube: Cube, mask_cube: Optional[Cube] = None) -> Cube:
        """
        Call the methods required to apply a neighbourhood processing to a cube.

        Applies neighbourhood processing to each 2D x-y-slice of the input cube.

        If the input cube is masked the neighbourhood sum is calculated from
        the total of the unmasked data in the neighbourhood around each grid
        point. The neighbourhood mean is then calculated by dividing the
        neighbourhood sum at each grid point by the total number of valid grid
        points that contributed to that sum. If a mask_cube is provided then
        this is used to mask each x-y-slice prior to the neighburhood sum
        or mean being calculated.


        Args:
            cube:
                Cube containing the array to which the neighbourhood processing
                will be applied.
            mask_cube:
                Cube containing the array to be used as a mask. Zero values in
                this array are taken as points to be masked.

        Returns:
            Cube containing the smoothed field after the
            neighbourhood method has been applied.
        """
        super().process(cube)
        check_if_grid_is_equal_area(cube)

        # If the data is masked, the mask will be processed as well as the
        # original_data * mask array.
        check_radius_against_distance(cube, self.radius)

        grid_cells = distance_to_number_of_grid_cells(cube, self.radius)
        if self.neighbourhood_method == "circular":
            self.kernel = circular_kernel(grid_cells, self.weighted_mode)
        elif self.neighbourhood_method == "square":
            self.nb_size = 2 * grid_cells + 1

        try:
            mask_cube_data = mask_cube.data
        except AttributeError:
            mask_cube_data = None

        result_slices = CubeList()
        for cube_slice in cube.slices(
            [cube.coord(axis="y"), cube.coord(axis="x")]):
            cube_slice.data = self._calculate_neighbourhood(
                cube_slice.data, mask_cube_data)
            result_slices.append(cube_slice)
        neighbourhood_averaged_cube = result_slices.merge_cube()

        return neighbourhood_averaged_cube
Example #11
0
    def process(self, cube: Cube) -> Cube:
        """
        Method to apply a circular kernel to the data within the input cube in
        order to derive percentiles over the kernel.

        Args:
            cube:
                Cube containing array to apply processing to.

        Returns:
            Cube containing the percentile fields.
            Has percentile as an added dimension.
        """
        super().process(cube)
        if np.ma.is_masked(cube.data):
            msg = ("The use of masked input cubes is not yet implemented in"
                   " the GeneratePercentilesFromANeighbourhood plugin.")
            raise NotImplementedError(msg)

        # Check that the cube has an equal area grid.
        check_if_grid_is_equal_area(cube)
        # Take data array and identify X and Y axes indices
        grid_cell = distance_to_number_of_grid_cells(cube, self.radius)
        check_radius_against_distance(cube, self.radius)
        kernel = circular_kernel(grid_cell, weighted_mode=False)
        # Loop over each 2D slice to reduce memory demand and derive
        # percentiles on the kernel. Will return an extra dimension.
        pctcubelist = iris.cube.CubeList()
        for slice_2d in cube.slices(
            ["projection_y_coordinate", "projection_x_coordinate"]):
            pctcubelist.append(self.pad_and_unpad_cube(slice_2d, kernel))

        result = pctcubelist.merge_cube()
        exception_coordinates = find_dimension_coordinate_mismatch(
            cube, result, two_way_mismatch=False)
        result = check_cube_coordinates(
            cube, result, exception_coordinates=exception_coordinates)

        # Arrange cube, so that the coordinate order is:
        # realization, percentile, other coordinates.
        required_order = []
        if result.coords("realization", dim_coords=True):
            required_order.append(result.coord_dims("realization")[0])
        if result.coords("percentile", dim_coords=True):
            required_order.append(result.coord_dims("percentile")[0])
        other_coords = []
        for coord in result.dim_coords:
            if coord.name() not in ["realization", "percentile"]:
                other_coords.append(result.coord_dims(coord.name())[0])
        required_order.extend(other_coords)
        result.transpose(required_order)

        return result
Example #12
0
 def test_basic_distance_to_grid_cells(self):
     """Test the distance in metres to grid cell conversion along the
     x-axis (default)."""
     result = distance_to_number_of_grid_cells(self.cube, self.DISTANCE)
     self.assertEqual(result, 3)
Example #13
0
 def test_error_zero_grid_cell_range(self):
     """Test behaviour with a non-zero point with zero range."""
     distance = 5
     msg = "Distance of 5m gives zero cell extent"
     with self.assertRaisesRegex(ValueError, msg):
         distance_to_number_of_grid_cells(self.cube, distance)
Example #14
0
 def test_error_negative_distance(self):
     """Test behaviour with a non-zero point with negative range."""
     distance = -1.0 * self.DISTANCE
     msg = "Please specify a positive distance in metres"
     with self.assertRaisesRegex(ValueError, msg):
         distance_to_number_of_grid_cells(self.cube, distance)
Example #15
0
 def test_basic_distance_to_grid_cells_km_grid(self):
     """Test the distance-to-grid-cell conversion, grid in km."""
     self.cube.coord("projection_x_coordinate").convert_units("kilometres")
     self.cube.coord("projection_y_coordinate").convert_units("kilometres")
     result = distance_to_number_of_grid_cells(self.cube, self.DISTANCE)
     self.assertEqual(result, 3)
Example #16
0
 def test_basic_distance_to_grid_cells_float(self):
     """Test the distance in metres to grid cell conversion."""
     result = distance_to_number_of_grid_cells(self.cube,
                                               self.DISTANCE,
                                               return_int=False)
     self.assertEqual(result, 3.05)
Example #17
0
    def run(self,
            cube: Cube,
            radius: float,
            mask_cube: Optional[Cube] = None) -> Cube:
        """
        Method to apply a circular kernel to the data within the input cube in
        order to derive percentiles over the kernel.

        Args:
            cube:
                Cube containing array to apply processing to.
            radius:
                Radius in metres for use in specifying the number of
                grid cells used to create a circular neighbourhood.
            mask_cube:
                Cube containing the array to be used as a mask.

        Returns:
            Cube containing the percentile fields.
            Has percentile as an added dimension.
        """
        if mask_cube is not None:
            msg = ("The use of a mask cube with a circular kernel is not "
                   "yet implemented.")
            raise NotImplementedError(msg)

        # Check that the cube has an equal area grid.
        check_if_grid_is_equal_area(cube)
        # Take data array and identify X and Y axes indices
        grid_cell = distance_to_number_of_grid_cells(cube, radius)
        check_radius_against_distance(cube, radius)
        ranges_xy = np.array((grid_cell, grid_cell))
        kernel = circular_kernel(ranges_xy, grid_cell, weighted_mode=False)
        # Loop over each 2D slice to reduce memory demand and derive
        # percentiles on the kernel. Will return an extra dimension.
        pctcubelist = iris.cube.CubeList()
        for slice_2d in cube.slices(
            ["projection_y_coordinate", "projection_x_coordinate"]):
            pctcubelist.append(self.pad_and_unpad_cube(slice_2d, kernel))

        result = pctcubelist.merge_cube()
        exception_coordinates = find_dimension_coordinate_mismatch(
            cube, result, two_way_mismatch=False)
        result = check_cube_coordinates(
            cube, result, exception_coordinates=exception_coordinates)

        # Arrange cube, so that the coordinate order is:
        # realization, percentile, other coordinates.
        required_order = []
        if result.coords("realization"):
            if result.coords("realization", dimensions=[]):
                result = iris.util.new_axis(result, "realization")
            required_order.append(result.coord_dims("realization")[0])
        if result.coords("percentile"):
            required_order.append(result.coord_dims("percentile")[0])
        other_coords = []
        for coord in result.dim_coords:
            if coord.name() not in ["realization", "percentile"]:
                other_coords.append(result.coord_dims(coord.name())[0])
        required_order.extend(other_coords)
        result.transpose(required_order)

        return result