def _create_template_slice(self, cube_to_collapse): """ Create a template cube from a slice of the cube we are collapsing. The slice will be over blend_coord, y and x and will remove any other dimensions. This means that the resulting spatial weights won't vary in any other dimension other than the blend_coord. If the mask does vary in another dimension an error is raised. Args: cube_to_collapse (iris.cube.Cube): The cube that will be collapsed along the blend_coord using the spatial weights generated using this plugin. Must be masked where there is invalid data. Returns: iris.cube.Cube: A cube with dimensions blend_coord, y, x, on which to shape the output weights cube. Raises: ValueError: if the blend coordinate is associated with more than one dimension on the cube to collapse, or no dimension ValueError: if the mask on cube_to_collapse varies along a dimension other than the dimension associated with blend_coord. """ self.blend_coord = find_blend_dim_coord(cube_to_collapse, self.blend_coord) # Find original dim coords in input cube original_dim_coords = get_dim_coord_names(cube_to_collapse) # Slice over required coords x_coord = cube_to_collapse.coord(axis="x").name() y_coord = cube_to_collapse.coord(axis="y").name() coords_to_slice_over = [self.blend_coord, y_coord, x_coord] if original_dim_coords == coords_to_slice_over: return cube_to_collapse # Check mask does not vary over additional dimensions slices = cube_to_collapse.slices(coords_to_slice_over) first_slice = next(slices) if np.ma.is_masked(first_slice.data): first_mask = first_slice.data.mask for cube_slice in slices: if not np.all(cube_slice.data.mask == first_mask): message = ( "The mask on the input cube can only vary along the " "blend_coord, differences in the mask were found " "along another dimension") raise ValueError(message) # Remove non-spatial non-blend dimensions, returning a 3D template cube without # additional scalar coordinates (eg realization) for coord in original_dim_coords: if coord not in coords_to_slice_over: first_slice.remove_coord(coord) # Return slice template return first_slice
def process(self, cube, weights=None): """Calculate weighted blend across the chosen coord, for either probabilistic or percentile data. If there is a percentile coordinate on the cube, it will blend using the PercentileBlendingAggregator but the percentile coordinate must have at least two points. Args: cube (iris.cube.Cube): Cube to blend across the coord. weights (iris.cube.Cube): Cube of blending weights. This will have 1 or 3 dimensions, corresponding either to blend dimension on the input cube with or without and additional 2 spatial dimensions. If None, the input cube is blended with equal weights across the blending dimension. Returns: iris.cube.Cube: Containing the weighted blend across the chosen coordinate (typically forecast reference time or model). Raises: TypeError : If the first argument not a cube. CoordinateNotFoundError : If coordinate to be collapsed not found in cube. CoordinateNotFoundError : If coordinate to be collapsed not found in provided weights cube. ValueError : If coordinate to be collapsed is not a dimension. """ if not isinstance(cube, iris.cube.Cube): msg = ("The first argument must be an instance of iris.cube.Cube " "but is {}.".format(type(cube))) raise TypeError(msg) if not cube.coords(self.blend_coord): msg = "Coordinate to be collapsed not found in cube." raise CoordinateNotFoundError(msg) output_dims = get_dim_coord_names( next(cube.slices_over(self.blend_coord))) self.blend_coord = find_blend_dim_coord(cube, self.blend_coord) # Ensure input cube and weights cube are ordered equivalently along # blending coordinate. cube = sort_coord_in_cube(cube, self.blend_coord) if weights is not None: if not weights.coords(self.blend_coord): msg = "Coordinate to be collapsed not found in weights cube." raise CoordinateNotFoundError(msg) weights = sort_coord_in_cube(weights, self.blend_coord) # Check that the time coordinate is single valued if required. self.check_compatible_time_points(cube) # Do blending and update metadata if self.check_percentile_coord(cube): enforce_coordinate_ordering(cube, [self.blend_coord, "percentile"]) result = self.percentile_weighted_mean(cube, weights) else: enforce_coordinate_ordering(cube, [self.blend_coord]) result = self.weighted_mean(cube, weights) # Reorder resulting dimensions to match input enforce_coordinate_ordering(result, output_dims) return result
def test_find_blend_dim_coord_noop(cycle_cube, input_coord_name): """Test no impact and returns correctly if called on dimension""" result = find_blend_dim_coord(cycle_cube, input_coord_name) assert result == "forecast_reference_time"
def test_find_blend_dim_coord_error_no_dim(cycle_cube): """Test error if blend coordinate has no dimension""" cube = next(cycle_cube.slices_over("forecast_reference_time")) with pytest.raises(ValueError, match="no associated dimension"): find_blend_dim_coord(cube, "forecast_reference_time")