def setUp(self):
        """Set up some probability cubes from different models"""
        data = np.array(
            [
                0.9 * np.ones((3, 3)), 0.5 * np.ones((3, 3)), 0.1 * np.ones(
                    (3, 3))
            ],
            dtype=np.float32,
        )
        thresholds = np.array([273.0, 275.0, 277.0], dtype=np.float32)
        time_point = dt(2015, 11, 23, 7)
        time_bounds = [dt(2015, 11, 23, 4), time_point]

        # set up a MOGREPS-UK cube with 7 hour forecast period
        self.cube_enuk = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_ens",
            time=time_point,
            frt=dt(2015, 11, 23, 0),
            time_bounds=time_bounds,
        )

        # set up a UKV cube with 4 hour forecast period
        self.cube_ukv = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_det",
            time=time_point,
            frt=dt(2015, 11, 23, 3),
            time_bounds=time_bounds,
        )

        self.cubelist = iris.cube.CubeList([self.cube_enuk, self.cube_ukv])

        # set up some non-UK test cubes
        cube_non_mo_ens = self.cube_enuk.copy()
        cube_non_mo_ens.attributes.pop("mosg__model_configuration")
        cube_non_mo_ens.attributes["non_mo_model_config"] = "non_uk_ens"
        cube_non_mo_det = self.cube_ukv.copy()
        cube_non_mo_det.attributes.pop("mosg__model_configuration")
        cube_non_mo_det.attributes["non_mo_model_config"] = "non_uk_det"

        self.non_mo_cubelist = iris.cube.CubeList(
            [cube_non_mo_ens, cube_non_mo_det])

        # set up plugin for multi-model blending weighted by forecast period
        self.plugin = MergeCubesForWeightedBlending(
            "model",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration",
        )
 def test_record_run(self):
     """Test recording the source runs in a blend record run attribute."""
     plugin = MergeCubesForWeightedBlending(
         "model",
         weighting_coord="forecast_period",
         model_id_attr="mosg__model_configuration",
         record_run_attr="mosg__model_run",
     )
     cube = plugin.process(self.cubelist)
     self.assertEqual(
         cube.attributes["mosg__model_run"],
         "uk_det:20151123T0300Z:\nuk_ens:20151123T0000Z:",
     )
Exemplo n.º 3
0
 def setUp(self):
     """Set up cube and plugin"""
     cubelist = set_up_masked_cubes()
     merger = MergeCubesForWeightedBlending(
         "model_id",
         weighting_coord="forecast_period",
         model_id_attr="mosg__model_configuration")
     self.cube = merger.process(cubelist)
     self.plugin = WeightAndBlend("model_id",
                                  "dict",
                                  weighting_coord="forecast_period",
                                  wts_dict=MODEL_WEIGHTS)
     self.initial_weights = (self.plugin._calculate_blending_weights(
         self.cube))
 def test_blend_realizations(self):
     """Test processing works for merging over coordinates that don't
     require specific setup"""
     data = np.ones((1, 3, 3), dtype=np.float32)
     cube1 = set_up_variable_cube(data, realizations=np.array([0]))
     cube1 = iris.util.squeeze(cube1)
     cube2 = set_up_variable_cube(data, realizations=np.array([1]))
     cube2 = iris.util.squeeze(cube2)
     plugin = MergeCubesForWeightedBlending("realization")
     result = plugin.process([cube1, cube2])
     self.assertIsInstance(result, iris.cube.Cube)
     self.assertArrayEqual(result.coord("realization").points, np.array([0, 1]))
     self.assertEqual(result[0].metadata, cube1.metadata)
     self.assertEqual(result[1].metadata, cube2.metadata)
 def test_record_run_existing(self):
     """Test recording blend source runs with existing record attributes."""
     plugin = MergeCubesForWeightedBlending(
         "model",
         weighting_coord="forecast_period",
         model_id_attr="mosg__model_configuration",
         record_run_attr="mosg__model_run",
     )
     self.cube_ukv.attributes[
         "mosg__model_run"] = "uk_det:20151123T0200Z:\nuk_det:20151123T0300Z:"
     cube = plugin.process([self.cube_ukv, self.cube_enuk])
     self.assertEqual(
         cube.attributes["mosg__model_run"],
         "uk_det:20151123T0200Z:\nuk_det:20151123T0300Z:\nuk_ens:20151123T0000Z:",
     )
Exemplo n.º 6
0
 def test_optional_args(self):
     """Test model ID and weighting coordinate setting"""
     plugin = MergeCubesForWeightedBlending(
         "model_id",
         weighting_coord="forecast_period",
         model_id_attr="mosg__model_configuration")
     self.assertEqual(plugin.weighting_coord, "forecast_period")
     self.assertEqual(plugin.model_id_attr, "mosg__model_configuration")
Exemplo n.º 7
0
 def test_cycle_blend(self):
     """Test merge for blending over forecast_reference_time"""
     cube = self.cube_ukv.copy()
     cube.coord("forecast_reference_time").points = (
         cube.coord("forecast_reference_time").points + 3600)
     cube.coord("forecast_period").points = (
         cube.coord("forecast_reference_time").points - 3600)
     plugin = MergeCubesForWeightedBlending("forecast_reference_time")
     result = plugin.process([self.cube_ukv, cube])
     self.assertIsInstance(result, iris.cube.Cube)
     self.assertIn(result.coord("forecast_reference_time"),
                   result.coords(dim_coords=True))
     # check no model coordinates have been added
     with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
         result.coord(MODEL_BLEND_COORD)
     with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
         result.coord(MODEL_NAME_COORD)
Exemplo n.º 8
0
 def test_blend_coord_ascending(self):
     """Test the order of the output blend coordinate is always ascending,
     independent of the input cube order"""
     frt = self.cube_ukv.coord("forecast_reference_time").points[0]
     fp = self.cube_ukv.coord("forecast_period").points[0]
     cube1 = self.cube_ukv.copy()
     cube1.coord("forecast_reference_time").points = [frt + 3600]
     cube1.coord("forecast_period").points = [fp - 3600]
     cube2 = self.cube_ukv.copy()
     cube2.coord("forecast_reference_time").points = [frt + 7200]
     cube2.coord("forecast_period").points = [fp - 7200]
     # input unordered cubes; expect ordered output
     expected_points = np.array([frt, frt + 3600, frt + 7200],
                                dtype=np.int64)
     plugin = MergeCubesForWeightedBlending("forecast_reference_time")
     result = plugin.process([cube1, self.cube_ukv, cube2])
     self.assertArrayEqual(
         result.coord("forecast_reference_time").points, expected_points)
Exemplo n.º 9
0
 def test_warning_unnecessary_model_id_attr(self, warning_list=None):
     """Test warning if model_id_attr is set for non-model blending"""
     warning_msg = "model_id_attr not required"
     plugin = MergeCubesForWeightedBlending(
         "realization", model_id_attr="mosg__model_configuration")
     self.assertTrue(
         any(item.category == UserWarning for item in warning_list))
     self.assertTrue(any(warning_msg in str(item) for item in warning_list))
     self.assertIsNone(plugin.model_id_attr)
Exemplo n.º 10
0
    def test_dict(self):
        """Test dictionary option for model blending with non-equal weights"""
        data = np.ones((3, 3, 3), dtype=np.float32)
        thresholds = np.array([276, 277, 278], dtype=np.float32)
        ukv_cube = set_up_probability_cube(
            data,
            thresholds,
            time=dt(2018, 9, 10, 7),
            frt=dt(2018, 9, 10, 1),
            standard_grid_metadata="uk_det",
        )
        enukx_cube = set_up_probability_cube(
            data,
            thresholds,
            time=dt(2018, 9, 10, 7),
            frt=dt(2018, 9, 10, 1),
            standard_grid_metadata="uk_ens",
        )
        merger = MergeCubesForWeightedBlending(
            "model_id",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration",
        )
        cube = merger.process([ukv_cube, enukx_cube])

        plugin = WeightAndBlend(
            "model_id",
            "dict",
            weighting_coord="forecast_period",
            wts_dict=MODEL_WEIGHTS,
        )

        # at 6 hours lead time we should have 1/3 UKV and 2/3 MOGREPS-UK,
        # according to the dictionary weights specified above
        weights = plugin._calculate_blending_weights(cube)
        self.assertArrayEqual(
            weights.coord("model_configuration").points, ["uk_det", "uk_ens"]
        )
        self.assertArrayAlmostEqual(weights.data, np.array([0.3333333, 0.6666667]))
    def test_forecast_coord_deprecation(self):
        """Test merging works if some (but not all) inputs have previously been cycle
        blended"""
        for cube in [self.cube_ukv, self.cube_enuk]:
            for coord in ["forecast_period", "forecast_reference_time"]:
                cube.coord(coord).attributes.update({"deprecation_message": "blah"})

        plugin = MergeCubesForWeightedBlending(
            "model_id", model_id_attr="mosg__model_configuration"
        )
        result = plugin([self.cube_nowcast, self.cube_ukv, self.cube_enuk])
        for coord in ["forecast_period", "forecast_reference_time"]:
            self.assertNotIn("deprecation_message", result.coord(coord).attributes)
Exemplo n.º 12
0
    def setUp(self):
        """Set up some probability cubes from different models"""
        data = np.array(
            [
                0.9 * np.ones((3, 3)), 0.5 * np.ones((3, 3)), 0.1 * np.ones(
                    (3, 3))
            ],
            dtype=np.float32,
        )
        thresholds = np.array([273.0, 275.0, 277.0], dtype=np.float32)
        time_point = dt(2015, 11, 23, 7)

        # set up a MOGREPS-UK cube with 7 hour forecast period
        self.cube_enuk = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_ens",
            time=time_point,
            frt=dt(2015, 11, 23, 0),
        )

        # set up a UKV cube with 4 hour forecast period
        self.cube_ukv = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_det",
            time=time_point,
            frt=dt(2015, 11, 23, 3),
        )

        self.cubelist = iris.cube.CubeList([self.cube_enuk, self.cube_ukv])
        self.plugin = MergeCubesForWeightedBlending(
            "model",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration",
        )
Exemplo n.º 13
0
    def test_handling_blend_time(self):
        """Test merging works with mismatched and / or missing blend time
        coordinates"""
        blend_time_ukv = self.cube_ukv.coord("forecast_reference_time").copy()
        blend_time_ukv.rename("blend_time")
        self.cube_ukv.add_aux_coord(blend_time_ukv)

        blend_time_enuk = self.cube_enuk.coord(
            "forecast_reference_time").copy()
        blend_time_enuk.rename("blend_time")
        self.cube_enuk.add_aux_coord(blend_time_enuk)

        plugin = MergeCubesForWeightedBlending(
            "model_id", model_id_attr="mosg__model_configuration")
        result = plugin([self.cube_nowcast, self.cube_ukv, self.cube_enuk])
        self.assertNotIn("blend_time", get_coord_names(result))
Exemplo n.º 14
0
 def test_time_bounds_mismatch(self):
     """Test failure for cycle blending when time bounds ranges are not
     matched (ie cycle blending different "accumulation periods")"""
     cube2 = self.cube_ukv.copy()
     cube2.coord("forecast_reference_time").points = (
         cube2.coord("forecast_reference_time").points + 3600)
     cube2.coord("time").bounds = [
         cube2.coord("time").bounds[0, 0] + 3600,
         cube2.coord("time").bounds[0, 1],
     ]
     cube2.coord("forecast_period").bounds = [
         cube2.coord("forecast_period").bounds[0, 0] + 3600,
         cube2.coord("forecast_period").bounds[0, 1],
     ]
     msg = "Cube with mismatching time bounds ranges cannot be blended"
     with self.assertRaisesRegex(ValueError, msg):
         MergeCubesForWeightedBlending("forecast_reference_time").process(
             [self.cube_ukv, cube2])
Exemplo n.º 15
0
class Test__create_model_coordinates(IrisTest):
    """Test the _create_model_coordinates method"""
    def setUp(self):
        """Set up some probability cubes from different models"""
        data = np.array(
            [
                0.9 * np.ones((3, 3)), 0.5 * np.ones((3, 3)), 0.1 * np.ones(
                    (3, 3))
            ],
            dtype=np.float32,
        )
        thresholds = np.array([273.0, 275.0, 277.0], dtype=np.float32)
        time_point = dt(2015, 11, 23, 7)

        # set up a MOGREPS-UK cube with 7 hour forecast period
        self.cube_enuk = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_ens",
            time=time_point,
            frt=dt(2015, 11, 23, 0),
        )

        # set up a UKV cube with 4 hour forecast period
        self.cube_ukv = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_det",
            time=time_point,
            frt=dt(2015, 11, 23, 3),
        )

        self.cubelist = iris.cube.CubeList([self.cube_enuk, self.cube_ukv])
        self.plugin = MergeCubesForWeightedBlending(
            "model",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration",
        )

    def test_values(self):
        """Test values of model coordinates are as expected"""
        expected_id = [0, 1000]
        expected_config = ["uk_ens", "uk_det"]
        self.plugin._create_model_coordinates(self.cubelist)
        for cube, m_id, m_conf in zip(self.cubelist, expected_id,
                                      expected_config):
            self.assertEqual(cube.coord(MODEL_BLEND_COORD).points, [m_id])
            self.assertEqual(cube.coord(MODEL_NAME_COORD).points, [m_conf])

    def test_unmatched_model_id_attr(self):
        """Test error if model_id_attr is not present on both input cubes"""
        self.cubelist[0].attributes.pop("mosg__model_configuration")
        msg = "Cannot create model ID coordinate for grid blending "
        with self.assertRaisesRegex(ValueError, msg):
            self.plugin._create_model_coordinates(self.cubelist)

    def test_error_same_model(self):
        """Test error if input cubes are from the same model"""
        new_cubelist = iris.cube.CubeList(
            [self.cube_enuk.copy(),
             self.cube_enuk.copy()])
        msg = "Cannot create model dimension"
        with self.assertRaisesRegex(ValueError, msg):
            self.plugin._create_model_coordinates(new_cubelist)
Exemplo n.º 16
0
 def test_error_missing_model_id_attr(self):
     """Test exception is raised if blending over model with no identifying
     attribute"""
     msg = "model_id_attr required to blend over model_id"
     with self.assertRaisesRegex(ValueError, msg):
         MergeCubesForWeightedBlending("model_id")
Exemplo n.º 17
0
 def test_basic(self):
     """Test default initialisation"""
     plugin = MergeCubesForWeightedBlending("realization")
     self.assertEqual(plugin.blend_coord, "realization")
     self.assertIsNone(plugin.weighting_coord)
     self.assertIsNone(plugin.model_id_attr)
Exemplo n.º 18
0
 def test_blend_coord_not_present(self):
     """Test exception when blend coord is not present on inputs"""
     msg = "realization coordinate is not present on all input cubes"
     with self.assertRaisesRegex(ValueError, msg):
         MergeCubesForWeightedBlending("realization").process(self.cubelist)
Exemplo n.º 19
0
class Test_process(IrisTest):
    """Test the process method"""
    def setUp(self):
        """Set up some probability cubes from different models"""
        data = np.array([
            0.9 * np.ones((3, 3)), 0.5 * np.ones((3, 3)), 0.1 * np.ones((3, 3))
        ],
                        dtype=np.float32)
        thresholds = np.array([273., 275., 277.], dtype=np.float32)
        time_point = dt(2015, 11, 23, 7)
        time_bounds = [dt(2015, 11, 23, 4), time_point]

        # set up a MOGREPS-UK cube with 7 hour forecast period
        self.cube_enuk = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata='uk_ens',
            time=time_point,
            frt=dt(2015, 11, 23, 0),
            time_bounds=time_bounds)

        # set up a UKV cube with 4 hour forecast period
        self.cube_ukv = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata='uk_det',
            time=time_point,
            frt=dt(2015, 11, 23, 3),
            time_bounds=time_bounds)

        self.cubelist = iris.cube.CubeList([self.cube_enuk, self.cube_ukv])

        # set up some non-UK test cubes
        cube_non_mo_ens = self.cube_enuk.copy()
        cube_non_mo_ens.attributes.pop("mosg__model_configuration")
        cube_non_mo_ens.attributes['non_mo_model_config'] = 'non_uk_ens'
        cube_non_mo_det = self.cube_ukv.copy()
        cube_non_mo_det.attributes.pop("mosg__model_configuration")
        cube_non_mo_det.attributes['non_mo_model_config'] = 'non_uk_det'

        self.non_mo_cubelist = iris.cube.CubeList(
            [cube_non_mo_ens, cube_non_mo_det])

        # set up plugin for multi-model blending weighted by forecast period
        self.plugin = MergeCubesForWeightedBlending(
            "model",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration")

    def test_basic(self):
        """Test single cube is returned unmodified"""
        cube = self.cube_enuk.copy()
        result = self.plugin.process(cube)
        self.assertArrayAlmostEqual(result.data, self.cube_enuk.data)
        self.assertEqual(result.metadata, self.cube_enuk.metadata)

    def test_single_item_list(self):
        """Test cube from single item list is returned unmodified"""
        cubelist = iris.cube.CubeList([self.cube_enuk.copy()])
        result = self.plugin.process(cubelist)
        self.assertArrayAlmostEqual(result.data, self.cube_enuk.data)
        self.assertEqual(result.metadata, self.cube_enuk.metadata)

    def test_multi_model_merge(self):
        """Test models merge OK and have expected model coordinates"""
        result = self.plugin.process(self.cubelist)
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayEqual(result.coord("model_id").points, [0, 1000])
        self.assertArrayEqual(
            result.coord("model_configuration").points, ["uk_ens", "uk_det"])

    def test_time_coords(self):
        """Test merged cube has scalar time coordinates if weighting models
        by forecast period"""
        result = self.plugin.process(self.cubelist)
        # test resulting cube has single 4 hour (shorter) forecast period
        self.assertEqual(result.coord("forecast_period").points, [4 * 3600])
        # check time and frt points are also consistent with the UKV input cube
        self.assertEqual(
            result.coord("time").points,
            self.cube_ukv.coord("time").points)
        self.assertEqual(
            result.coord("forecast_reference_time").points,
            self.cube_ukv.coord("forecast_reference_time").points)

    def test_cycle_blend(self):
        """Test merge for blending over forecast_reference_time"""
        cube = self.cube_ukv.copy()
        cube.coord("forecast_reference_time").points = (
            cube.coord("forecast_reference_time").points + 3600)
        cube.coord("forecast_period").points = (
            cube.coord("forecast_reference_time").points - 3600)
        plugin = MergeCubesForWeightedBlending("forecast_reference_time")
        result = plugin.process([self.cube_ukv, cube])
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertIn(result.coord("forecast_reference_time"),
                      result.coords(dim_coords=True))
        # check forecast period coordinate has been removed
        with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
            result.coord("forecast_period")
        # check no model coordinates have been added
        with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
            result.coord("model_id")
        with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
            result.coord("model_configuration")

    def test_cycletime(self):
        """Test merged cube has updated forecast reference time and forecast
        period if specified using the 'cycletime' argument"""
        result = self.plugin.process(self.cubelist, cycletime="20151123T0600Z")
        # test resulting cube has forecast period consistent with cycletime
        self.assertEqual(result.coord("forecast_period").points, [3600])
        self.assertEqual(
            result.coord("forecast_reference_time").points,
            self.cube_ukv.coord("forecast_reference_time").points + 3 * 3600)
        # check validity time is unchanged
        self.assertEqual(
            result.coord("time").points,
            self.cube_ukv.coord("time").points)

    def test_non_mo_model_id(self):
        """Test that a model ID attribute string can be specified when
        merging multi model cubes"""
        plugin = MergeCubesForWeightedBlending(
            "model", model_id_attr="non_mo_model_config")
        result = plugin.process(self.non_mo_cubelist)
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayEqual(result.coord("model_id").points, [0, 1000])

    def test_model_id_attr_mismatch(self):
        """Test that when a model ID attribute string is specified that does
        not match the model ID attribute key name on both cubes to be merged,
        an error is thrown"""
        plugin = MergeCubesForWeightedBlending(
            "model", model_id_attr="non_matching_model_config")
        msg = "Cannot create model ID coordinate"
        with self.assertRaisesRegex(ValueError, msg):
            plugin.process(self.non_mo_cubelist)

    def test_model_id_attr_mismatch_one_cube(self):
        """Test that when a model ID attribute string is specified that only
        matches the model ID attribute key name on one of the cubes to be
        merged, an error is thrown"""
        self.non_mo_cubelist[1].attributes.pop("non_mo_model_config")
        self.non_mo_cubelist[1].attributes[
            "non_matching_model_config"] = "non_uk_det"
        plugin = MergeCubesForWeightedBlending(
            "model", model_id_attr="non_matching_model_config")
        msg = "Cannot create model ID coordinate"
        with self.assertRaisesRegex(ValueError, msg):
            plugin.process(self.non_mo_cubelist)

    def test_time_bounds_mismatch(self):
        """Test failure for cycle blending when time bounds ranges are not
        matched (ie cycle blending different "accumulation periods")"""
        cube2 = self.cube_ukv.copy()
        cube2.coord("forecast_reference_time").points = (
            cube2.coord("forecast_reference_time").points + 3600)
        cube2.coord("time").bounds = [
            cube2.coord("time").bounds[0, 0] + 3600,
            cube2.coord("time").bounds[0, 1]
        ]
        cube2.coord("forecast_period").bounds = [
            cube2.coord("forecast_period").bounds[0, 0] + 3600,
            cube2.coord("forecast_period").bounds[0, 1]
        ]
        msg = "Cube with mismatching time bounds ranges cannot be blended"
        with self.assertRaisesRegex(ValueError, msg):
            MergeCubesForWeightedBlending("forecast_reference_time").process(
                [self.cube_ukv, cube2])

    def test_blend_coord_not_present(self):
        """Test exception when blend coord is not present on inputs"""
        msg = "realization coordinate is not present on all input cubes"
        with self.assertRaisesRegex(ValueError, msg):
            MergeCubesForWeightedBlending("realization").process(self.cubelist)

    def test_blend_realizations(self):
        """Test processing works for merging over coordinates that don't
        require specific setup"""
        data = np.ones((1, 3, 3), dtype=np.float32)
        cube1 = set_up_variable_cube(data, realizations=np.array([0]))
        cube1 = iris.util.squeeze(cube1)
        cube2 = set_up_variable_cube(data, realizations=np.array([1]))
        cube2 = iris.util.squeeze(cube2)
        plugin = MergeCubesForWeightedBlending("realization")
        result = plugin.process([cube1, cube2])
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayEqual(
            result.coord("realization").points, np.array([0, 1]))
        self.assertEqual(result[0].metadata, cube1.metadata)
        self.assertEqual(result[1].metadata, cube2.metadata)
Exemplo n.º 20
0
class Test__rationalise_blend_time_coords(IrisTest):
    """Tests for the _rationalise_cycle_blend_time_coords method"""
    def setUp(self):
        """Set up a list of cubes from different models with some probability
        data in them."""

        # make a cube with a forecast reference time and period labelled as
        # coming from the UKV
        data = np.full((3, 3), 0.6, dtype=np.float32)
        self.ukv_cube = set_up_variable_cube(
            data,
            name='probability_of_air_temperature_above_threshold',
            units='1',
            time=dt(2017, 1, 10, 3, 0),
            frt=dt(2017, 1, 9, 23, 0),
            standard_grid_metadata='uk_det')

        # make a cube labelled as coming from MOGREPS-UK, with a different
        # forecast reference time from the UKV cube
        self.enuk_cube = set_up_variable_cube(
            data,
            name='probability_of_air_temperature_above_threshold',
            units='1',
            time=dt(2017, 1, 10, 3, 0),
            frt=dt(2017, 1, 10, 0, 0),
            standard_grid_metadata='uk_ens')

        # make a cube list and merged cube containing the two model cubes, for
        # use in defining reference coordinates for tests below
        self.cubelist = iris.cube.CubeList([self.ukv_cube, self.enuk_cube])

        # set up a plugin for multi-model blending
        self.plugin = MergeCubesForWeightedBlending(
            "model",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration")

    def test_null_irrelevant_coord(self):
        """Test function does nothing if not given a relevant coord"""
        reference_cubelist = self.cubelist.copy()
        plugin = MergeCubesForWeightedBlending("realization")
        plugin._rationalise_blend_time_coords(self.cubelist)
        self.assertEqual(self.cubelist, reference_cubelist)

    def test_null_cubes_have_fp(self):
        """Test function does nothing if blending over forecast_reference_time
        where a forecast period coordinate exists"""
        reference_cubelist = self.cubelist.copy()
        plugin = MergeCubesForWeightedBlending("forecast_reference_time")
        plugin._rationalise_blend_time_coords(self.cubelist)
        self.assertEqual(self.cubelist, reference_cubelist)

    def test_null_model_no_fp(self):
        """Test function does nothing if blending over models but not weighting
        by forecast period"""
        reference_cubelist = self.cubelist.copy()
        self.plugin._rationalise_blend_time_coords(self.cubelist)
        self.assertEqual(self.cubelist, reference_cubelist)

    def test_remove_fp(self):
        """Test function removes forecast_period coord if blending over
        forecast_reference_time"""
        plugin = MergeCubesForWeightedBlending("forecast_reference_time")
        plugin._rationalise_blend_time_coords(self.cubelist)
        for cube in self.cubelist:
            self.assertTrue("forecast_period" not in cube.coords())

    def test_unify_frt(self):
        """Test function equalises forecast reference times if weighting a
        model blend by forecast_period"""
        expected_frt, = self.enuk_cube.coord("forecast_reference_time").points
        expected_fp = 3 * 3600
        self.plugin._rationalise_blend_time_coords(self.cubelist)
        for cube in self.cubelist:
            self.assertEqual(
                cube.coord("forecast_reference_time").points[0], expected_frt)
            self.assertEqual(
                cube.coord("forecast_period").points[0], expected_fp)

    def test_cycletime(self):
        """Test function sets different cycle time if passed in as argument"""
        expected_frt, = (
            self.enuk_cube.coord("forecast_reference_time").points -
            (3 * 3600))
        expected_fp = 6 * 3600
        self.plugin._rationalise_blend_time_coords(self.cubelist,
                                                   cycletime='20170109T2100Z')
        for cube in self.cubelist:
            self.assertEqual(
                cube.coord("forecast_reference_time").points[0], expected_frt)
            self.assertEqual(
                cube.coord("forecast_period").points[0], expected_fp)

    def test_error_frt_dim(self):
        """Test an error is raised if forecast reference time is a dimension
        coordinate on any of the model input cubes"""
        ukv_cube_2 = self.ukv_cube.copy()
        ukv_cube_2.coord("forecast_reference_time").points = [
            ukv_cube_2.coord("forecast_reference_time").points[0] + 3600
        ]
        ukv_cube_2.coord("forecast_period").points = [
            ukv_cube_2.coord("forecast_period").points[0] + 3600
        ]
        ukv_cubelist = iris.cube.CubeList([self.ukv_cube, ukv_cube_2]).merge()
        msg = 'Expecting scalar forecast_reference_time'
        with self.assertRaisesRegex(ValueError, msg):
            self.plugin._rationalise_blend_time_coords(ukv_cubelist)
Exemplo n.º 21
0
class Test_process(IrisTest):
    """Test the process method"""
    def setUp(self):
        """Set up some probability cubes from different models"""
        data = np.array(
            [
                0.9 * np.ones((3, 3)), 0.5 * np.ones((3, 3)), 0.1 * np.ones(
                    (3, 3))
            ],
            dtype=np.float32,
        )
        thresholds = np.array([273.0, 275.0, 277.0], dtype=np.float32)
        time_point = dt(2015, 11, 23, 7)
        time_bounds = [dt(2015, 11, 23, 4), time_point]

        # set up a MOGREPS-UK cube with 7 hour forecast period
        self.cube_enuk = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_ens",
            time=time_point,
            frt=dt(2015, 11, 23, 0),
            time_bounds=time_bounds,
        )

        # set up a UKV cube with 4 hour forecast period
        self.cube_ukv = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="uk_det",
            time=time_point,
            frt=dt(2015, 11, 23, 3),
            time_bounds=time_bounds,
        )

        self.cubelist = iris.cube.CubeList([self.cube_enuk, self.cube_ukv])

        # set up a nowcast cube
        self.cube_nowcast = set_up_probability_cube(
            data.copy(),
            thresholds,
            standard_grid_metadata="nc_det",
            time=time_point,
            frt=dt(2015, 11, 23, 3, 15),
            time_bounds=time_bounds,
        )

        # set up some non-UK test cubes
        cube_non_mo_ens = self.cube_enuk.copy()
        cube_non_mo_ens.attributes.pop("mosg__model_configuration")
        cube_non_mo_ens.attributes["non_mo_model_config"] = "non_uk_ens"
        cube_non_mo_det = self.cube_ukv.copy()
        cube_non_mo_det.attributes.pop("mosg__model_configuration")
        cube_non_mo_det.attributes["non_mo_model_config"] = "non_uk_det"

        self.non_mo_cubelist = iris.cube.CubeList(
            [cube_non_mo_ens, cube_non_mo_det])

        # set up plugin for multi-model blending weighted by forecast period
        self.plugin = MergeCubesForWeightedBlending(
            "model",
            weighting_coord="forecast_period",
            model_id_attr="mosg__model_configuration",
        )

    def test_multi_model_merge(self):
        """Test models merge OK and have expected model coordinates"""
        result = self.plugin.process(self.cubelist)
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayEqual(
            result.coord(MODEL_BLEND_COORD).points, [0, 1000])
        self.assertArrayEqual(
            result.coord(MODEL_NAME_COORD).points, ["uk_ens", "uk_det"])

    def test_time_coords(self):
        """Test merged cube has scalar time coordinates if weighting models
        by forecast period"""
        result = self.plugin.process(self.cubelist)
        # test resulting cube has single 4 hour (shorter) forecast period
        self.assertEqual(result.coord("forecast_period").points, [4 * 3600])
        # check time and frt points are also consistent with the UKV input cube
        self.assertEqual(
            result.coord("time").points,
            self.cube_ukv.coord("time").points)
        self.assertEqual(
            result.coord("forecast_reference_time").points,
            self.cube_ukv.coord("forecast_reference_time").points,
        )

    def test_cycle_blend(self):
        """Test merge for blending over forecast_reference_time"""
        cube = self.cube_ukv.copy()
        cube.coord("forecast_reference_time").points = (
            cube.coord("forecast_reference_time").points + 3600)
        cube.coord("forecast_period").points = (
            cube.coord("forecast_reference_time").points - 3600)
        plugin = MergeCubesForWeightedBlending("forecast_reference_time")
        result = plugin.process([self.cube_ukv, cube])
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertIn(result.coord("forecast_reference_time"),
                      result.coords(dim_coords=True))
        # check no model coordinates have been added
        with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
            result.coord(MODEL_BLEND_COORD)
        with self.assertRaises(iris.exceptions.CoordinateNotFoundError):
            result.coord(MODEL_NAME_COORD)

    def test_blend_coord_ascending(self):
        """Test the order of the output blend coordinate is always ascending,
        independent of the input cube order"""
        frt = self.cube_ukv.coord("forecast_reference_time").points[0]
        fp = self.cube_ukv.coord("forecast_period").points[0]
        cube1 = self.cube_ukv.copy()
        cube1.coord("forecast_reference_time").points = [frt + 3600]
        cube1.coord("forecast_period").points = [fp - 3600]
        cube2 = self.cube_ukv.copy()
        cube2.coord("forecast_reference_time").points = [frt + 7200]
        cube2.coord("forecast_period").points = [fp - 7200]
        # input unordered cubes; expect ordered output
        expected_points = np.array([frt, frt + 3600, frt + 7200],
                                   dtype=np.int64)
        plugin = MergeCubesForWeightedBlending("forecast_reference_time")
        result = plugin.process([cube1, self.cube_ukv, cube2])
        self.assertArrayEqual(
            result.coord("forecast_reference_time").points, expected_points)

    def test_cycletime(self):
        """Test merged cube has updated forecast reference time and forecast
        period if specified using the 'cycletime' argument"""
        result = self.plugin.process(self.cubelist, cycletime="20151123T0600Z")
        # test resulting cube has forecast period consistent with cycletime
        self.assertEqual(result.coord("forecast_period").points, [3600])
        self.assertEqual(
            result.coord("forecast_reference_time").points,
            self.cube_ukv.coord("forecast_reference_time").points + 3 * 3600,
        )
        # check validity time is unchanged
        self.assertEqual(
            result.coord("time").points,
            self.cube_ukv.coord("time").points)

    def test_non_mo_model_id(self):
        """Test that a model ID attribute string can be specified when
        merging multi model cubes"""
        plugin = MergeCubesForWeightedBlending(
            "model", model_id_attr="non_mo_model_config")
        result = plugin.process(self.non_mo_cubelist)
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayEqual(
            result.coord(MODEL_BLEND_COORD).points, [0, 1000])

    def test_model_id_attr_mismatch(self):
        """Test that when a model ID attribute string is specified that does
        not match the model ID attribute key name on both cubes to be merged,
        an error is thrown"""
        plugin = MergeCubesForWeightedBlending(
            "model", model_id_attr="non_matching_model_config")
        msg = "Cannot create model ID coordinate"
        with self.assertRaisesRegex(ValueError, msg):
            plugin.process(self.non_mo_cubelist)

    def test_model_id_attr_mismatch_one_cube(self):
        """Test that when a model ID attribute string is specified that only
        matches the model ID attribute key name on one of the cubes to be
        merged, an error is thrown"""
        self.non_mo_cubelist[1].attributes.pop("non_mo_model_config")
        self.non_mo_cubelist[1].attributes[
            "non_matching_model_config"] = "non_uk_det"
        plugin = MergeCubesForWeightedBlending(
            "model", model_id_attr="non_matching_model_config")
        msg = "Cannot create model ID coordinate"
        with self.assertRaisesRegex(ValueError, msg):
            plugin.process(self.non_mo_cubelist)

    def test_time_bounds_mismatch(self):
        """Test failure for cycle blending when time bounds ranges are not
        matched (ie cycle blending different "accumulation periods")"""
        cube2 = self.cube_ukv.copy()
        cube2.coord("forecast_reference_time").points = (
            cube2.coord("forecast_reference_time").points + 3600)
        cube2.coord("time").bounds = [
            cube2.coord("time").bounds[0, 0] + 3600,
            cube2.coord("time").bounds[0, 1],
        ]
        cube2.coord("forecast_period").bounds = [
            cube2.coord("forecast_period").bounds[0, 0] + 3600,
            cube2.coord("forecast_period").bounds[0, 1],
        ]
        msg = "Cube with mismatching time bounds ranges cannot be blended"
        with self.assertRaisesRegex(ValueError, msg):
            MergeCubesForWeightedBlending("forecast_reference_time").process(
                [self.cube_ukv, cube2])

    def test_blend_coord_not_present(self):
        """Test exception when blend coord is not present on inputs"""
        msg = "realization coordinate is not present on all input cubes"
        with self.assertRaisesRegex(ValueError, msg):
            MergeCubesForWeightedBlending("realization").process(self.cubelist)

    def test_blend_realizations(self):
        """Test processing works for merging over coordinates that don't
        require specific setup"""
        data = np.ones((1, 3, 3), dtype=np.float32)
        cube1 = set_up_variable_cube(data, realizations=np.array([0]))
        cube1 = iris.util.squeeze(cube1)
        cube2 = set_up_variable_cube(data, realizations=np.array([1]))
        cube2 = iris.util.squeeze(cube2)
        plugin = MergeCubesForWeightedBlending("realization")
        result = plugin.process([cube1, cube2])
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayEqual(
            result.coord("realization").points, np.array([0, 1]))
        self.assertEqual(result[0].metadata, cube1.metadata)
        self.assertEqual(result[1].metadata, cube2.metadata)

    def test_handling_blend_time(self):
        """Test merging works with mismatched and / or missing blend time
        coordinates"""
        blend_time_ukv = self.cube_ukv.coord("forecast_reference_time").copy()
        blend_time_ukv.rename("blend_time")
        self.cube_ukv.add_aux_coord(blend_time_ukv)

        blend_time_enuk = self.cube_enuk.coord(
            "forecast_reference_time").copy()
        blend_time_enuk.rename("blend_time")
        self.cube_enuk.add_aux_coord(blend_time_enuk)

        plugin = MergeCubesForWeightedBlending(
            "model_id", model_id_attr="mosg__model_configuration")
        result = plugin([self.cube_nowcast, self.cube_ukv, self.cube_enuk])
        self.assertNotIn("blend_time", get_coord_names(result))

    def test_forecast_coord_deprecation(self):
        """Test merging works if some (but not all) inputs have previously been cycle
        blended"""
        for cube in [self.cube_ukv, self.cube_enuk]:
            for coord in ["forecast_period", "forecast_reference_time"]:
                cube.coord(coord).attributes.update(
                    {"deprecation_message": "blah"})

        plugin = MergeCubesForWeightedBlending(
            "model_id", model_id_attr="mosg__model_configuration")
        result = plugin([self.cube_nowcast, self.cube_ukv, self.cube_enuk])
        for coord in ["forecast_period", "forecast_reference_time"]:
            self.assertNotIn("deprecation_message",
                             result.coord(coord).attributes)
Exemplo n.º 22
0
    def process(self,
                cubelist,
                cycletime=None,
                model_id_attr=None,
                spatial_weights=False,
                fuzzy_length=20000):
        """
        Merge a cubelist, calculate appropriate blend weights and compute the
        weighted mean. Returns a single cube collapsed over the dimension
        given by self.blend_coord.

        Args:
            cubelist (iris.cube.CubeList):
                List of cubes to be merged and blended

        Kwargs:
            cycletime (str):
                Forecast reference time to use for output cubes, in the format
                YYYYMMDDTHHMMZ.  If not set, the latest of the input cube
                forecast reference times is used.
            model_id_attr (str):
                Name of the attribute by which to identify the source model and
                construct "model" coordinates for blending.
            spatial_weights (bool):
                If true, calculate spatial weights.
            fuzzy_length (float):
                Distance (in metres) over which to smooth spatial weights.
                Default is 20 km.
        """
        # Prepare cubes for weighted blending, including creating model_id and
        # model_configuration coordinates for multi-model blending. The merged
        # cube has a monotonically ascending blend coordinate. Plugin raises an
        # error if blend_coord is not present on all input cubes.
        merger = MergeCubesForWeightedBlending(
            self.blend_coord,
            weighting_coord=self.weighting_coord,
            model_id_attr=model_id_attr)
        cube = merger.process(cubelist, cycletime=cycletime)

        # if blend_coord has only one value, or is not present (case where only
        # one model has been provided for a model blend) update metadata only
        coord_names = [coord.name() for coord in cube.coords()]
        if (self.blend_coord not in coord_names
                or len(cube.coord(self.blend_coord).points) == 1):
            result = cube.copy()
            conform_metadata(result,
                             cube,
                             self.blend_coord,
                             cycletime=cycletime)

        # otherwise, calculate weights and blend across specified dimension
        else:
            # set up special treatment for model blending
            if "model" in self.blend_coord:
                self.blend_coord = "model_id"

            # calculate blend weights
            weights = self._calculate_blending_weights(cube)
            if spatial_weights:
                weights = self._update_spatial_weights(cube, weights,
                                                       fuzzy_length)

            # blend across specified dimension
            BlendingPlugin = WeightedBlendAcrossWholeDimension(
                self.blend_coord, cycletime=cycletime)
            result = BlendingPlugin.process(cube, weights=weights)

        return result
Exemplo n.º 23
0
def main(argv=None):
    """Load in arguments and ensure they are set correctly.
       Then load in the data to blend and calculate default weights
       using the method chosen before carrying out the blending."""
    parser = ArgParser(
        description='Calculate the default weights to apply in weighted '
        'blending plugins using the ChooseDefaultWeightsLinear or '
        'ChooseDefaultWeightsNonLinear plugins. Then apply these '
        'weights to the dataset using the BasicWeightedAverage plugin.'
        ' Required for ChooseDefaultWeightsLinear: y0val and ynval.'
        ' Required for ChooseDefaultWeightsNonLinear: cval.'
        ' Required for ChooseWeightsLinear with dict: wts_dict.')

    parser.add_argument('--wts_calc_method',
                        metavar='WEIGHTS_CALCULATION_METHOD',
                        choices=['linear', 'nonlinear', 'dict'],
                        default='linear',
                        help='Method to use to calculate '
                        'weights used in blending. "linear" (default): '
                        'calculate linearly varying blending weights. '
                        '"nonlinear": calculate blending weights that decrease'
                        ' exponentially with increasing blending coordinate. '
                        '"dict": calculate weights using a dictionary passed '
                        'in as a command line argument.')

    parser.add_argument('coordinate',
                        type=str,
                        metavar='COORDINATE_TO_AVERAGE_OVER',
                        help='The coordinate over which the blending '
                        'will be applied.')
    parser.add_argument('--coordinate_unit',
                        metavar='UNIT_STRING',
                        default='hours since 1970-01-01 00:00:00',
                        help='Units for blending coordinate. Default= '
                        'hours since 1970-01-01 00:00:00')
    parser.add_argument('--calendar',
                        metavar='CALENDAR',
                        help='Calendar for time coordinate. Default=gregorian')
    parser.add_argument('--cycletime',
                        metavar='CYCLETIME',
                        type=str,
                        help='The forecast reference time to be used after '
                        'blending has been applied, in the format '
                        'YYYYMMDDTHHMMZ. If not provided, the blended file '
                        'will take the latest available forecast reference '
                        'time from the input cubes supplied.')
    parser.add_argument('--model_id_attr',
                        metavar='MODEL_ID_ATTR',
                        type=str,
                        default="mosg__model_configuration",
                        help='The name of the netCDF file attribute to be '
                        'used to identify the source model for '
                        'multi-model blends. Default assumes Met Office '
                        'model metadata. Must be present on all input '
                        'files if blending over models.')
    parser.add_argument('--spatial_weights_from_mask',
                        action='store_true',
                        default=False,
                        help='If set this option will result in the generation'
                        ' of spatially varying weights based on the'
                        ' masks of the data we are blending. The'
                        ' one dimensional weights are first calculated '
                        ' using the chosen weights calculation method,'
                        ' but the weights will then be adjusted spatially'
                        ' based on where there is masked data in the data'
                        ' we are blending. The spatial weights are'
                        ' calculated using the'
                        ' SpatiallyVaryingWeightsFromMask plugin.')
    parser.add_argument('weighting_mode',
                        metavar='WEIGHTED_BLEND_MODE',
                        choices=['weighted_mean', 'weighted_maximum'],
                        help='The method used in the weighted blend. '
                        '"weighted_mean": calculate a normal weighted'
                        ' mean across the coordinate. '
                        '"weighted_maximum": multiplies the values in the'
                        ' coordinate by the weights, and then takes the'
                        ' maximum.')

    parser.add_argument('input_filepaths',
                        metavar='INPUT_FILES',
                        nargs="+",
                        help='Paths to input files to be blended.')
    parser.add_argument('output_filepath',
                        metavar='OUTPUT_FILE',
                        help='The output path for the processed NetCDF.')

    spatial = parser.add_argument_group(
        'Spatial weights from mask options',
        'Options for calculating the spatial weights using the '
        'SpatiallyVaryingWeightsFromMask plugin.')
    spatial.add_argument('--fuzzy_length',
                         metavar='FUZZY_LENGTH',
                         type=float,
                         default=20000,
                         help='When calculating spatially varying weights we'
                         ' can smooth the weights so that areas close to'
                         ' areas that are masked have lower weights than'
                         ' those further away. This fuzzy length controls'
                         ' the scale over which the weights are smoothed.'
                         ' The fuzzy length is in terms of m, the'
                         ' default is 20km. This distance is then'
                         ' converted into a number of grid squares,'
                         ' which does not have to be an integer. Assumes'
                         ' the grid spacing is the same in the x and y'
                         ' directions, and raises an error if this is not'
                         ' true. See SpatiallyVaryingWeightsFromMask for'
                         ' more detail.')

    linear = parser.add_argument_group(
        'linear weights options', 'Options for the linear weights '
        'calculation in '
        'ChooseDefaultWeightsLinear')
    linear.add_argument('--y0val',
                        metavar='LINEAR_STARTING_POINT',
                        type=float,
                        help='The relative value of the weighting start point '
                        '(lowest value of blend coord) for choosing default '
                        'linear weights. This must be a positive float or 0.')
    linear.add_argument('--ynval',
                        metavar='LINEAR_END_POINT',
                        type=float,
                        help='The relative value of the weighting '
                        'end point (highest value of blend coord) for choosing'
                        ' default linear weights. This must be a positive '
                        'float or 0.  Note that if blending over forecast '
                        'reference time, ynval >= y0val would normally be '
                        'expected (to give greater weight to the more recent '
                        'forecast).')

    nonlinear = parser.add_argument_group(
        'nonlinear weights options', 'Options for the non-linear '
        'weights calculation in '
        'ChooseDefaultWeightsNonLinear')
    nonlinear.add_argument('--cval',
                           metavar='NON_LINEAR_FACTOR',
                           type=float,
                           help='Factor used to determine how skewed the '
                           'non linear weights will be. '
                           'A value of 1 implies equal weighting. If not '
                           'set, a default value of cval=0.85 is set.')

    wts_dict = parser.add_argument_group(
        'dict weights options', 'Options for linear weights to be '
        'calculated based on parameters '
        'read from a json file dict')
    wts_dict.add_argument('--wts_dict',
                          metavar='WEIGHTS_DICTIONARY',
                          help='Path to json file containing dictionary from '
                          'which to calculate blending weights. Dictionary '
                          'format is as specified in the improver.blending.'
                          'weights.ChooseWeightsLinear plugin.')
    wts_dict.add_argument('--weighting_coord',
                          metavar='WEIGHTING_COORD',
                          default='forecast_period',
                          help='Name of '
                          'coordinate over which linear weights should be '
                          'scaled. This coordinate must be avilable in the '
                          'weights dictionary.')

    args = parser.parse_args(args=argv)

    # if the linear weights method is called with non-linear args or vice
    # versa, exit with error
    if (args.wts_calc_method == "linear") and args.cval:
        parser.wrong_args_error('cval', 'linear')
    if ((args.wts_calc_method == "nonlinear")
            and np.any([args.y0val, args.ynval])):
        parser.wrong_args_error('y0val, ynval', 'non-linear')
    if (args.wts_calc_method == "dict") and not args.wts_dict:
        parser.error('Dictionary is required if --wts_calc_method="dict"')

    # set blending coordinate units
    if "time" in args.coordinate:
        coord_unit = Unit(args.coordinate_unit, args.calendar)
    elif args.coordinate_unit != 'hours since 1970-01-01 00:00:00.':
        coord_unit = args.coordinate_unit
    else:
        coord_unit = 'no_unit'

    # For blending across models, only blending across "model_id" is directly
    # supported. This is because the blending coordinate must be sortable, in
    # order to ensure that the data cube and the weights cube have coordinates
    # in the same order for blending. Whilst the model_configuration is
    # sortable itself, as it is associated with model_id, which is the
    # dimension coordinate, sorting the model_configuration coordinate can
    # result in the model_id coordinate becoming non-monotonic. As dimension
    # coordinates must be monotonic, this leads to the model_id coordinate
    # being demoted to an auxiliary coordinate. Therefore, for simplicity
    # model_id is used as the blending coordinate, instead of
    # model_configuration.
    # TODO: Support model_configuration as a blending coordinate directly.
    if args.coordinate == "model_configuration":
        blend_coord = "model_id"
        dict_coord = "model_configuration"
    else:
        blend_coord = args.coordinate
        dict_coord = args.coordinate

    # load cubes to be blended
    cubelist = load_cubelist(args.input_filepaths)

    # determine whether or not to equalise forecast periods for model
    # blending weights calculation
    weighting_coord = (args.weighting_coord
                       if args.weighting_coord else "forecast_period")

    # prepare cubes for weighted blending
    merger = MergeCubesForWeightedBlending(blend_coord,
                                           weighting_coord=weighting_coord,
                                           model_id_attr=args.model_id_attr)
    cube = merger.process(cubelist, cycletime=args.cycletime)

    # if the coord for blending does not exist or has only one value,
    # update metadata only
    coord_names = [coord.name() for coord in cube.coords()]
    if (blend_coord not in coord_names) or (len(
            cube.coord(blend_coord).points) == 1):
        result = cube.copy()
        conform_metadata(result, cube, blend_coord, cycletime=args.cycletime)
        # raise a warning if this happened because the blend coordinate
        # doesn't exist
        if blend_coord not in coord_names:
            warnings.warn('Blend coordinate {} is not present on input '
                          'data'.format(blend_coord))

    # otherwise, calculate weights and blend across specified dimension
    else:
        weights = calculate_blending_weights(
            cube,
            blend_coord,
            args.wts_calc_method,
            wts_dict=args.wts_dict,
            weighting_coord=args.weighting_coord,
            coord_unit=coord_unit,
            y0val=args.y0val,
            ynval=args.ynval,
            cval=args.cval,
            dict_coord=dict_coord)

        if args.spatial_weights_from_mask:
            check_if_grid_is_equal_area(cube)
            grid_cells_x, _ = convert_distance_into_number_of_grid_cells(
                cube, args.fuzzy_length, int_grid_cells=False)
            SpatialWeightsPlugin = SpatiallyVaryingWeightsFromMask(
                grid_cells_x)
            weights = SpatialWeightsPlugin.process(cube, weights, blend_coord)

        # blend across specified dimension
        BlendingPlugin = WeightedBlendAcrossWholeDimension(
            blend_coord, args.weighting_mode, cycletime=args.cycletime)
        result = BlendingPlugin.process(cube, weights=weights)

    save_netcdf(result, args.output_filepath)
Exemplo n.º 24
0
    def process(
        self,
        cubelist: Union[List[Cube], CubeList],
        cycletime: Optional[str] = None,
        model_id_attr: Optional[str] = None,
        spatial_weights: bool = False,
        fuzzy_length: float = 20000,
        attributes_dict: Optional[Dict[str, str]] = None,
    ) -> Cube:
        """
        Merge a cubelist, calculate appropriate blend weights and compute the
        weighted mean. Returns a single cube collapsed over the dimension
        given by self.blend_coord.

        Args:
            cubelist:
                List of cubes to be merged and blended
            cycletime:
                The forecast reference time to be used after blending has been
                applied, in the format YYYYMMDDTHHMMZ. If not provided, the
                blended file takes the latest available forecast reference time
                from the input datasets supplied.
            model_id_attr:
                The name of the dataset attribute to be used to identify the source
                model when blending data from different models.
            spatial_weights:
                If True, this option will result in the generation of spatially
                varying weights based on the masks of the data we are blending.
                The one dimensional weights are first calculated using the chosen
                weights calculation method, but the weights will then be adjusted
                spatially based on where there is masked data in the data we are
                blending. The spatial weights are calculated using the
                SpatiallyVaryingWeightsFromMask plugin.
            fuzzy_length:
                When calculating spatially varying weights we can smooth the
                weights so that areas close to areas that are masked have lower
                weights than those further away. This fuzzy length controls the
                scale over which the weights are smoothed. The fuzzy length is in
                terms of m, the default is 20km. This distance is then converted
                into a number of grid squares, which does not have to be an
                integer. Assumes the grid spacing is the same in the x and y
                directions and raises an error if this is not true. See
                SpatiallyVaryingWeightsFromMask for more details.
            attributes_dict:
                Dictionary describing required changes to attributes after blending

        Returns:
            Cube of blended data.

        Warns:
            UserWarning: If blending masked data without spatial weights.
                         This has not been fully tested.
        """
        # Prepare cubes for weighted blending, including creating custom metadata
        # for multi-model blending. The merged cube has a monotonically ascending
        # blend coordinate. Plugin raises an error if blend_coord is not present on
        # all input cubes.
        merger = MergeCubesForWeightedBlending(
            self.blend_coord,
            weighting_coord=self.weighting_coord,
            model_id_attr=model_id_attr,
        )
        cube = merger(cubelist, cycletime=cycletime)

        if "model" in self.blend_coord:
            self.blend_coord = copy(MODEL_BLEND_COORD)

        coords_to_remove = get_coords_to_remove(cube, self.blend_coord)

        if len(cube.coord(self.blend_coord).points) > 1:
            weights = self._calculate_blending_weights(cube)
            cube, weights = self._remove_zero_weighted_slices(cube, weights)

        # Deal with case of only one input cube or non-zero-weighted slice
        if len(cube.coord(self.blend_coord).points) == 1:
            result = cube
        else:
            if spatial_weights:
                weights = self._update_spatial_weights(cube, weights,
                                                       fuzzy_length)
            elif np.ma.is_masked(cube.data):
                # Raise warning if blending masked arrays using non-spatial weights.
                warnings.warn(
                    "Blending masked data without spatial weights has not been"
                    " fully tested.")

            # Blend across specified dimension
            BlendingPlugin = WeightedBlendAcrossWholeDimension(
                self.blend_coord)
            result = BlendingPlugin(cube, weights=weights)

        # Remove custom metadata and and update time-type coordinates.  Remove
        # non-time-type coordinate that were previously associated with the blend
        # dimension (coords_to_remove).  Add user-specified and standard blend
        # attributes.
        update_blended_metadata(
            result,
            self.blend_coord,
            coords_to_remove=coords_to_remove,
            cycletime=cycletime,
            attributes_dict=attributes_dict,
            model_id_attr=model_id_attr,
        )

        return result
Exemplo n.º 25
0
    def process(
        self,
        cubelist,
        cycletime=None,
        model_id_attr=None,
        spatial_weights=False,
        fuzzy_length=20000,
        attributes_dict=None,
    ):
        """
        Merge a cubelist, calculate appropriate blend weights and compute the
        weighted mean. Returns a single cube collapsed over the dimension
        given by self.blend_coord.

        Args:
            cubelist (iris.cube.CubeList):
                List of cubes to be merged and blended
            cycletime (str):
                Forecast reference time to use for output cubes, in the format
                YYYYMMDDTHHMMZ.  If not set, the latest of the input cube
                forecast reference times is used.
            model_id_attr (str):
                Name of the attribute by which to identify the source model and
                construct "model" coordinates for blending.
            spatial_weights (bool):
                If true, calculate spatial weights.
            fuzzy_length (float):
                Distance (in metres) over which to smooth spatial weights.
                Default is 20 km.
            attributes_dict (dict or None):
                Changes to cube attributes to be applied after blending

        Returns:
            iris.cube.Cube:
                Cube of blended data.

        Warns:
            UserWarning: If blending masked data without spatial weights.
                         This has not been fully tested.
        """
        # Prepare cubes for weighted blending, including creating custom metadata
        # for multi-model blending. The merged cube has a monotonically ascending
        # blend coordinate. Plugin raises an error if blend_coord is not present on
        # all input cubes.
        merger = MergeCubesForWeightedBlending(
            self.blend_coord,
            weighting_coord=self.weighting_coord,
            model_id_attr=model_id_attr,
        )
        cube = merger(cubelist, cycletime=cycletime)

        if "model" in self.blend_coord:
            self.blend_coord = copy(MODEL_BLEND_COORD)

        coords_to_remove = get_coords_to_remove(cube, self.blend_coord)

        if len(cube.coord(self.blend_coord).points) > 1:
            weights = self._calculate_blending_weights(cube)
            cube, weights = self._remove_zero_weighted_slices(cube, weights)

        # Deal with case of only one input cube or non-zero-weighted slice
        if len(cube.coord(self.blend_coord).points) == 1:
            result = cube
        else:
            if spatial_weights:
                weights = self._update_spatial_weights(cube, weights,
                                                       fuzzy_length)
            elif np.ma.is_masked(cube.data):
                # Raise warning if blending masked arrays using non-spatial weights.
                warnings.warn(
                    "Blending masked data without spatial weights has not been"
                    " fully tested.")

            # Blend across specified dimension
            BlendingPlugin = WeightedBlendAcrossWholeDimension(
                self.blend_coord)
            result = BlendingPlugin(cube, weights=weights)

        # Remove custom metadata and and update time-type coordinates.  Remove
        # non-time-type coordinate that were previously associated with the blend
        # dimension (coords_to_remove).  Add user-specified and standard blend
        # attributes.
        update_blended_metadata(
            result,
            self.blend_coord,
            coords_to_remove=coords_to_remove,
            cycletime=cycletime,
            attributes_dict=attributes_dict,
            model_id_attr=model_id_attr,
        )

        return result
    def process(
        self,
        cubelist,
        cycletime=None,
        model_id_attr=None,
        spatial_weights=False,
        fuzzy_length=20000,
        attributes_dict=None,
    ):
        """
        Merge a cubelist, calculate appropriate blend weights and compute the
        weighted mean. Returns a single cube collapsed over the dimension
        given by self.blend_coord.

        Args:
            cubelist (iris.cube.CubeList):
                List of cubes to be merged and blended
            cycletime (str):
                Forecast reference time to use for output cubes, in the format
                YYYYMMDDTHHMMZ.  If not set, the latest of the input cube
                forecast reference times is used.
            model_id_attr (str):
                Name of the attribute by which to identify the source model and
                construct "model" coordinates for blending.
            spatial_weights (bool):
                If true, calculate spatial weights.
            fuzzy_length (float):
                Distance (in metres) over which to smooth spatial weights.
                Default is 20 km.
            attributes_dict (dict or None):
                Changes to cube attributes to be applied after blending

         Warns:
            UserWarning: If blending masked data without spatial weights.
                         This has not been fully tested.
        """
        # Prepare cubes for weighted blending, including creating model_id and
        # model_configuration coordinates for multi-model blending. The merged
        # cube has a monotonically ascending blend coordinate. Plugin raises an
        # error if blend_coord is not present on all input cubes.
        merger = MergeCubesForWeightedBlending(
            self.blend_coord,
            weighting_coord=self.weighting_coord,
            model_id_attr=model_id_attr,
        )
        cube = merger(cubelist, cycletime=cycletime)

        # if blend_coord has only one value (for example cycle blending with
        # only one cycle available), or is not present (case where only
        # one model has been provided for a model blend), update attributes
        # and ensure that the forecast reference time on the returned cube
        # is set to the current IMPROVER processing cycle.
        coord_names = [coord.name() for coord in cube.coords()]
        if (self.blend_coord not in coord_names
                or len(cube.coord(self.blend_coord).points) == 1):
            result = cube.copy()
            if attributes_dict is not None:
                amend_attributes(result, attributes_dict)
            (result, ) = rebadge_forecasts_as_latest_cycle([result], cycletime)

        # otherwise, calculate weights and blend across specified dimension
        else:
            # set up special treatment for model blending
            if "model" in self.blend_coord:
                self.blend_coord = "model_id"

            # calculate blend weights
            weights = self._calculate_blending_weights(cube)
            if spatial_weights:
                weights = self._update_spatial_weights(cube, weights,
                                                       fuzzy_length)
            elif np.ma.is_masked(cube.data):
                # Raise warning if blending masked arrays using non-spatial weights.
                warnings.warn(
                    "Blending masked data without spatial weights has not been"
                    " fully tested.")

            # blend across specified dimension
            BlendingPlugin = WeightedBlendAcrossWholeDimension(
                self.blend_coord)
            result = BlendingPlugin(
                cube,
                weights=weights,
                cycletime=cycletime,
                attributes_dict=attributes_dict,
            )

        return result
Exemplo n.º 27
0
 def test_null_irrelevant_coord(self):
     """Test function does nothing if not given a relevant coord"""
     reference_cubelist = self.cubelist.copy()
     plugin = MergeCubesForWeightedBlending("realization")
     plugin._rationalise_blend_time_coords(self.cubelist)
     self.assertEqual(self.cubelist, reference_cubelist)