def test_catch_warnings_percentage_change(self, warning_list=None):
        """
        Test that two warnings are generated if the minimisation
        does not result in a convergence. The first warning reports a that
        the minimisation did not result in convergence, whilst the second
        warning reports that the percentage change in the final iteration was
        greater than the tolerated value.
        The ensemble mean is the predictor.
        """
        initial_guess = np.array([0, 1, 5000, 1], dtype=np.float64)

        predictor = "mean"
        distribution = "truncnorm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=5)

        plugin.process(
            initial_guess,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg_min = "Minimisation did not result in convergence after"
        warning_msg_iter = "The final iteration resulted in a percentage "
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(
            any(warning_msg_min in str(item) for item in warning_list))
        self.assertTrue(
            any(warning_msg_iter in str(item) for item in warning_list))
    def test_mean_predictor_point_by_point_sites(self):
        """
        Test that the expected coefficients are generated when the ensemble
        mean is the predictor for a normal distribution and coefficients are
        calculated independently at each site location. The coefficients are
        calculated by minimising the CRPS.
        """
        forecast_spot_cube = self.historic_forecast_spot_cube.collapsed(
            "realization", iris.analysis.MEAN)
        forecast_var_spot_cube = forecast_spot_cube.copy()
        forecast_var_spot_cube.data = forecast_var_spot_cube.data / 10.0

        predictor = "mean"
        distribution = "norm"

        initial_guess = np.broadcast_to(
            self.initial_guess_for_mean,
            (
                len(self.truth.coord(axis="y").points) *
                len(self.truth.coord(axis="x").points),
                len(self.initial_guess_for_mean),
            ),
        )

        plugin = Plugin(tolerance=self.tolerance, point_by_point=True)
        result = plugin.process(
            initial_guess,
            forecast_spot_cube,
            self.truth_spot_cube,
            forecast_var_spot_cube,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_mean_coefficients_point_by_point_sites)
    def test_realizations_predictor_point_by_point(self):
        """
        Test that the expected coefficients are generated when the ensemble
        realizations are the predictor for a normal distribution and
        coefficients are calculated independently at each grid point. The
        coefficients are calculated by minimising the CRPS.
        """
        predictor = "realizations"
        distribution = "norm"

        initial_guess = np.broadcast_to(
            self.initial_guess_for_realization,
            (
                len(self.truth.coord(axis="y").points) *
                len(self.truth.coord(axis="x").points),
                len(self.initial_guess_for_realization),
            ),
        )

        # Use a larger value for the tolerance to terminate sooner to avoid
        # minimising in computational noise.
        plugin = Plugin(tolerance=0.01, point_by_point=True)
        result = plugin.process(
            initial_guess,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertArrayAlmostEqual(
            result,
            self.expected_realizations_coefficients_point_by_point,
            decimal=2)
コード例 #4
0
    def test_point_by_point_with_nans(self):
        """
        Test that the expected coefficients are generated when the ensemble
        mean is the predictor for a normal distribution and coefficients are
        calculated independently at each grid point with one grid point
        having NaN values for the truth.
        """
        predictor = "mean"
        distribution = "norm"

        self.truth.data[:, 0, 0] = np.nan
        plugin = Plugin(predictor,
                        tolerance=self.tolerance,
                        point_by_point=True)
        result = plugin.process(
            self.initial_guess_spot_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            distribution,
        )
        self.expected_mean_coefficients_point_by_point[:, 0,
                                                       0] = self.initial_guess_for_mean
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_mean_coefficients_point_by_point)
    def test_mean_predictor_point_by_point(self):
        """
        Test that the expected coefficients are generated when the ensemble
        mean is the predictor for a normal distribution and coefficients are
        calculated independently at each grid point. The coefficients are
        calculated by minimising the CRPS.
        """
        predictor = "mean"
        distribution = "norm"

        initial_guess = np.broadcast_to(
            self.initial_guess_for_mean,
            (
                len(self.truth.coord(axis="y").points) *
                len(self.truth.coord(axis="x").points),
                len(self.initial_guess_for_mean),
            ),
        )

        plugin = Plugin(tolerance=self.tolerance, point_by_point=True)
        result = plugin.process(
            initial_guess,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_mean_coefficients_point_by_point)
    def test_realizations_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble realizations are the
        predictor assuming a truncated normal distribution and the value
        specified for the max_iterations is overridden. The coefficients are
        calculated by minimising the CRPS and using a set default value for
        the initial guess.
        """
        predictor = "realizations"
        max_iterations = 1000
        distribution = "truncnorm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)
    def test_catch_warnings(self, warning_list=None):
        """
        Test that a warning is generated if the minimisation
        does not result in a convergence. The ensemble mean is the predictor.
        """
        predictor = "mean"
        distribution = "truncated_gaussian"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=10)
        plugin.process(self.initial_guess_for_mean,
                       self.forecast_predictor_mean, self.truth,
                       self.forecast_variance, predictor, distribution)
        warning_msg = "Minimisation did not result in convergence after"
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(any(warning_msg in str(item) for item in warning_list))
コード例 #8
0
    def test_mean_predictor_point_by_point_sites_additional_predictor(self):
        """
        Test that the plugin returns a numpy array with the expected
        coefficients. Coefficients are calculated independently at each site.
        The ensemble mean and altitude are the predictors.
        """
        predictor = "mean"
        distribution = "norm"

        plugin = Plugin(predictor, tolerance=self.tolerance, point_by_point=True)
        result = plugin.process(
            self.ig_spot_mean_additional_predictor,
            self.fp_additional_predictor_spot,
            self.truth_spot_cube,
            self.forecast_variance_spot,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_point_by_point_sites_additional_predictor
        )
コード例 #9
0
    def test_mean_predictor_point_by_point_sites(self):
        """
        Test that the expected coefficients are generated when the ensemble
        mean is the predictor for a normal distribution and coefficients are
        calculated independently at each site location. The coefficients are
        calculated by minimising the CRPS.
        """
        predictor = "mean"
        distribution = "norm"

        plugin = Plugin(predictor, tolerance=self.tolerance, point_by_point=True)
        result = plugin.process(
            self.initial_guess_spot_mean,
            self.forecast_predictor_spot,
            self.truth_spot_cube,
            self.forecast_variance_spot,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_mean_coefficients_point_by_point_sites
        )
    def test_mean_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble mean is the predictor
        assuming a normal distribution and the value specified for the
        max_iterations is overridden. The coefficients are calculated by
        minimising the CRPS.
        """
        predictor = "mean"
        max_iterations = 400
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)
class Test_process_truncated_normal_distribution(SetupTruncatedNormalInputs,
                                                 EnsembleCalibrationAssertions
                                                 ):
    """
    Test minimising the CRPS for a truncated normal distribution.
    Either the ensemble mean or the individual ensemble realizations are used
    as the predictors.
    """
    def setUp(self):
        """Set up expected output."""
        super().setUp()
        self.tolerance = 1e-4
        self.plugin = Plugin(tolerance=self.tolerance)
        self.expected_mean_coefficients = [0.3958, 0.9854, -0.0, 0.621]
        self.expected_realizations_coefficients = [
            0.1898,
            -0.1558,
            0.4452,
            0.8877,
            -0.1331,
            -0.0002,
        ]

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "The final iteration resulted in",
            "invalid value encountered in",
            "divide by zero encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_basic_mean_predictor(self):
        """
        Test that the plugin returns a numpy array. The ensemble mean
        is the predictor.
        """
        predictor = "mean"
        distribution = "truncnorm"

        result = self.plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertIsInstance(result, np.ndarray)
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "invalid value encountered in",
            "divide by zero encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_basic_realizations_predictor(self):
        """
        Test that the plugin returns a numpy array with the expected
        coefficients. The ensemble realizations are the predictor.
        """
        predictor = "realizations"
        distribution = "truncnorm"

        result = self.plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertIsInstance(result, np.ndarray)
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_mean_predictor_keyerror(self):
        """
        Test that an exception is raised when the distribution requested is
        not an available option when the predictor is the
        ensemble mean.
        """
        predictor = "mean"
        distribution = "foo"

        msg = "Distribution requested"
        with self.assertRaisesRegex(KeyError, msg):
            self.plugin.process(
                self.initial_guess_for_mean,
                self.forecast_predictor_mean,
                self.truth,
                self.forecast_variance,
                predictor,
                distribution,
            )

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "The final iteration resulted in",
            "invalid value encountered in",
            "divide by zero encountered in",
        ],
        warning_types=[
            UserWarning,
            UserWarning,
            UserWarning,
            RuntimeWarning,
            RuntimeWarning,
        ],
    )
    def test_mean_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble mean is the predictor
        assuming a truncated normal distribution and the value specified
        for the max_iterations is overridden. The coefficients are
        calculated by minimising the CRPS.
        """
        predictor = "mean"
        max_iterations = 400
        distribution = "truncnorm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "invalid value encountered in",
            "divide by zero encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_realizations_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble realizations are the
        predictor assuming a truncated normal distribution and the value
        specified for the max_iterations is overridden. The coefficients are
        calculated by minimising the CRPS.
        """
        predictor = "realizations"
        max_iterations = 1000
        distribution = "truncnorm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)

    @ManageWarnings(
        record=True,
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_catch_warnings(self, warning_list=None):
        """
        Test that a warning is generated if the minimisation
        does not result in a convergence. The ensemble mean is the predictor.
        """
        predictor = "mean"
        distribution = "truncnorm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=10)
        plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg = "Minimisation did not result in convergence after"
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(any(warning_msg in str(item) for item in warning_list))

    @ManageWarnings(
        record=True,
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_catch_warnings_percentage_change(self, warning_list=None):
        """
        Test that two warnings are generated if the minimisation
        does not result in a convergence. The first warning reports a that
        the minimisation did not result in convergence, whilst the second
        warning reports that the percentage change in the final iteration was
        greater than the tolerated value.
        The ensemble mean is the predictor.
        """
        initial_guess = np.array([0, 1, 5000, 1], dtype=np.float64)

        predictor = "mean"
        distribution = "truncnorm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=5)

        plugin.process(
            initial_guess,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg_min = "Minimisation did not result in convergence after"
        warning_msg_iter = "The final iteration resulted in a percentage "
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(
            any(warning_msg_min in str(item) for item in warning_list))
        self.assertTrue(
            any(warning_msg_iter in str(item) for item in warning_list))
class Test_process_normal_distribution(SetupNormalInputs,
                                       EnsembleCalibrationAssertions):
    """
    Test minimising the CRPS for a normal distribution.
    Either the ensemble mean or the individual ensemble realizations are used
    as the predictors.
    """
    def setUp(self):
        """Set up expected output.
        The coefficients are in the order [alpha, beta, gamma, delta].
        """
        super().setUp()
        self.tolerance = 1e-4
        self.plugin = Plugin(tolerance=self.tolerance)
        self.expected_mean_coefficients = [-0.0003, 1.0013, 0.0012, 0.5945]
        self.expected_realizations_coefficients = [
            0.0254,
            0.4349,
            0.39,
            0.8122,
            -0.0016,
            0.2724,
        ]
        self.expected_mean_coefficients_point_by_point = np.array(
            [
                [
                    [0.0015, 0.0037, -0.002],
                    [-0.0009, 0.0008, 0.0015],
                    [-0.0046, 0.0053, -0.0038],
                ],
                [
                    [1.0039, 1.0035, 1.0009],
                    [1.0013, 1.0011, 1.001],
                    [1.002, 1.0015, 1.0008],
                ],
                [
                    [0.0017, -0.0009, -0.0002],
                    [0.0054, 0.0003, -0.0002],
                    [-0.0001, -0.0018, 0.0002],
                ],
                [
                    [-0.0, 0.0007, -0.0009],
                    [0.0003, -0.0001, -0.001],
                    [-0.0013, 0.0, 0.0006],
                ],
            ],
            dtype=np.float32,
        )

        self.expected_mean_coefficients_point_by_point_sites = np.array(
            [
                [0.0017, 0.0017, 0.0017, 0.0017],
                [1.0036, 1.0036, 1.0036, 1.0036],
                [0.0017, 0.0017, 0.0017, 0.0017],
                [-0.0, -0.0, -0.0, 0.0],
            ],
            dtype=np.float32,
        )

        self.expected_realizations_coefficients_point_by_point = np.array(
            [
                [
                    [0.0001, 0.0001, 0.0001],
                    [0.0001, 0.0001, 0.0],
                    [0.0001, 0.0001, 0.0001],
                ],
                [
                    [0.579, 0.5793, 0.5782],
                    [0.5782, 0.5778, 0.5781],
                    [0.5786, 0.5782, 0.5783],
                ],
                [
                    [0.5795, 0.5786, 0.5782],
                    [0.5783, 0.578, 0.5767],
                    [0.5791, 0.578, 0.5763],
                ],
                [
                    [0.5773, 0.5769, 0.5763],
                    [0.5769, 0.5771, 0.5782],
                    [0.5764, 0.5773, 0.5783],
                ],
                [
                    [0.0001, 0.0001, 0.0001],
                    [0.0001, 0.0001, 0.0001],
                    [0.0001, 0.0001, 0.0],
                ],
                [
                    [1.0194, 1.0143, 1.0199],
                    [1.0199, 1.02, 1.013],
                    [1.0144, 0.9885, 1.0246],
                ],
            ],
            dtype=np.float32,
        )

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    def test_basic_mean_predictor(self):
        """
        Test that the plugin returns a numpy array with the expected
        coefficients. The ensemble mean is the predictor.
        """
        predictor = "mean"
        distribution = "norm"
        result = self.plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertIsInstance(result, np.ndarray)
        self.assertEqual(result.dtype, np.float32)
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
            "invalid value encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_basic_realizations_predictor(self):
        """
        Test that the plugin returns a numpy array with the expected
        coefficients. The ensemble realizations are the predictor.
        """
        predictor = "realizations"
        distribution = "norm"
        result = self.plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertIsInstance(result, np.ndarray)
        self.assertEqual(result.dtype, np.float32)
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_mean_predictor_keyerror(self):
        """
        Test that the minimisation has resulted in a KeyError, if the
        distribution that has been requested was not within the dictionary
        containing the minimisation functions.
        """
        predictor = "mean"
        distribution = "foo"

        msg = "Distribution requested"
        with self.assertRaisesRegex(KeyError, msg):
            self.plugin.process(
                self.initial_guess_for_mean,
                self.forecast_predictor_mean,
                self.truth,
                self.forecast_variance,
                predictor,
                distribution,
            )

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    def test_mean_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble mean is the predictor
        assuming a normal distribution and the value specified for the
        max_iterations is overridden. The coefficients are calculated by
        minimising the CRPS.
        """
        predictor = "mean"
        max_iterations = 400
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
            "invalid value encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_realizations_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble realizations are the
        predictor assuming a truncated normal distribution and the value
        specified for the MAX_ITERATIONS is overridden. The coefficients are
        calculated by minimising the CRPS.
        """
        predictor = "realizations"
        max_iterations = 1000
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    def test_mean_predictor_point_by_point(self):
        """
        Test that the expected coefficients are generated when the ensemble
        mean is the predictor for a normal distribution and coefficients are
        calculated independently at each grid point. The coefficients are
        calculated by minimising the CRPS.
        """
        predictor = "mean"
        distribution = "norm"

        initial_guess = np.broadcast_to(
            self.initial_guess_for_mean,
            (
                len(self.truth.coord(axis="y").points) *
                len(self.truth.coord(axis="x").points),
                len(self.initial_guess_for_mean),
            ),
        )

        plugin = Plugin(tolerance=self.tolerance, point_by_point=True)
        result = plugin.process(
            initial_guess,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_mean_coefficients_point_by_point)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    def test_mean_predictor_point_by_point_sites(self):
        """
        Test that the expected coefficients are generated when the ensemble
        mean is the predictor for a normal distribution and coefficients are
        calculated independently at each site location. The coefficients are
        calculated by minimising the CRPS.
        """
        forecast_spot_cube = self.historic_forecast_spot_cube.collapsed(
            "realization", iris.analysis.MEAN)
        forecast_var_spot_cube = forecast_spot_cube.copy()
        forecast_var_spot_cube.data = forecast_var_spot_cube.data / 10.0

        predictor = "mean"
        distribution = "norm"

        initial_guess = np.broadcast_to(
            self.initial_guess_for_mean,
            (
                len(self.truth.coord(axis="y").points) *
                len(self.truth.coord(axis="x").points),
                len(self.initial_guess_for_mean),
            ),
        )

        plugin = Plugin(tolerance=self.tolerance, point_by_point=True)
        result = plugin.process(
            initial_guess,
            forecast_spot_cube,
            self.truth_spot_cube,
            forecast_var_spot_cube,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_mean_coefficients_point_by_point_sites)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
            "invalid value encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_realizations_predictor_point_by_point(self):
        """
        Test that the expected coefficients are generated when the ensemble
        realizations are the predictor for a normal distribution and
        coefficients are calculated independently at each grid point. The
        coefficients are calculated by minimising the CRPS.
        """
        predictor = "realizations"
        distribution = "norm"

        initial_guess = np.broadcast_to(
            self.initial_guess_for_realization,
            (
                len(self.truth.coord(axis="y").points) *
                len(self.truth.coord(axis="x").points),
                len(self.initial_guess_for_realization),
            ),
        )

        # Use a larger value for the tolerance to terminate sooner to avoid
        # minimising in computational noise.
        plugin = Plugin(tolerance=0.01, point_by_point=True)
        result = plugin.process(
            initial_guess,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertArrayAlmostEqual(
            result,
            self.expected_realizations_coefficients_point_by_point,
            decimal=2)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    @ManageWarnings(
        record=True,
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_catch_warnings(self, warning_list=None):
        """
        Test that a warning is generated if the minimisation
        does not result in a convergence. The ensemble mean is the predictor.
        """
        predictor = "mean"
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=10)
        plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg = "Minimisation did not result in convergence after"
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(any(warning_msg in str(item) for item in warning_list))

    @ManageWarnings(
        record=True,
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_catch_warnings_percentage_change(self, warning_list=None):
        """
        Test that two warnings are generated if the minimisation
        does not result in a convergence. The first warning reports a that
        the minimisation did not result in convergence, whilst the second
        warning reports that the percentage change in the final iteration was
        greater than the tolerated value. The ensemble mean is the predictor.
        """
        initial_guess = np.array([5000, 1, 0, 1], dtype=np.float64)
        predictor = "mean"
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=5)
        plugin.process(
            initial_guess,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg_min = "Minimisation did not result in convergence after"
        warning_msg_iter = "The final iteration resulted in a percentage "
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(
            any(warning_msg_min in str(item) for item in warning_list))
        self.assertTrue(
            any(warning_msg_iter in str(item) for item in warning_list))
class Test_process_normal_distribution(SetupNormalInputs,
                                       EnsembleCalibrationAssertions):
    """
    Test minimising the CRPS for a normal distribution.
    Either the ensemble mean or the individual ensemble realizations are used
    as the predictors.
    """
    def setUp(self):
        """Set up expected output.
        The coefficients are in the order [alpha, beta, gamma, delta].
        """
        super().setUp()
        self.tolerance = 1e-4
        self.plugin = Plugin(tolerance=self.tolerance)
        self.expected_mean_coefficients = [-0.0008, 1.0009, 0.0023, 0.8070]
        self.expected_realizations_coefficients = [
            0.0427,
            0.4117,
            0.1946,
            0.8907,
            -0.1435,
            0.037,
        ]

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    def test_basic_mean_predictor(self):
        """
        Test that the plugin returns a numpy array with the expected
        coefficients. The ensemble mean is the predictor.
        """
        predictor = "mean"
        distribution = "norm"
        result = self.plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertIsInstance(result, np.ndarray)
        self.assertEqual(result.dtype, np.float32)
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
            "invalid value encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_basic_realizations_predictor(self):
        """
        Test that the plugin returns a numpy array with the expected
        coefficients. The ensemble realizations are the predictor.
        """
        predictor = "realizations"
        distribution = "norm"
        result = self.plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertIsInstance(result, np.ndarray)
        self.assertEqual(result.dtype, np.float32)
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_mean_predictor_keyerror(self):
        """
        Test that the minimisation has resulted in a KeyError, if the
        distribution that has been requested was not within the dictionary
        containing the minimisation functions.
        """
        predictor = "mean"
        distribution = "foo"

        msg = "Distribution requested"
        with self.assertRaisesRegex(KeyError, msg):
            self.plugin.process(
                self.initial_guess_for_mean,
                self.forecast_predictor_mean,
                self.truth,
                self.forecast_variance,
                predictor,
                distribution,
            )

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
        ],
        warning_types=[UserWarning, UserWarning, RuntimeWarning],
    )
    def test_mean_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble mean is the predictor
        assuming a normal distribution and the value specified for the
        max_iterations is overridden. The coefficients are calculated by
        minimising the CRPS and using a set default value for the
        initial guess.
        """
        predictor = "mean"
        max_iterations = 400
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(result,
                                               self.expected_mean_coefficients)

    @ManageWarnings(
        ignored_messages=[
            "Collapsing a non-contiguous coordinate.",
            "Minimisation did not result in convergence",
            "divide by zero encountered in",
            "invalid value encountered in",
        ],
        warning_types=[
            UserWarning, UserWarning, RuntimeWarning, RuntimeWarning
        ],
    )
    def test_realizations_predictor_max_iterations(self):
        """
        Test that the plugin returns a list of coefficients
        equal to specific values, when the ensemble realizations are the
        predictor assuming a truncated normal distribution and the value
        specified for the MAX_ITERATIONS is overridden. The coefficients are
        calculated by minimising the CRPS and using a set default value for
        the initial guess.
        """
        predictor = "realizations"
        max_iterations = 1000
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance,
                        max_iterations=max_iterations)
        result = plugin.process(
            self.initial_guess_for_realization,
            self.forecast_predictor_realizations,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        self.assertEMOSCoefficientsAlmostEqual(
            result, self.expected_realizations_coefficients)

    @ManageWarnings(
        record=True,
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_catch_warnings(self, warning_list=None):
        """
        Test that a warning is generated if the minimisation
        does not result in a convergence. The ensemble mean is the predictor.
        """
        predictor = "mean"
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=10)
        plugin.process(
            self.initial_guess_for_mean,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg = "Minimisation did not result in convergence after"
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(any(warning_msg in str(item) for item in warning_list))

    @ManageWarnings(
        record=True,
        ignored_messages=["Collapsing a non-contiguous coordinate."])
    def test_catch_warnings_percentage_change(self, warning_list=None):
        """
        Test that two warnings are generated if the minimisation
        does not result in a convergence. The first warning reports a that
        the minimisation did not result in convergence, whilst the second
        warning reports that the percentage change in the final iteration was
        greater than the tolerated value. The ensemble mean is the predictor.
        """
        initial_guess = np.array([5000, 1, 0, 1], dtype=np.float64)
        predictor = "mean"
        distribution = "norm"

        plugin = Plugin(tolerance=self.tolerance, max_iterations=5)
        plugin.process(
            initial_guess,
            self.forecast_predictor_mean,
            self.truth,
            self.forecast_variance,
            predictor,
            distribution,
        )
        warning_msg_min = "Minimisation did not result in convergence after"
        warning_msg_iter = "The final iteration resulted in a percentage "
        self.assertTrue(
            any(item.category == UserWarning for item in warning_list))
        self.assertTrue(
            any(warning_msg_min in str(item) for item in warning_list))
        self.assertTrue(
            any(warning_msg_iter in str(item) for item in warning_list))