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
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)
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
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
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)
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
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)
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
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
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
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
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)
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)
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)
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)
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)
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