def test_no_matches_exception(self): """Test for when no matches in validity time are found between the historic forecasts and the truths. In this case, an exception is raised.""" partial_truth = self.truth[2] msg = "The filtering has found no matches in validity time " with self.assertRaisesRegex(ValueError, msg): filter_non_matching_cubes(self.partial_historic_forecasts, partial_truth)
def test_fewer_truths(self): """Test for when there are fewer truths than historic forecasts, for example, if there is a missing analysis.""" hf_result, truth_result = filter_non_matching_cubes( self.historic_temperature_forecast_cube, self.partial_truth) self.assertEqual(hf_result, self.partial_historic_forecasts) self.assertEqual(truth_result, self.partial_truth)
def test_fewer_historic_forecasts(self): """Test for when there are fewer historic forecasts than truths, for example, if there is a missing forecast cycle.""" hf_result, truth_result = filter_non_matching_cubes( self.partial_historic_forecasts, self.temperature_truth_cube) self.assertEqual(hf_result, self.partial_historic_forecasts) self.assertEqual(truth_result, self.partial_truth)
def test_all_matching(self): """Test for when the historic forecast and truth cubes all match.""" hf_result, truth_result = filter_non_matching_cubes( self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEqual(hf_result, self.historic_temperature_forecast_cube) self.assertEqual(truth_result, self.temperature_truth_cube)
def test_mismatching(self): """Test for when there is both a missing historic forecasts and a missing truth at different validity times. This results in the expected historic forecasts and the expected truths containing cubes at three matching validity times.""" partial_truth = self.truth[1:].merge_cube() expected_historical_forecasts = iris.cube.CubeList([ self.historic_forecasts[index] for index in (1, 3, 4) ]).merge_cube() expected_truth = iris.cube.CubeList( [self.truth[index] for index in (1, 3, 4)]).merge_cube() hf_result, truth_result = filter_non_matching_cubes( self.partial_historic_forecasts, partial_truth) self.assertEqual(hf_result, expected_historical_forecasts) self.assertEqual(truth_result, expected_truth)
def test_bounded_variables(self): """Test for when the historic forecast and truth cubes all match inclusive of both the points and bounds on the time coordinate.""" # Define bounds so that the lower bound is one hour preceding the point # whilst the upper bound is equal to the point. points = self.historic_temperature_forecast_cube.coord("time").points bounds = [] for point in points: bounds.append([point - 1 * 60 * 60, point]) self.historic_temperature_forecast_cube.coord("time").bounds = bounds self.temperature_truth_cube.coord("time").bounds = bounds hf_result, truth_result = filter_non_matching_cubes( self.historic_temperature_forecast_cube, self.temperature_truth_cube) self.assertEqual(hf_result, self.historic_temperature_forecast_cube) self.assertEqual(truth_result, self.temperature_truth_cube)
def process(self, historic_forecasts, truths): """ Slice data over threshold and time coordinates to construct reliability tables. These are summed over time to give a single table for each threshold, constructed from all the provided historic forecasts and truths. .. See the documentation for an example of the resulting reliability table cube. .. include:: extended_documentation/calibration/ reliability_calibration/reliability_calibration_examples.rst Note that the forecast and truth data used is probabilistic, i.e. has already been thresholded relative to the thresholds of interest, using the equality operator required. As such this plugin is agnostic as to whether the data is thresholded below or above a given diagnostic threshold. Args: historic_forecasts (iris.cube.Cube): A cube containing the historical forecasts used in calibration. These are expected to all have a consistent cycle hour, that is the hour in the forecast reference time. truths (iris.cube.Cube): A cube containing the thresholded gridded truths used in calibration. Returns: iris.cube.CubeList: A cubelist of reliability table cubes, one for each threshold in the historic forecast cubes. Raises: ValueError: If the forecast and truth cubes have differing threshold coordinates. """ historic_forecasts, truths = filter_non_matching_cubes( historic_forecasts, truths) threshold_coord = find_threshold_coordinate(historic_forecasts) truth_threshold_coord = find_threshold_coordinate(truths) if not threshold_coord == truth_threshold_coord: msg = "Threshold coordinates differ between forecasts and truths." raise ValueError(msg) time_coord = historic_forecasts.coord("time") check_forecast_consistency(historic_forecasts) reliability_cube = self._create_reliability_table_cube( historic_forecasts, threshold_coord) reliability_tables = iris.cube.CubeList() threshold_slices = zip( historic_forecasts.slices_over(threshold_coord), truths.slices_over(threshold_coord), ) for forecast_slice, truth_slice in threshold_slices: threshold_reliability = [] time_slices = zip( forecast_slice.slices_over(time_coord), truth_slice.slices_over(time_coord), ) for forecast, truth in time_slices: reliability_table = self._populate_reliability_bins( forecast.data, truth.data) threshold_reliability.append(reliability_table) # Stack and sum reliability tables for all times table_values = np.stack(threshold_reliability) table_values = np.sum(table_values, axis=0, dtype=np.float32) reliability_entry = reliability_cube.copy(data=table_values) reliability_entry.replace_coord( forecast_slice.coord(threshold_coord)) reliability_tables.append(reliability_entry) return MergeCubes()(reliability_tables)
def process(self, historic_forecast, truth, landsea_mask=None): """ Using Nonhomogeneous Gaussian Regression/Ensemble Model Output Statistics, estimate the required coefficients from historical forecasts. The main contents of this method is: 1. Check that the predictor is valid. 2. Filter the historic forecasts and truth to ensure that these inputs match in validity time. 3. Apply unit conversion to ensure that the historic forecasts and truth have the desired units for calibration. 4. Calculate the variance of the historic forecasts. If the chosen predictor is the mean, also calculate the mean of the historic forecasts. 5. If a land-sea mask is provided then mask out sea points in the truth and predictor from the historic forecasts. 6. Calculate initial guess at coefficient values by performing a linear regression, if requested, otherwise default values are used. 7. Perform minimisation. Args: historic_forecast (iris.cube.Cube): The cube containing the historical forecasts used for calibration. truth (iris.cube.Cube): The cube containing the truth used for calibration. landsea_mask (iris.cube.Cube): The optional cube containing a land-sea mask. If provided, only land points are used to calculate the coefficients. Within the land-sea mask cube land points should be specified as ones, and sea points as zeros. Returns: iris.cube.Cube: Cube containing the coefficients estimated using EMOS. The cube contains a coefficient_index dimension coordinate and a coefficient_name auxiliary coordinate. Raises: ValueError: If either the historic_forecast or truth cubes were not passed in. ValueError: If the units of the historic and truth cubes do not match. """ if not (historic_forecast and truth): raise ValueError("historic_forecast and truth cubes must be " "provided.") # Ensure predictor is valid. check_predictor(self.predictor) historic_forecast, truth = ( filter_non_matching_cubes(historic_forecast, truth)) # Make sure inputs have the same units. if self.desired_units: historic_forecast.convert_units(self.desired_units) truth.convert_units(self.desired_units) if historic_forecast.units != truth.units: msg = ("The historic forecast units of {} do not match " "the truth units {}. These units must match, so that " "the coefficients can be estimated.") raise ValueError(msg) if self.predictor.lower() == "mean": no_of_realizations = None forecast_predictor = collapsed( historic_forecast, "realization", iris.analysis.MEAN) elif self.predictor.lower() == "realizations": no_of_realizations = len( historic_forecast.coord("realization").points) forecast_predictor = historic_forecast forecast_var = collapsed( historic_forecast, "realization", iris.analysis.VARIANCE) # If a landsea_mask is provided mask out the sea points if landsea_mask: self.mask_cube(forecast_predictor, landsea_mask) self.mask_cube(forecast_var, landsea_mask) self.mask_cube(truth, landsea_mask) # Computing initial guess for EMOS coefficients initial_guess = self.compute_initial_guess( truth, forecast_predictor, self.predictor, self.ESTIMATE_COEFFICIENTS_FROM_LINEAR_MODEL_FLAG, no_of_realizations=no_of_realizations) # Calculate coefficients if there are no nans in the initial guess. if np.any(np.isnan(initial_guess)): optimised_coeffs = initial_guess else: optimised_coeffs = ( self.minimiser( initial_guess, forecast_predictor, truth, forecast_var, self.predictor, self.distribution.lower())) coefficients_cube = ( self.create_coefficients_cube(optimised_coeffs, historic_forecast)) return coefficients_cube