def calculate_sleet_probability(prob_of_snow, prob_of_rain): """ This calculates the probability of sleet using the calculation: prob(sleet) = 1 - (prob(snow) + prob(rain)) Args: prob_of_snow (iris.cube.Cube): Cube of the probability of snow. This can be a fraction (0 <= x <= 1) or categorical (0 or 1) prob_of_rain (iris.cube.Cube): Cube of the probability of rain. This can be a fraction (0 <= x <= 1) or categorical (0 or 1) Returns: iris.cube.Cube: Cube of the probability of sleet. This will be fractional or categorical, matching the highest precision of the inputs. Raises: ValueError: If the cube contains negative values for the the probability of sleet. """ sleet_prob = 1 - (prob_of_snow.data + prob_of_rain.data) if np.any(sleet_prob < 0): msg = "Negative values of sleet probability have been calculated." raise ValueError(msg) # Copy all of the attributes from the prob_of_snow cube mandatory_attributes = generate_mandatory_attributes([prob_of_rain, prob_of_snow]) probability_of_sleet = create_new_diagnostic_cube( "probability_of_sleet", "1", prob_of_snow, mandatory_attributes, data=sleet_prob ) return probability_of_sleet
def _calculate_snow_fraction(self): """ Calculates the snow fraction data and interpolates to fill in the missing points. Returns: iris.cube.Cube: Snow fraction cube. """ with np.errstate(divide="ignore", invalid="ignore"): snow_fraction = self.snow.data / (self.rain.data + self.snow.data) snow_fraction_cube = create_new_diagnostic_cube( "snow_fraction", "1", template_cube=self.rain, mandatory_attributes=generate_mandatory_attributes( iris.cube.CubeList([self.rain, self.snow]), model_id_attr=self.model_id_attr, ), data=snow_fraction, ) spatial_dims = [snow_fraction_cube.coord(axis=n).name() for n in ["y", "x"]] snow_fraction_interpolated = iris.cube.CubeList() for snow_fraction_slice in snow_fraction_cube.slices(spatial_dims): snow_fraction_interpolated.append( snow_fraction_slice.copy( interpolate_missing_data(snow_fraction_slice.data, method="nearest") ) ) return snow_fraction_interpolated.merge_cube()
def create_wet_bulb_temperature_cube(self, temperature, relative_humidity, pressure): """ Creates a cube of wet bulb temperature values Args: temperature (iris.cube.Cube): Cube of air temperatures. relative_humidity (iris.cube.Cube): Cube of relative humidities. pressure (iris.cube.Cube): Cube of air pressures. Returns: iris.cube.Cube: Cube of wet bulb temperature (K). """ temperature.convert_units('K') relative_humidity.convert_units(1) pressure.convert_units('Pa') wbt_data = self._calculate_wet_bulb_temperature( pressure.data, relative_humidity.data, temperature.data) attributes = generate_mandatory_attributes( [temperature, relative_humidity, pressure]) wbt = create_new_diagnostic_cube('wet_bulb_temperature', 'K', temperature, attributes, data=wbt_data) return wbt
def create_symbol_cube(cubes): """ Create an empty weather symbol cube Args: cubes (list or iris.cube.CubeList): List of input cubes used to generate weather symbols Returns: iris.cube.Cube: A cube with suitable metadata to describe the weather symbols that will fill it """ threshold_coord = find_threshold_coordinate(cubes[0]) template_cube = next(cubes[0].slices_over([threshold_coord])).copy() # remove coordinates and bounds that do not apply to weather symbols template_cube.remove_coord(threshold_coord) for coord in template_cube.coords(): if coord.name() in ['forecast_period', 'time']: coord.bounds = None attributes = generate_mandatory_attributes(cubes) symbols = create_new_diagnostic_cube( "weather_code", "1", template_cube, attributes, optional_attributes=weather_code_attributes(), dtype=np.int32) return symbols
def _create_template_cube(self, cube): """ Create a template cube to store the timezone masks. This cube has only one scalar coordinate which is time, denoting when it is valid; this is only relevant if using daylight savings. The attribute includes_daylight_savings is set to indicate this. Args: cube (iris.cube.Cube): A cube with the desired grid from which coordinates are taken for inclusion in the template. Returns: iris.cube.Cube: A template cube in which each timezone mask can be stored. """ time_point = np.array(self.time.timestamp(), dtype=np.int64) time_coord = iris.coords.DimCoord( time_point, "time", units=Unit("seconds since 1970-01-01 00:00:00", calendar="gregorian"), ) for crd in cube.coords(dim_coords=False): cube.remove_coord(crd) cube.add_aux_coord(time_coord) attributes = generate_mandatory_attributes([cube]) attributes["includes_daylight_savings"] = str(self.include_dst) return create_new_diagnostic_cube( "timezone_mask", 1, cube, attributes, dtype=np.int8 )
def test_dtype(self): """Test dummy data of a different type can be set""" result = create_new_diagnostic_cube(self.name, self.units, self.template_cube, dtype=np.int32) self.assertEqual(result.data.dtype, np.int32)
def _prepare_error_probability_cube(self, forecast_cube): """Initialise a cube with the same dimensions as the input forecast_cube, with an additional threshold dimension added as the leading dimension. Args: forecast_cube: Cube containing the forecast to be calibrated. Returns: An empty probability cube. """ # Create a template for error CDF, with threshold the leading dimension. forecast_error_variable = f"forecast_error_of_{forecast_cube.name()}" error_probability_cube = create_new_diagnostic_cube( name=f"probability_of_{forecast_error_variable}_above_threshold", units="1", template_cube=forecast_cube, mandatory_attributes=generate_mandatory_attributes([forecast_cube ]), ) error_threshold_coord = DimCoord( self.error_thresholds, long_name=forecast_error_variable, var_name="threshold", units=forecast_cube.units, attributes={"spp__relative_to_threshold": "above"}, ) error_probability_cube = add_coordinate_to_cube( error_probability_cube, new_coord=error_threshold_coord, ) return error_probability_cube
def calculate_sleet_probability(prob_of_snow, prob_of_rain): """ This calculates the probability of sleet using the calculation: prob(sleet) = 1 - (prob(snow) + prob(rain)) Args: prob_of_snow (iris.cube.Cube): Cube of the probability of snow. prob_of_rain (iris.cube.Cube): Cube of the probability of rain. Returns: iris.cube.Cube: Cube of the probability of sleet. Raises: ValueError: If the cube contains negative values for the the probability of sleet. """ ones = np.ones((prob_of_snow.shape), dtype="float32") sleet_prob = ones - (prob_of_snow.data + prob_of_rain.data) if np.any(sleet_prob < 0.0): msg = "Negative values of sleet probability have been calculated." raise ValueError(msg) # Copy all of the attributes from the prob_of_snow cube mandatory_attributes = generate_mandatory_attributes( [prob_of_rain, prob_of_snow]) probability_of_sleet = create_new_diagnostic_cube("probability_of_sleet", "1", prob_of_snow, mandatory_attributes, data=sleet_prob) return probability_of_sleet
def _create_output_cube(self, cube, advected_data, timestep): """ Create a cube and appropriate metadata to contain the advected forecast Args: cube (iris.cube.Cube): Source cube (before advection) advected_data (numpy.ndarray): Advected data timestep (datetime.timedelta): Time difference between the advected output and the source Returns: iris.cube.Cube """ attributes = generate_mandatory_attributes([cube]) if "institution" in cube.attributes.keys(): attributes["source"] = "{} Nowcast".format(attributes["institution"]) else: attributes["source"] = "Nowcast" advected_cube = create_new_diagnostic_cube( cube.name(), cube.units, cube, attributes, data=advected_data ) amend_attributes(advected_cube, self.attributes_dict) set_history_attribute(advected_cube, "Nowcast") self._update_time(cube.coord("time").copy(), advected_cube, timestep) self._add_forecast_reference_time(cube.coord("time").copy(), advected_cube) self._add_forecast_period(advected_cube, timestep) return advected_cube
def process(self, snow_fraction, phase): """ Make significant-phase-mask cube for the specified phase. Args: snow_fraction (iris.cube.Cube): The input snow-fraction data to derive the phase mask from. phase (str): One of "rain", "sleet" or "snow". This is the phase mask that will be returned. Returns: iris.cube.Cube: The requested phase mask containing 1 where that phase is dominant and 0 elsewhere. Dimensions will be identical to snow-fraction. """ self._validate_snow_fraction(snow_fraction) try: data = self.phase_operator[phase](snow_fraction.data).astype( np.int8) except KeyError: raise KeyError( f"Requested phase mask '{phase}' not in {list(self.phase_operator.keys())}" ) phase_mask = create_new_diagnostic_cube( f"{phase}_mask", "1", snow_fraction, generate_mandatory_attributes([snow_fraction], model_id_attr=self.model_id_attr), data=data, ) return phase_mask
def create_wet_bulb_temperature_cube(self, temperature: Cube, relative_humidity: Cube, pressure: Cube) -> Cube: """ Creates a cube of wet bulb temperature values Args: temperature: Cube of air temperatures. relative_humidity: Cube of relative humidities. pressure: Cube of air pressures. Returns: Cube of wet bulb temperature (K). """ temperature.convert_units("K") relative_humidity.convert_units(1) pressure.convert_units("Pa") wbt_data = self._calculate_wet_bulb_temperature( pressure.data, relative_humidity.data, temperature.data) attributes = generate_mandatory_attributes( [temperature, relative_humidity, pressure]) wbt = create_new_diagnostic_cube("wet_bulb_temperature", "K", temperature, attributes, data=wbt_data) return wbt
def create_symbol_cube(cubes): """ Create an empty weather symbol cube Args: cubes (list or iris.cube.CubeList): List of input cubes used to generate weather symbols Returns: iris.cube.Cube: A cube with suitable metadata to describe the weather symbols that will fill it and data initiated with the value -1 to allow any unset points to be readily identified. """ threshold_coord = find_threshold_coordinate(cubes[0]) template_cube = next(cubes[0].slices_over([threshold_coord])).copy() # remove coordinates and bounds that do not apply to weather symbols template_cube.remove_coord(threshold_coord) for coord in template_cube.coords(): if coord.name() in ["forecast_period", "time"]: coord.bounds = None attributes = generate_mandatory_attributes(cubes) symbols = create_new_diagnostic_cube( "weather_code", "1", template_cube, attributes, optional_attributes=weather_code_attributes(), data=np.ma.masked_all_like(template_cube.data).astype(np.int32), ) return symbols
def create_symbol_cube(self, cubes: Union[List[Cube], CubeList]) -> Cube: """ Create an empty weather symbol cube Args: cubes: List of input cubes used to generate weather symbols Returns: A cube with suitable metadata to describe the weather symbols that will fill it and data initiated with the value -1 to allow any unset points to be readily identified. """ threshold_coord = find_threshold_coordinate(self.template_cube) template_cube = next(self.template_cube.slices_over([threshold_coord ])).copy() # remove coordinates and bounds that do not apply to weather symbols template_cube.remove_coord(threshold_coord) mandatory_attributes = generate_mandatory_attributes(cubes) optional_attributes = weather_code_attributes() if self.model_id_attr: optional_attributes.update( update_model_id_attr_attribute(cubes, self.model_id_attr)) symbols = create_new_diagnostic_cube( "weather_code", "1", template_cube, mandatory_attributes, optional_attributes=optional_attributes, data=np.ma.masked_all_like(template_cube.data).astype(np.int32), ) return symbols
def test_non_standard_name(self): """Test cube can be created with a non-CF-standard name""" result = create_new_diagnostic_cube("RainRate Composite", self.units, self.template_cube, self.mandatory_attributes) self.assertEqual(result.long_name, "RainRate Composite") self.assertIsNone(result.standard_name)
def process(self, cubes): """ Calculate the convective ratio from the convective and dynamic components as: convective_ratio = convective / (convective + dynamic) If convective + dynamic is zero, then the resulting point is masked. Args: cubes (List[iris.cube.Cube, iris.cube.Cube]): Both the convective and dynamic components as iris.cube.Cube in a list with names 'lwe_convective_precipitation_rate' and 'lwe_stratiform_precipitation_rate' Returns: iris.cube.Cube: Cube containing the convective ratio. """ self._split_input(cubes) attributes = generate_mandatory_attributes([self.convective]) output_cube = create_new_diagnostic_cube( "convective_ratio", "1", self.convective, attributes, data=self._convective_ratio(), ) return output_cube
def _create_shower_condition_cube(self, data: ndarray, cube: Cube) -> Cube: """ Returns a shower condition cube, with coordinates and mandatory attributes based upon the provided cube. The threshold coordinate is modified to describe shower conditions, such that the probabilities describe the likelihood of conditions being showery. The arbitrary threshold value is 1. Args: data: The shower condition probabilities to populate the new cube. cube: The cube to use as a template, and from which to extract attributes for use in the new diagnostic cube. Returns: A probability of shower conditions cube. """ template = make_shower_condition_cube(cube) attributes = generate_mandatory_attributes( [cube], model_id_attr=self.model_id_attr) result = create_new_diagnostic_cube( template.name(), "1", template, mandatory_attributes=attributes, data=data, ) return result
def _create_output_cube(gradient, diff, cube, axis): """ Create the output gradient cube. Args: gradient (numpy.ndarray): Gradient values used in the data array of the resulting cube. diff (iris.cube.Cube): Cube containing differences along the x or y axis cube (iris.cube.Cube): Cube with correct output dimensions axis (str): Short-hand reference for the x or y coordinate, as allowed by iris.util.guess_coord_axis. Returns: iris.cube.Cube: A cube of the gradients in the coordinate direction specified. """ grad_cube = create_new_diagnostic_cube( "gradient_of_" + cube.name(), cube.units / diff.coord(axis=axis).units, diff, MANDATORY_ATTRIBUTE_DEFAULTS, data=gradient, ) return grad_cube
def process(self, cubes: List[Cube], model_id_attr: Optional[str] = None) -> Cube: """ Calculate the convective ratio from the convective and dynamic components as: convective_ratio = convective / (convective + dynamic) If convective + dynamic is zero, then the resulting point is masked. Args: cubes: Both the convective and dynamic components as iris.cube.Cube in a list with names 'lwe_convective_precipitation_rate' and 'lwe_stratiform_precipitation_rate' model_id_attr: Name of the attribute used to identify the source model for blending. This is inherited from the input temperature cube. Returns: Cube containing the convective ratio. """ self._split_input(cubes) attributes = generate_mandatory_attributes([self.convective], model_id_attr=model_id_attr) output_cube = create_new_diagnostic_cube( "convective_ratio", "1", self.convective, attributes, data=self._convective_ratio(), ) return output_cube
def calculate_feels_like_temperature(temperature, wind_speed, relative_humidity, pressure, model_id_attr=None): """ Calculates the feels like temperature using a combination of the wind chill index and Steadman's apparent temperature equation. Args: temperature (iris.cube.Cube): Cube of air temperatures wind_speed (iris.cube.Cube): Cube of 10m wind speeds relative_humidity (iris.cube.Cube): Cube of relative humidities pressure (iris.cube.Cube): Cube of air pressure model_id_attr (str): Name of the attribute used to identify the source model for blending. Returns: iris.cube.Cube: Cube of feels like temperatures in the same units as the input temperature cube. """ t_cube = temperature.copy() t_cube.convert_units('degC') t_celsius = t_cube.data w_cube = wind_speed.copy() w_cube.convert_units('m s-1') p_cube = pressure.copy() p_cube.convert_units('Pa') rh_cube = relative_humidity.copy() rh_cube.convert_units('1') apparent_temperature = _calculate_apparent_temperature( t_celsius, w_cube.data, rh_cube.data, p_cube.data) w_cube.convert_units('km h-1') wind_chill = _calculate_wind_chill(t_celsius, w_cube.data) feels_like_temperature = _feels_like_temperature(t_celsius, apparent_temperature, wind_chill) attributes = generate_mandatory_attributes( [temperature, wind_speed, relative_humidity, pressure], model_id_attr=model_id_attr) feels_like_temperature_cube = create_new_diagnostic_cube( "feels_like_temperature", "degC", temperature, attributes, data=feels_like_temperature) feels_like_temperature_cube.convert_units(temperature.units) return feels_like_temperature_cube
def test_data(self): """Test data can be set on the output cube""" data = np.arange(3 * 5 * 5).reshape((3, 5, 5)).astype(np.float32) result = create_new_diagnostic_cube(self.name, self.units, self.template_cube, data=data) self.assertTrue(np.allclose(result.data, data))
def test_attributes(self): """Test attributes can be set on the output cube""" attributes = {"source": "IMPROVER"} result = create_new_diagnostic_cube(self.name, self.units, self.template_cube, attributes=attributes) self.assertDictEqual(result.attributes, attributes)
def _apply_error_to_forecast(self, forecast_cube: Cube, error_percentiles_cube: Cube) -> Cube: """Apply the error distributions (as error percentiles) to the forecast cube. The result is a series (sub-ensemble) of values for each forecast realization. Note: Within the RainForests approach we work with an additive error correction as opposed to a multiplicative correction used in ECPoint. The advantage of using an additive error is that we are also able to calibrate zero-values in the input forecast. Warning: After applying the error distributions to the forecast cube, values outside the expected bounds of the forecast parameter can arise. These values occur when when the input forecast value is between error thresholds and there exists a lower bound on the observable value (eg. 0 in the case of rainfall). In this situation, error thresholds below the residual value (min(obs) - fcst) must have a probability of exceedance of 1, whereas as error thresholds above this value can take on any value between [0, 1]. In the subsequent step where error percentile values are extracted, the linear interpolation in mapping from probabilities to percentiles can give percentile values that lie below the residual value; when these are applied to the forecast value, they result in forecast values outside the expected bounds of the forecast parameter in the resultant sub-ensemble. To address this, we remap all values outside of the expected bounds to nearest bound (eg. negative values are mapped to 0 in the case of rainfall). Args: forecast_cube: Cube containing the forecast to be calibrated. error_percentiles_cube: Cube containing percentile values for the error distributions. Returns: Cube containing the forecast sub-ensembles. """ # Apply the error_percentiles to the forecast_cube (additive correction) forecast_subensembles_data = (forecast_cube.data[:, np.newaxis] + error_percentiles_cube.data) # RAINFALL SPECIFIC IMPLEMENTATION: # As described above, we need to address value outside of expected bounds. # In the case of rainfall, we map all negative values to 0. forecast_subensembles_data = np.maximum(0.0, forecast_subensembles_data) # Return cube containing forecast subensembles return create_new_diagnostic_cube( name=forecast_cube.name(), units=forecast_cube.units, template_cube=error_percentiles_cube, mandatory_attributes=generate_mandatory_attributes([forecast_cube ]), optional_attributes=forecast_cube.attributes, data=forecast_subensembles_data, )
def _create_output_cube(self, template, data, points, bounds): """ Populates a template cube with data from the integration Args: template (iris.cube.Cube): Copy of upper or lower bounds cube, based on direction of integration data (list or numpy.ndarray): Integrated data points (list or numpy.ndarray): Points values for the integrated coordinate. These will not match the template cube if any slices were skipped in the integration, and therefore are used to slice the template cube to match the data array. bounds (list or numpy.ndarray): Bounds values for the integrated coordinate Returns: iris.cube.Cube """ # extract required slices from template cube template = template.extract( iris.Constraint(coord_values={ self.coord_name_to_integrate: lambda x: x in points })) # re-promote integrated coord to dimension coord if need be aux_coord_names = [coord.name() for coord in template.aux_coords] if self.coord_name_to_integrate in aux_coord_names: template = iris.util.new_axis(template, self.coord_name_to_integrate) # order dimensions on the template cube so that the integrated # coordinate is first (as this is the leading dimension on the # data array) enforce_coordinate_ordering(template, self.coord_name_to_integrate) # generate appropriate metadata for new cube attributes = generate_mandatory_attributes([template]) coord_dtype = template.coord(self.coord_name_to_integrate).dtype name, units = self._generate_output_name_and_units() # create new cube from template integrated_cube = create_new_diagnostic_cube(name, units, template, attributes, data=np.array(data)) integrated_cube.coord(self.coord_name_to_integrate).bounds = np.array( bounds).astype(coord_dtype) # re-order cube to match dimensions of input cube ordered_dimensions = get_dim_coord_names(self.input_cube) enforce_coordinate_ordering(integrated_cube, ordered_dimensions) return integrated_cube
def process(self, cubes: CubeList, model_id_attr: str = None) -> Cube: """ From the supplied CAPE and precipitation-rate cubes, calculate a probability of lightning cube. Args: cubes: Cubes of CAPE and Precipitation rate. model_id_attr: The name of the dataset attribute to be used to identify the source model when blending data from different models. Returns: Cube of lightning data Raises: ValueError: If one of the cubes is not found or doesn't match the other """ cape, precip = self._get_inputs(cubes) cape_true = LatitudeDependentThreshold( lambda lat: latitude_to_threshold( lat, midlatitude=350.0, tropics=500.0), threshold_units="J kg-1", comparison_operator=">", )(cape) precip_true = LatitudeDependentThreshold( lambda lat: latitude_to_threshold( lat, midlatitude=1.0, tropics=4.0), threshold_units="mm h-1", comparison_operator=">", )(precip) data = cape_true.data * precip_true.data cube = create_new_diagnostic_cube( name= "probability_of_number_of_lightning_flashes_per_unit_area_above_threshold", units="1", template_cube=precip, data=data.astype(FLOAT_DTYPE), mandatory_attributes=generate_mandatory_attributes( cubes, model_id_attr=model_id_attr), ) coord = DimCoord( np.array([0], dtype=FLOAT_DTYPE), units="m-2", long_name="number_of_lightning_flashes_per_unit_area", var_name="threshold", attributes={"spp__relative_to_threshold": "greater_than"}, ) cube.add_aux_coord(coord) return cube
def create_output_cube(self, cube: Cube, local_time: datetime) -> Cube: """ Constructs the output cube Args: cube: Cube of data to extract timezone-offsets from. Must contain a time coord spanning all the timezones. local_time: The "local" time of the output cube as %Y%m%dT%H%MZ. This will form a scalar "time_in_local_timezone" coord on the output cube, while the "time" coord will be auxillary to the spatial coords and will show the UTC time that matches the local_time at each point. """ template_cube = cube.slices_over("time").next().copy() template_cube.remove_coord("time") template_cube.remove_coord("forecast_period") output_cube = create_new_diagnostic_cube( template_cube.name(), template_cube.units, template_cube, generate_mandatory_attributes([template_cube]), optional_attributes=template_cube.attributes, data=self.output_data, ) # Copy cell-methods from template_cube [output_cube.add_cell_method(cm) for cm in template_cube.cell_methods] # Create a local time coordinate to help with plotting data. local_time_coord_standards = TIME_COORDS["time_in_local_timezone"] local_time_units = cf_units.Unit( local_time_coord_standards.units, calendar=local_time_coord_standards.calendar, ) timezone_points = np.array( np.round(local_time_units.date2num(local_time)), dtype=local_time_coord_standards.dtype, ) output_cube.add_aux_coord( AuxCoord( timezone_points, long_name="time_in_local_timezone", units=local_time_units, ) ) output_cube.add_aux_coord( AuxCoord( self.time_points, bounds=self.time_bounds, standard_name="time", units=self.time_units, ), [n + output_cube.ndim for n in [-2, -1]], ) return output_cube
def test_attributes(self): """Test optional attributes can be set on the output cube, and override the values in mandatory_attributes""" attributes = {"source": "Mars", "mosg__model_configuration": "uk_det"} expected_attributes = self.mandatory_attributes expected_attributes.update(attributes) result = create_new_diagnostic_cube(self.name, self.units, self.template_cube, self.mandatory_attributes, optional_attributes=attributes) self.assertDictEqual(result.attributes, expected_attributes)
def process(self, cube): """ Calculate the convective ratio either for the underlying field e.g. precipitation rate, or using the differences between adjacent grid squares. If the difference between adjacent grid squares is used, firstly the absolute differences are calculated, and then the difference cubes are thresholded using a high and low threshold. The thresholded difference cubes are then summed in order to put these cubes back onto the grid of the original cube. The convective ratio is then calculated by applying neighbourhood processing to the resulting cubes by dividing the high threshold cube by the low threshold cube. Args: cube (iris.cube.Cube): The cube from which the convective ratio will be calculated. Returns: iris.cube.Cube: Cube containing the convective ratio defined as the ratio between a cube with a high threshold applied and a cube with a low threshold applied. """ cubelist = iris.cube.CubeList([]) threshold_list = [self.lower_threshold, self.higher_threshold] if self.use_adjacent_grid_square_differences: for threshold in threshold_list: diff_cubelist = self.absolute_differences_between_adjacent_grid_squares( cube) thresholded_cubes = self.iterate_over_threshold( diff_cubelist, threshold) cubelist.append( self.sum_differences_between_adjacent_grid_squares( cube, thresholded_cubes)) else: for threshold in threshold_list: cubelist.extend(self.iterate_over_threshold([cube], threshold)) convective_ratios = self._calculate_convective_ratio( cubelist, threshold_list) attributes = generate_mandatory_attributes([cube]) output_cube = create_new_diagnostic_cube("convective_ratio", "1", cube, attributes, data=convective_ratios) return output_cube
def test_basic(self): """Test result is a cube that inherits coordinates only""" result = create_new_diagnostic_cube(self.name, self.units, self.template_cube) self.assertIsInstance(result, iris.cube.Cube) self.assertEqual(result.standard_name, "lwe_precipitation_rate") self.assertEqual(result.units, "mm h-1") self.assertSequenceEqual(result.coords(dim_coords=True), self.template_cube.coords(dim_coords=True)) self.assertSequenceEqual(result.coords(dim_coords=False), self.template_cube.coords(dim_coords=False)) self.assertFalse(np.allclose(result.data, self.template_cube.data)) self.assertFalse(result.attributes) self.assertFalse(result.cell_methods) self.assertEqual(result.data.dtype, np.float32)
def process(self, cubes: Union[CubeList, List[Cube]]) -> Cube: """ Derives the probability of a precipitation phase at the surface. If the snow-sleet falling-level is supplied, this is the probability of snow at (or below) the surface. If the sleet-rain falling-level is supplied, this is the probability of rain at (or above) the surface. If the hail-rain falling-level is supplied, this is the probability of rain from hail at (or above) the surface. Args: cubes: Contains cubes of the altitude of the phase-change level (this can be snow->sleet, hail->rain or sleet->rain) and the altitude of the orography. Returns: Cube containing the probability of a specific precipitation phase reaching the surface orography. If the falling_level_cube was snow->sleet, then this will be the probability of snow at the surface. If the falling_level_cube was sleet->rain, then this will be the probability of rain from sleet at the surface. If the falling_level_cube was hail->rain, then this will be the probability of rain from hail at the surface. The probabilities are categorical (1 or 0) allowing precipitation to be divided uniquely between snow, sleet and rain phases. """ self._extract_input_cubes(cubes) processed_falling_level = iris.util.squeeze( self.get_discriminating_percentile(self.falling_level_cube)) result_data = np.where( self.comparator(self.orography_cube.data, processed_falling_level.data), 1, 0, ).astype(np.int8) mandatory_attributes = generate_mandatory_attributes( [self.falling_level_cube]) cube = create_new_diagnostic_cube( f"probability_of_{self.param}_at_surface", Unit("1"), self.falling_level_cube, mandatory_attributes, data=result_data, ) return cube
def _make_updraught_cube(self, data: np.ndarray) -> Cube: """Puts the data array into a CF-compliant cube""" attributes = {} if self.model_id_attr: attributes[self.model_id_attr] = self.precip.attributes[ self.model_id_attr] cube = create_new_diagnostic_cube( "maximum_vertical_updraught", "m s-1", self.precip, mandatory_attributes=generate_mandatory_attributes( [self.precip, self.cape]), optional_attributes=attributes, data=data, ) return cube