def test_coefficient_values_for_fake_distribution(self): """Ensure the appropriate error is raised if the minimisation function requested is not available.""" distribution = "fake" plugin = Plugin(distribution, self.current_cycle) msg = "Distribution requested" with self.assertRaisesRegex(KeyError, msg): plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube)
def test_non_matching_units(self): """Test that an exception is raised if the historic forecasts and truth have non matching units.""" self.historic_temperature_forecast_cube.convert_units("Fahrenheit") plugin = Plugin(self.distribution, self.current_cycle) msg = "The historic forecast units" with self.assertRaisesRegex(ValueError, msg): plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube)
def test_basic(self): """Ensure that the optimised_coefficients are returned as a cube, with the expected number of coefficients.""" plugin = Plugin(self.distribution, self.current_cycle) result = plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertIsInstance(result, iris.cube.Cube) self.assertEqual(len(result.data), len(self.coeff_names))
def test_historic_forecast_unit_conversion(self): """Ensure the expected optimised coefficients are generated, even if the input historic forecast cube has different units.""" self.historic_temperature_forecast_cube.convert_units("Fahrenheit") desired_units = "Kelvin" plugin = Plugin(self.distribution, self.current_cycle, desired_units=desired_units) result = plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEMOSCoefficientsAlmostEqual( result.data, self.expected_mean_predictor_gaussian)
def test_coefficient_values_for_gaussian_distribution(self): """Ensure that the values for the optimised_coefficients match the expected values, and the coefficient names also match expected values for a Gaussian distribution. In this case, a linear least-squares regression is used to construct the initial guess.""" plugin = Plugin(self.distribution, self.current_cycle) result = plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEMOSCoefficientsAlmostEqual( result.data, self.expected_mean_predictor_gaussian) self.assertArrayEqual( result.coord("coefficient_name").points, self.coeff_names)
def test_coefficients_gaussian_distribution_default_initial_guess(self): """Ensure that the values for the optimised_coefficients match the expected values, and the coefficient names also match expected values for a Gaussian distribution, where the default values for the initial guess are used, rather than using a linear least-squares regression to construct an initial guess.""" expected = [-0.0002, 0.7977, 0.0004, 0.9973] plugin = Plugin(self.distribution, self.current_cycle) plugin.ESTIMATE_COEFFICIENTS_FROM_LINEAR_MODEL_FLAG = False result = plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEMOSCoefficientsAlmostEqual(result.data, expected) self.assertArrayEqual( result.coord("coefficient_name").points, self.coeff_names)
def test_coefficients_gaussian_realizations_statsmodels(self): """Ensure that the values for the optimised_coefficients match the expected values, and the coefficient names also match expected values for a Gaussian distribution where the realizations are used as the predictor of the mean.""" predictor_of_mean_flag = "realizations" plugin = Plugin(self.distribution, self.current_cycle, predictor_of_mean_flag=predictor_of_mean_flag) result = plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEMOSCoefficientsAlmostEqual( result.data, self.expected_realizations_gaussian_statsmodels) self.assertArrayEqual( result.coord("coefficient_name").points, self.coeff_names_realizations)
def test_coefficients_truncated_gaussian_default_initial_guess(self): """Ensure that the values for the optimised_coefficients match the expected values, and the coefficient names also match expected values for a truncated Gaussian distribution, where the default values for the initial guess are used, rather than using a linear least-squares regression to construct an initial guess..""" distribution = "truncated_gaussian" plugin = Plugin(distribution, self.current_cycle) plugin.ESTIMATE_COEFFICIENTS_FROM_LINEAR_MODEL_FLAG = False result = plugin.process(self.historic_wind_speed_forecast_cube, self.wind_speed_truth_cube) self.assertEMOSCoefficientsAlmostEqual( result.data, self.expected_mean_predictor_truncated_gaussian) self.assertArrayEqual( result.coord("coefficient_name").points, self.coeff_names)
def test_coefficient_values_for_gaussian_distribution_max_iterations(self): """Ensure that the values for the optimised_coefficients match the expected values, and the coefficient names also match expected values for a Gaussian distribution, when the max_iterations argument is specified.""" max_iterations = 800 plugin = Plugin(self.distribution, self.current_cycle, max_iterations=max_iterations) result = plugin.process(self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEMOSCoefficientsAlmostEqual( result.data, self.expected_mean_predictor_gaussian) self.assertArrayEqual( result.coord("coefficient_name").points, self.coeff_names)
def test_coefficient_values_for_gaussian_distribution_mismatching_inputs( self): """Test that the values for the optimised coefficients match the expected values, and the coefficient names also match expected values for a Gaussian distribution for when the historic forecasts and truths input having some mismatches in validity time. """ partial_historic_forecasts = ( self.historic_forecasts[:2] + self.historic_forecasts[3:]).merge_cube() partial_truth = self.truth[1:].merge_cube() plugin = Plugin(self.distribution, self.current_cycle) result = plugin.process(partial_historic_forecasts, partial_truth) self.assertEMOSCoefficientsAlmostEqual( result.data, self.expected_mean_predictor_gaussian) self.assertArrayEqual( result.coord("coefficient_name").points, self.coeff_names)
def main(argv=None): """Load in arguments for estimating coefficients for Ensemble Model Output Statistics (EMOS), otherwise known as Non-homogeneous Gaussian Regression (NGR). 2 sources of input data must be provided: historical forecasts and historical truth data (to use in calibration). The estimated coefficients are written to a netCDF file. """ parser = ArgParser( description='Estimate coefficients for Ensemble Model Output ' 'Statistics (EMOS), otherwise known as Non-homogeneous ' 'Gaussian Regression (NGR)') parser.add_argument('distribution', metavar='DISTRIBUTION', choices=['gaussian', 'truncated gaussian'], help='The distribution that will be used for ' 'calibration. This will be dependent upon the ' 'input phenomenon. This has to be supported by ' 'the minimisation functions in ' 'ContinuousRankedProbabilityScoreMinimisers.') parser.add_argument('cycletime', metavar='CYCLETIME', type=str, help='This denotes the cycle at which forecasts ' 'will be calibrated using the calculated ' 'EMOS coefficients. The validity time in the ' 'output coefficients cube will be calculated ' 'relative to this cycletime. ' 'This cycletime is in the format ' 'YYYYMMDDTHHMMZ.') # Filepaths for historic and truth data. parser.add_argument('historic_filepath', metavar='HISTORIC_FILEPATH', help='A path to an input NetCDF file containing the ' 'historic forecast(s) used for calibration.') parser.add_argument('truth_filepath', metavar='TRUTH_FILEPATH', help='A path to an input NetCDF file containing the ' 'historic truth analyses used for calibration.') parser.add_argument('output_filepath', metavar='OUTPUT_FILEPATH', help='The output path for the processed NetCDF') # Optional arguments. parser.add_argument('--units', metavar='UNITS', help='The units that calibration should be undertaken ' 'in. The historical forecast and truth will be ' 'converted as required.') parser.add_argument('--predictor_of_mean', metavar='PREDICTOR_OF_MEAN', choices=['mean', 'realizations'], default='mean', help='String to specify the predictor used to ' 'calibrate the forecast mean. Currently the ' 'ensemble mean ("mean") and the ensemble ' 'realizations ("realizations") are supported as ' 'options. Default: "mean".') parser.add_argument('--max_iterations', metavar='MAX_ITERATIONS', type=np.int32, default=1000, help='The maximum number of iterations allowed ' 'until the minimisation has converged to a ' 'stable solution. If the maximum number ' 'of iterations is reached, but the ' 'minimisation has not yet converged to a ' 'stable solution, then the available solution ' 'is used anyway, and a warning is raised.' 'This may be modified for testing purposes ' 'but otherwise kept fixed. If the ' 'predictor_of_mean is "realizations", ' 'then the number of iterations may require ' 'increasing, as there will be more coefficients ' 'to solve for.') args = parser.parse_args(args=argv) historic_forecast = load_cube(args.historic_filepath) truth = load_cube(args.truth_filepath) # Estimate coefficients using Ensemble Model Output Statistics (EMOS). estcoeffs = EstimateCoefficientsForEnsembleCalibration( args.distribution, args.cycletime, desired_units=args.units, predictor_of_mean_flag=args.predictor_of_mean, max_iterations=args.max_iterations) coefficients = ( estcoeffs.process(historic_forecast, truth)) save_netcdf(coefficients, args.output_filepath)