def process(self, cube: Cube) -> Cube: """Expected value calculation and metadata updates. Args: cube: Probabilistic data with a realization, threshold or percentile representation. Returns: Expected value of probability distribution. Same shape as input cube but with realization/threshold/percentile coordinate removed. """ if is_probability(cube): # TODO: replace this with direct calculation of the integral over # probability thresholds. The current approach works and has the # correct interface, but some accuracy will be lost in the conversion # and memory usage is very high. # # 19 percentiles corresponds to 5, 10, 15...95%. cube = ConvertProbabilitiesToPercentiles().process( cube, no_of_percentiles=19) if is_percentile(cube): cube = RebadgePercentilesAsRealizations().process(cube) mean_cube = collapse_realizations(cube) mean_cube.add_cell_method(CellMethod("mean", coords="realization")) return mean_cube
def process(cube: cli.inputcube): """Sets night values to zero for UV index. Args: cube (iris.cube.Cube): Cube that will have night values set to zero. This should contain either diagnostic values or probabilities of UV index above threshold. Returns: iris.cube.Cube: Input cube with all night values set to zero. Raises: ValueError: If input cube is suspicious, within reason. Note that this is a general check: the CLI expects a cube of UV index or probability of UV index above thresold, and will raise an error if given a probability below threshold, but will not recognise a completely inappropriate cube (eg temperature in Kelvin). Therefore this CLI should be used with care. """ import numpy as np from improver.metadata.probabilistic import is_probability from improver.utilities.solar import DayNightMask if is_probability(cube): if "above_threshold" not in cube.name(): raise ValueError(f"{cube.name()} unsuitable for night masking") mask = DayNightMask()(cube).data # Broadcast mask to shape of input cube to account for additional dimensions. mask = np.broadcast_to(mask, cube.shape) # setting night values to zero. cube.data = np.where(mask == DayNightMask().night, 0, cube.data) return cube
def test_false(self): """Test cube that does not contain thresholded probabilities evaluates as false""" cube = set_up_variable_cube( self.data, name="probability_of_rain_at_surface", units="1" ) result = is_probability(cube) self.assertFalse(result)
def process(self, cube: Cube) -> Cube: """Expected value calculation and metadata updates. Args: cube: Probabilistic data with a realization, threshold or percentile representation. Returns: Expected value of probability distribution. Same shape as input cube but with realization/threshold/percentile coordinate removed. """ if is_probability(cube): ev_cube = self.integrate_over_thresholds(cube) elif is_percentile(cube): ev_cube = collapse_realizations( RebadgePercentilesAsRealizations().process(cube) ) else: ev_cube = collapse_realizations(cube) ev_cube.add_cell_method(CellMethod("mean", coords="realization")) return ev_cube
def test_scalar_threshold_coord(self): """Test a probability cube with a single threshold evaluates as true""" cube = iris.util.squeeze(self.prob_cube[0]) result = is_probability(cube) self.assertTrue(result)
def test_true(self): """Test a probability cube evaluates as true""" result = is_probability(self.prob_cube) self.assertTrue(result)
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
def process( cube: cli.inputcube, *, coordinates: cli.comma_separated_list = None, percentiles: cli.comma_separated_list = None, ignore_ecc_bounds=False, ): r"""Collapses cube coordinates and calculate percentiled data. Calculate percentiled data over a given coordinate by collapsing that coordinate. Typically used to convert realization data into percentiled data, but may calculate over any dimension coordinate. Alternatively calling this with a dataset containing probabilities will convert those to percentiles using the ensemble coupla coupling plugin. If no particular percentiles are given at which to calculate values and no 'number of percentiles' to calculate are specified, the following defaults will be used. '[0, 5, 10, 20, 25, 30, 40, 50, 60, 70, 75, 80, 90, 95, 100]' Args: cube (iris.cube.Cube): A Cube for processing. coordinates (str or list): Coordinate or coordinates over which to collapse data and calculate percentiles; e.g. 'realization' or 'latitude,longitude'. This argument must be provided when collapsing a coordinate or coordinates to create percentiles, but is redundant when converting probabilities to percentiles and may be omitted. This coordinate(s) will be removed and replaced by a percentile coordinate. percentiles (list): Optional definition of percentiles at which to calculate data. ignore_ecc_bounds (bool): If True, where calculated percentiles are outside the ECC bounds range, raises a warning rather than an exception. Returns: iris.cube.Cube: The processed Cube. Raises: ValueError: If the cube name does not contain 'probability_of\_' and coordinates isn't used. Warns: Warning: If 'probability_of\_' is in the cube name and coordinates is used. """ import warnings import numpy as np from improver.ensemble_copula_coupling.ensemble_copula_coupling import ( ConvertProbabilitiesToPercentiles, ) from improver.metadata.probabilistic import is_probability from improver.percentile import PercentileConverter if percentiles is not None: percentiles = [float(p) for p in percentiles] if is_probability(cube): result = ConvertProbabilitiesToPercentiles( ecc_bounds_warning=ignore_ecc_bounds)(cube, percentiles=percentiles) if coordinates: warnings.warn("Converting probabilities to percentiles. The " "provided COORDINATES_TO_COLLAPSE variable will " "not be used.") else: if not coordinates: raise ValueError("To collapse a coordinate to calculate " "percentiles, a coordinate or list of " "coordinates must be provided.") # Switch back to use the slow scipy method if the cube contains masked # data which the numpy method cannot handle. fast_percentile_method = True if np.ma.is_masked(cube.data): # Check for masked points: fast_percentile_method = False elif np.ma.isMaskedArray(cube.data): # Check if we have a masked array with an empty mask. If so, # replace it with a non-masked array: cube.data = cube.data.data result = PercentileConverter( coordinates, percentiles=percentiles, fast_percentile_method=fast_percentile_method, )(cube) return result
def process(self, cube: Cube) -> Cube: """ Produces the vicinity processed data. The input data is sliced to yield y-x slices to which the maximum_within_vicinity method is applied. The different vicinity radii (if multiple) are looped over and a coordinate recording the radius used is added to each resulting cube. A single cube is returned with the leading coordinates of the input cube preserved. If a single vicinity radius is provided, a new scalar radius_of_vicinity coordinate will be found on the returned cube. If multiple radii are provided, this coordinate will be a dimension coordinate following any probabilistic / realization coordinates. Args: cube: Thresholded cube. Returns: Cube containing the occurrences within a vicinity for each radius, calculated for each yx slice, which have been merged to yield a single cube. Raises: ValueError: Cube and land mask have differing spatial coordinates. """ if self.land_mask_cube and not spatial_coords_match( [cube, self.land_mask_cube]): raise ValueError( "Supplied cube do not have the same spatial coordinates and land mask" ) if not self.native_grid_point_radius: grid_point_radii = [ distance_to_number_of_grid_cells(cube, radius) for radius in self.radii ] else: grid_point_radii = self.radii radii_cubes = CubeList() # List of non-spatial dimensions to restore as leading on the output. leading_dimensions = [ crd.name() for crd in cube.coords(dim_coords=True) if not crd.coord_system ] for radius, grid_point_radius in zip(self.radii, grid_point_radii): max_cubes = CubeList([]) for cube_slice in cube.slices( [cube.coord(axis="y"), cube.coord(axis="x")]): max_cubes.append( self.maximum_within_vicinity(cube_slice, grid_point_radius)) result_cube = max_cubes.merge_cube() # Put dimensions back if they were there before. result_cube = check_cube_coordinates(cube, result_cube) # Add a coordinate recording the vicinity radius applied to the data. self._add_vicinity_coordinate(result_cube, radius) radii_cubes.append(result_cube) # Merge cubes produced for each vicinity radius. result_cube = radii_cubes.merge_cube() # Enforce order of leading dimensions on the output to match the input. enforce_coordinate_ordering(result_cube, leading_dimensions) if is_probability(result_cube): result_cube.rename(in_vicinity_name_format(result_cube.name())) else: result_cube.rename(f"{result_cube.name()}_in_vicinity") return result_cube
def process( cube: cli.inputcube, raw_cube: cli.inputcube = None, *, realizations_count: int = None, random_seed: int = None, ignore_ecc_bounds=False, ): """Converts an incoming cube into one containing realizations. Args: cube (iris.cube.Cube): A cube to be processed. raw_cube (iris.cube.Cube): Cube of raw (not post processed) weather data. If this argument is given ensemble realizations will be created from percentiles by reshuffling them in correspondence to the rank order of the raw ensemble. Otherwise, the percentiles are rebadged as realizations. realizations_count (int): The number of ensemble realizations in the output. random_seed (int): Option to specify a value for the random seed when reordering percentiles. This value is for testing purposes only, to ensure reproduceable outputs. It should not be used in real time operations as it may introduce a bias into the reordered forecasts. ignore_ecc_bounds (bool): If True where percentiles (calculated as an intermediate output before realization) exceed the ECC bounds range, raises a warning rather than an exception. Returns: iris.cube.Cube: The processed cube. """ from improver.ensemble_copula_coupling.ensemble_copula_coupling import ( ConvertProbabilitiesToPercentiles, EnsembleReordering, RebadgePercentilesAsRealizations, ResamplePercentiles, ) from improver.metadata.probabilistic import is_probability if cube.coords("realization"): return cube if not cube.coords("percentile") and not is_probability(cube): raise ValueError("Unable to convert to realizations:\n" + str(cube)) if realizations_count is None: try: realizations_count = len(raw_cube.coord("realization").points) except AttributeError: # raised if raw_cube is None, hence has no attribute "coord" msg = "Either realizations_count or raw_cube must be provided" raise ValueError(msg) if cube.coords("percentile"): percentiles = ResamplePercentiles( ecc_bounds_warning=ignore_ecc_bounds)( cube, no_of_percentiles=realizations_count) else: percentiles = ConvertProbabilitiesToPercentiles( ecc_bounds_warning=ignore_ecc_bounds)( cube, no_of_percentiles=realizations_count) if raw_cube: result = EnsembleReordering()(percentiles, raw_cube, random_seed=random_seed) else: result = RebadgePercentilesAsRealizations()(percentiles) return result