class Test__calculate_location_parameter_from_mean( SetupCoefficientsCubes, EnsembleCalibrationAssertions): """Test the __calculate_location_parameter_from_mean method.""" def setUp(self): """Set-up coefficients and plugin for testing.""" super().setUp() self.optimised_coeffs = (dict( zip( self.coeffs_from_mean.coord("coefficient_name").points, self.coeffs_from_mean.data))) self.plugin = Plugin() self.plugin.current_forecast = self.current_temperature_forecast_cube @ManageWarnings( ignored_messages=["Collapsing a non-contiguous coordinate."]) def test_basic(self): """Test that the expected values for the location parameter are calculated when using the ensemble mean. These expected values are compared to the results when using the ensemble realizations to ensure that the results are similar.""" location_parameter = ( self.plugin._calculate_location_parameter_from_mean( self.optimised_coeffs)) self.assertCalibratedVariablesAlmostEqual(location_parameter, self.expected_loc_param_mean) assert_array_almost_equal( location_parameter, self.expected_loc_param_statsmodels_realizations, decimal=0) assert_array_almost_equal( location_parameter, self.expected_loc_param_no_statsmodels_realizations, decimal=0)
def setUp(self): """Set-up coefficients and plugin for testing.""" super().setUp() self.optimised_coeffs = (dict( zip( self.coeffs_from_mean.coord("coefficient_name").points, self.coeffs_from_mean.data))) self.plugin = Plugin() self.plugin.current_forecast = self.current_temperature_forecast_cube
class Test__spatial_domain_match(SetupCoefficientsCubes): """ Test the _spatial_domain_match method.""" def setUp(self): super().setUp() self.plugin = Plugin() def test_matching(self): """Test case in which spatial domains match.""" self.plugin.current_forecast = self.current_temperature_forecast_cube self.plugin.coefficients_cube = self.coeffs_from_mean self.plugin._spatial_domain_match() def test_unmatching_x_axis(self): """Test case in which the x-dimensions of the domains do not match.""" self.current_temperature_forecast_cube.coord(axis='x').points = ( self.current_temperature_forecast_cube.coord(axis='x').points * 2.) self.plugin.current_forecast = self.current_temperature_forecast_cube self.plugin.coefficients_cube = self.coeffs_from_mean msg = "The domain along the x axis given by the current forecast" with self.assertRaisesRegex(ValueError, msg): self.plugin._spatial_domain_match() def test_unmatching_y_axis(self): """Test case in which the y-dimensions of the domains do not match.""" self.current_temperature_forecast_cube.coord(axis='y').points = ( self.current_temperature_forecast_cube.coord(axis='y').points * 2.) self.plugin.current_forecast = self.current_temperature_forecast_cube self.plugin.coefficients_cube = self.coeffs_from_mean msg = "The domain along the y axis given by the current forecast" with self.assertRaisesRegex(ValueError, msg): self.plugin._spatial_domain_match()
class Test__calculate_scale_parameter(SetupCoefficientsCubes, EnsembleCalibrationAssertions): """Test the _calculate_scale_parameter method.""" def setUp(self): """Set-up the plugin for testing.""" super().setUp() self.plugin = Plugin() self.plugin.current_forecast = self.current_temperature_forecast_cube @ManageWarnings( ignored_messages=["Collapsing a non-contiguous coordinate."]) def test_basic(self): """Test the scale parameter is calculated correctly.""" optimised_coeffs = dict( zip( self.coeffs_from_mean.coord("coefficient_name").points, self.coeffs_from_mean.data)) scale_parameter = ( self.plugin._calculate_scale_parameter(optimised_coeffs)) self.assertCalibratedVariablesAlmostEqual( scale_parameter, self.expected_scale_param_mean)
class Test__create_output_cubes(SetupCoefficientsCubes, EnsembleCalibrationAssertions): """Test the _create_output_cubes method.""" def setUp(self): """Set-up the plugin for testing.""" super().setUp() self.plugin = Plugin() self.plugin.current_forecast = self.current_temperature_forecast_cube @ManageWarnings( ignored_messages=["Collapsing a non-contiguous coordinate."]) def test_basic(self): """Test that the cubes created containing the location and scale parameter are formatted as expected.""" location_parameter_cube, scale_parameter_cube = ( self.plugin._create_output_cubes(self.expected_loc_param_mean, self.expected_scale_param_mean)) self.assertEqual(location_parameter_cube, self.expected_loc_param_mean_cube) self.assertEqual(scale_parameter_cube, self.expected_scale_param_mean_cube)
def process(cube: cli.inputcube, coefficients: cli.inputcube = None, land_sea_mask: cli.inputcube = None, *, distribution, realizations_count: int = None, randomise=False, random_seed: int = None, ignore_ecc_bounds=False, predictor='mean', shape_parameters: cli.comma_separated_list = None): """Applying coefficients for Ensemble Model Output Statistics. Load in arguments for applying coefficients for Ensemble Model Output Statistics (EMOS), otherwise known as Non-homogeneous Gaussian Regression (NGR). The coefficients are applied to the forecast that is supplied, so as to calibrate the forecast. The calibrated forecast is written to a cube. If no coefficients are provided the input forecast is returned unchanged. Args: cube (iris.cube.Cube): A Cube containing the forecast to be calibrated. The input format could be either realizations, probabilities or percentiles. coefficients (iris.cube.Cube): A cube containing the coefficients used for calibration or None. If none then then input is returned unchanged. land_sea_mask (iris.cube.Cube): A cube containing the land-sea mask on the same domain as the forecast that is to be calibrated. Land points are " "specified by ones and sea points are specified by zeros. " "If not None this argument will enable land-only calibration, in " "which sea points are returned without the application of " "calibration." distribution (str): The distribution for constructing realizations, percentiles or probabilities. This should typically match the distribution used for minimising the Continuous Ranked Probability Score when estimating the EMOS coefficients. The distributions available are those supported by :data:`scipy.stats`. realizations_count (int): Option to specify the number of ensemble realizations that will be created from probabilities or percentiles for input into EMOS. randomise (bool): Option to reorder the post-processed forecasts randomly. If not set, the ordering of the raw ensemble is used. This option is only valid when the input format is realizations. random_seed (int): Option to specify a value for the random seed for testing purposes, otherwise the default random seen behaviour is utilised. The random seed is used in the generation of the random numbers used for either the randomise option to order the input percentiles randomly, rather than use the ordering from the raw ensemble, or for splitting tied values within the raw ensemble, so that the values from the input percentiles can be ordered to match the raw ensemble. ignore_ecc_bounds (bool): If True, where the percentiles exceed the ECC bounds range, raises a warning rather than an exception. This occurs when the current forecasts is in the form of probabilities and is converted to percentiles, as part of converting the input probabilities into realizations. predictor (str): String to specify the form of the predictor used to calculate the location parameter when estimating the EMOS coefficients. Currently the ensemble mean ("mean") and the ensemble realizations ("realizations") are supported as the predictors. shape_parameters (float or str): The shape parameters required for defining the distribution specified by the distribution argument. The shape parameters should either be a number or 'inf' or '-inf' to represent infinity. Further details about appropriate shape parameters are available in scipy.stats. For the truncated normal distribution with a lower bound of zero, as available when estimating EMOS coefficients, the appropriate shape parameters are 0 and inf. Returns: iris.cube.Cube: The calibrated forecast cube. Raises: ValueError: If the current forecast is a coefficients cube. ValueError: If the coefficients cube does not have the right name of "emos_coefficients". ValueError: If the forecast type is 'percentiles' or 'probabilities' and the realizations_count argument is not provided. """ import warnings import numpy as np from iris.exceptions import CoordinateNotFoundError from improver.calibration.ensemble_calibration import ( ApplyCoefficientsFromEnsembleCalibration) from improver.ensemble_copula_coupling.ensemble_copula_coupling import ( EnsembleReordering, ConvertLocationAndScaleParametersToPercentiles, ConvertLocationAndScaleParametersToProbabilities, ConvertProbabilitiesToPercentiles, RebadgePercentilesAsRealizations, ResamplePercentiles) from improver.calibration.utilities import merge_land_and_sea from improver.metadata.probabilistic import find_percentile_coordinate current_forecast = cube if current_forecast.name() in ['emos_coefficients', 'land_binary_mask']: msg = "The current forecast cube has the name {}" raise ValueError(msg.format(current_forecast.name())) if coefficients is None: msg = ("There are no coefficients provided for calibration. The " "uncalibrated forecast will be returned.") warnings.warn(msg) return current_forecast if coefficients.name() != 'emos_coefficients': msg = ("The current coefficients cube does not have the " "name 'emos_coefficients'") raise ValueError(msg) if land_sea_mask and land_sea_mask.name() != 'land_binary_mask': msg = ("The land_sea_mask cube does not have the " "name 'land_binary_mask'") raise ValueError(msg) original_current_forecast = current_forecast.copy() try: find_percentile_coordinate(current_forecast) input_forecast_type = "percentiles" except CoordinateNotFoundError: input_forecast_type = "realizations" if current_forecast.name().startswith("probability_of"): input_forecast_type = "probabilities" conversion_plugin = ConvertProbabilitiesToPercentiles( ecc_bounds_warning=ignore_ecc_bounds) elif input_forecast_type == "percentiles": # Initialise plugin to resample percentiles so that the percentiles are # evenly spaced. conversion_plugin = ResamplePercentiles( ecc_bounds_warning=ignore_ecc_bounds) if input_forecast_type in ["percentiles", "probabilities"]: if not realizations_count: raise ValueError( "The current forecast has been provided as {0}. " "These {0} need to be converted to realizations " "for ensemble calibration. The realizations_count " "argument is used to define the number of realizations " "to construct from the input {0}, so if the " "current forecast is provided as {0} then " "realizations_count must be defined.".format( input_forecast_type)) current_forecast = conversion_plugin.process( current_forecast, no_of_percentiles=realizations_count) current_forecast = ( RebadgePercentilesAsRealizations().process(current_forecast)) # Apply coefficients as part of Ensemble Model Output Statistics (EMOS). ac = ApplyCoefficientsFromEnsembleCalibration(predictor=predictor) location_parameter, scale_parameter = ac.process( current_forecast, coefficients, landsea_mask=land_sea_mask) if shape_parameters: shape_parameters = [np.float32(x) for x in shape_parameters] # Convert the output forecast type (i.e. realizations, percentiles, # probabilities) to match the input forecast type. if input_forecast_type == "probabilities": result = ConvertLocationAndScaleParametersToProbabilities( distribution=distribution, shape_parameters=shape_parameters).process( location_parameter, scale_parameter, original_current_forecast) elif input_forecast_type == "percentiles": perc_coord = find_percentile_coordinate(original_current_forecast) result = ConvertLocationAndScaleParametersToPercentiles( distribution=distribution, shape_parameters=shape_parameters).process( location_parameter, scale_parameter, original_current_forecast, percentiles=perc_coord.points) elif input_forecast_type == "realizations": # Ensemble Copula Coupling to generate realizations # from the location and scale parameter. no_of_percentiles = len(current_forecast.coord('realization').points) percentiles = ConvertLocationAndScaleParametersToPercentiles( distribution=distribution, shape_parameters=shape_parameters).process( location_parameter, scale_parameter, original_current_forecast, no_of_percentiles=no_of_percentiles) result = EnsembleReordering().process(percentiles, current_forecast, random_ordering=randomise, random_seed=random_seed) if land_sea_mask: # Fill in masked sea points with uncalibrated data. merge_land_and_sea(result, original_current_forecast) return result
def setUp(self): """Set-up the plugin for testing.""" super().setUp() self.plugin = Plugin()
class Test_process(SetupCoefficientsCubes, EnsembleCalibrationAssertions): """Test the process plugin.""" def setUp(self): """Set-up the plugin for testing.""" super().setUp() self.plugin = Plugin() @ManageWarnings( ignored_messages=["Collapsing a non-contiguous coordinate."]) def test_variable_setting(self): """Test that the cubes passed into the plugin are allocated to plugin variables appropriately.""" _, _ = self.plugin.process(self.current_temperature_forecast_cube, self.coeffs_from_mean) self.assertEqual(self.current_temperature_forecast_cube, self.plugin.current_forecast) self.assertEqual(self.coeffs_from_mean, self.plugin.coefficients_cube) @ManageWarnings( ignored_messages=["Collapsing a non-contiguous coordinate."]) def test_end_to_end(self): """An example end-to-end calculation. This repeats the test elements above but all grouped together.""" calibrated_forecast_predictor, calibrated_forecast_var = ( self.plugin.process(self.current_temperature_forecast_cube, self.coeffs_from_mean)) self.assertCalibratedVariablesAlmostEqual( calibrated_forecast_predictor.data, self.expected_loc_param_mean) self.assertCalibratedVariablesAlmostEqual( calibrated_forecast_var.data, self.expected_scale_param_mean) self.assertEqual(calibrated_forecast_predictor.dtype, np.float32) @ManageWarnings( ignored_messages=["Collapsing a non-contiguous coordinate."]) def test_end_to_end_with_mask(self): """An example end-to-end calculation, but making sure that the areas that are masked within the landsea mask, are masked at the end.""" # Construct a mask and encapsulate as a cube. mask = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) mask_cube = self.current_temperature_forecast_cube[0].copy(data=mask) # Convention for IMPROVER is that land points are ones and sea points # are zeros in land-sea masks. In this case we want to mask sea points. expected_mask = np.array([[False, True, True], [True, False, True], [True, True, False]]) calibrated_forecast_predictor, calibrated_forecast_var = ( self.plugin.process(self.current_temperature_forecast_cube, self.coeffs_from_mean, landsea_mask=mask_cube)) self.assertCalibratedVariablesAlmostEqual( calibrated_forecast_predictor.data.data, self.expected_loc_param_mean) self.assertArrayEqual(calibrated_forecast_predictor.data.mask, expected_mask) self.assertCalibratedVariablesAlmostEqual( calibrated_forecast_var.data.data, self.expected_scale_param_mean) self.assertArrayEqual(calibrated_forecast_var.data.mask, expected_mask)
def setUp(self): """Set-up the plugin for testing.""" super().setUp() self.plugin = Plugin() self.plugin.current_forecast = self.current_temperature_forecast_cube
def setUp(self): super().setUp() self.plugin = Plugin()