def process(*cubes: cli.inputcube, ):
    """ Calculate the convection ratio from convective and dynamic (stratiform)
    precipitation rate components.

    Calculates the convective ratio as:

        ratio = convective_rate / (convective_rate + dynamic_rate)

    Then calculates the mean ratio across realizations.

    Args:
        cubes (iris.cube.CubeList):
            Cubes of "lwe_convective_precipitation_rate" and "lwe_stratiform_precipitation_rate"
            in units that can be converted to "m s-1"

    Returns:
        iris.cube.Cube:
            A single cube of convection_ratio.
    """
    from improver.blending.calculate_weights_and_blend import WeightAndBlend
    from improver.convection import ConvectionRatioFromComponents
    from iris.coords import CellMethod

    if len(cubes) != 2:
        raise IOError(f"Expected 2 input cubes, received {len(cubes)}")
    convection_ratio = ConvectionRatioFromComponents()(cubes)
    mean_convection_ratio = WeightAndBlend("realization",
                                           "linear",
                                           y0val=1.0,
                                           ynval=1.0)(convection_ratio)
    mean_convection_ratio.add_cell_method(CellMethod("mean", "realization"))
    return mean_convection_ratio
Esempio n. 2
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]))
Esempio n. 3
0
 def setUp(self):
     """Set up a masked nowcast and unmasked UKV cube"""
     self.cubelist = set_up_masked_cubes()
     self.plugin = WeightAndBlend("model_id",
                                  "dict",
                                  weighting_coord="forecast_period",
                                  wts_dict=MODEL_WEIGHTS)
class Test_process_spatial_weights(IrisTest):
    """Test the process method with spatial weights options"""
    def setUp(self):
        """Set up a masked nowcast and unmasked UKV cube"""
        self.cubelist = set_up_masked_cubes()
        self.plugin = WeightAndBlend(
            "model_id",
            "dict",
            weighting_coord="forecast_period",
            wts_dict=MODEL_WEIGHTS,
        )

    @ManageWarnings(ignored_messages=[
        "Collapsing a non-contiguous coordinate",
        "Deleting unmatched attribute",
    ])
    def test_default(self):
        """Test plugin returns a cube with expected values where default fuzzy
        length is less than grid length (no smoothing)"""
        # data is 50:50 where radar is valid, 100% UKV where radar is masked
        expected_data = np.array(
            [
                np.broadcast_to([0.95, 0.95, 0.95, 0.9, 0.9], (5, 5)),
                np.broadcast_to([0.55, 0.55, 0.55, 0.5, 0.5], (5, 5)),
                np.broadcast_to([0.1, 0.1, 0.1, 0.0, 0.0], (5, 5)),
            ],
            dtype=np.float32,
        )
        result = self.plugin.process(
            self.cubelist,
            model_id_attr="mosg__model_configuration",
            spatial_weights=True,
        )
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayAlmostEqual(result.data, expected_data)

    @ManageWarnings(ignored_messages=[
        "Collapsing a non-contiguous coordinate",
        "Deleting unmatched attribute",
    ])
    def test_fuzzy_length(self):
        """Test values where fuzzy length is equal to 2 grid lengths"""
        # proportion of radar data is reduced at edge of valid region; still
        # 100% UKV where radar data is masked
        expected_data = np.array(
            [
                np.broadcast_to([0.95, 0.95, 0.9333333, 0.9, 0.9], (5, 5)),
                np.broadcast_to([0.55, 0.55, 0.5333333, 0.5, 0.5], (5, 5)),
                np.broadcast_to([0.1, 0.1, 0.0666666, 0.0, 0.0], (5, 5)),
            ],
            dtype=np.float32,
        )
        result = self.plugin.process(
            self.cubelist,
            model_id_attr="mosg__model_configuration",
            spatial_weights=True,
            fuzzy_length=400000,
        )
        self.assertArrayAlmostEqual(result.data, expected_data)
Esempio n. 5
0
    def setUp(self):
        """Set up test cubes (each with a single point and 3 thresholds)"""
        thresholds = np.array([0.5, 1, 2], dtype=np.float32)
        units = "mm h-1"
        name = "lwe_precipitation_rate"
        datatime = dt(2018, 9, 10, 7)
        cycletime = dt(2018, 9, 10, 3)

        # a UKV cube with some rain and a 4 hr forecast period
        rain_data = np.array([[[0.9]], [[0.5]], [[0]]], dtype=np.float32)
        self.ukv_cube = set_up_probability_cube(
            rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=cycletime,
            standard_grid_metadata="uk_det")

        # a UKV cube from a more recent cycle with more rain
        more_rain_data = np.array([[[1]], [[0.6]], [[0.2]]], dtype=np.float32)
        self.ukv_cube_latest = set_up_probability_cube(
            more_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 4),
            standard_grid_metadata="uk_det")

        # a nowcast cube with more rain and a 2 hr forecast period
        self.nowcast_cube = set_up_probability_cube(
            more_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 5),
            attributes={"mosg__model_configuration": "nc_det"})

        # a MOGREPS-UK cube with less rain and a 4 hr forecast period
        less_rain_data = np.array([[[0.7]], [[0.3]], [[0]]], dtype=np.float32)
        self.enukx_cube = set_up_probability_cube(
            less_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=cycletime,
            standard_grid_metadata="uk_ens")

        self.plugin_cycle = WeightAndBlend("forecast_reference_time",
                                           "linear",
                                           y0val=1,
                                           ynval=1)
        self.plugin_model = WeightAndBlend("model_id",
                                           "dict",
                                           weighting_coord="forecast_period",
                                           wts_dict=MODEL_WEIGHTS)
class Test__update_spatial_weights(IrisTest):
    """Test the _update_spatial_weights method"""
    @ManageWarnings(ignored_messages=["Deleting unmatched attribute"])
    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)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_basic(self):
        """Test function returns a cube of the expected shape"""
        expected_dims = [
            "model_id",
            "projection_y_coordinate",
            "projection_x_coordinate",
        ]
        expected_shape = (2, 5, 5)
        result = self.plugin._update_spatial_weights(self.cube,
                                                     self.initial_weights,
                                                     20000)
        result_dims = [
            coord.name() for coord in result.coords(dim_coords=True)
        ]
        self.assertSequenceEqual(result_dims, expected_dims)
        self.assertSequenceEqual(result.shape, expected_shape)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_values(self):
        """Test weights are fuzzified as expected"""
        expected_data = np.array(
            [
                np.broadcast_to([0.5, 0.5, 0.5, 0.5, 0.5], (5, 5)),
                np.broadcast_to([0.5, 0.5, 0.25, 0.0, 0.0], (5, 5)),
            ],
            dtype=np.float32,
        )
        result = self.plugin._update_spatial_weights(self.cube,
                                                     self.initial_weights,
                                                     400000)
        self.assertArrayEqual(
            result.coord("model_configuration").points, ["uk_det", "nc_det"])
        self.assertArrayAlmostEqual(result.data, expected_data)
Esempio n. 7
0
 def test_default_linear(self):
     """Test linear weighting over realizations"""
     cube = set_up_variable_cube(278.0 * np.ones((4, 3, 3), dtype=np.float32))
     plugin = WeightAndBlend("realization", "linear", y0val=1, ynval=1)
     weights = plugin._calculate_blending_weights(cube)
     self.assertIsInstance(weights, iris.cube.Cube)
     weights_dims = [coord.name() for coord in weights.coords(dim_coords=True)]
     self.assertSequenceEqual(weights_dims, ["realization"])
     self.assertArrayAlmostEqual(weights.data, 0.25 * np.ones((4,)))
 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))
Esempio n. 9
0
    def test_default_nonlinear(self):
        """Test non-linear weighting over forecast reference time, where the
        earlier cycle has a higher weighting"""
        data = np.ones((3, 3, 3), dtype=np.float32)
        thresholds = np.array([276, 277, 278], dtype=np.float32)
        ukv_cube_earlier = set_up_probability_cube(
            data, thresholds, time=dt(2018, 9, 10, 7), frt=dt(2018, 9, 10, 3)
        )
        ukv_cube_later = set_up_probability_cube(
            data, thresholds, time=dt(2018, 9, 10, 7), frt=dt(2018, 9, 10, 4)
        )
        cube = iris.cube.CubeList([ukv_cube_later, ukv_cube_earlier]).merge_cube()

        plugin = WeightAndBlend("forecast_reference_time", "nonlinear", cval=0.85)
        weights = plugin._calculate_blending_weights(cube)
        self.assertArrayAlmostEqual(weights.data, np.array([0.5405405, 0.45945945]))
Esempio n. 10
0
 def test_blend_with_zero_weight_one_model_valid(self):
     """Test plugin can cope with only one remaining model in the list to blend"""
     plugin = WeightAndBlend(
         "model_id",
         "dict",
         weighting_coord="forecast_period",
         wts_dict=MODEL_WEIGHTS_WITH_ZERO,
     )
     expected_data = self.nowcast_cube.data.copy()
     result = plugin.process(
         [self.ukv_cube, self.nowcast_cube],
         model_id_attr="mosg__model_configuration",
         cycletime=self.cycletime,
     )
     self.assertArrayAlmostEqual(result.data, expected_data)
     self.assertEqual(result.attributes["mosg__model_configuration"],
                      "nc_det")
Esempio n. 11
0
 def test_cycle(self):
     """Test initialisation for cycle blending"""
     plugin = WeightAndBlend("forecast_reference_time", "linear", y0val=1, ynval=1)
     self.assertEqual(plugin.blend_coord, "forecast_reference_time")
     self.assertEqual(plugin.wts_calc_method, "linear")
     self.assertIsNone(plugin.weighting_coord)
     self.assertAlmostEqual(plugin.y0val, 1.0)
     self.assertAlmostEqual(plugin.ynval, 1.0)
Esempio n. 12
0
 def test_blend_with_zero_weight_one_model_input(self):
     """Test plugin returns data unchanged from a single model, even if that
     model had zero weight"""
     plugin = WeightAndBlend(
         "model_id",
         "dict",
         weighting_coord="forecast_period",
         wts_dict=MODEL_WEIGHTS_WITH_ZERO,
     )
     expected_data = self.ukv_cube.data.copy()
     result = plugin.process(
         self.ukv_cube,
         model_id_attr="mosg__model_configuration",
         cycletime=self.cycletime,
     )
     self.assertArrayAlmostEqual(result.data, expected_data)
     self.assertEqual(result.attributes["mosg__model_configuration"],
                      "uk_det")
 def test_model(self):
     """Test initialisation for model blending"""
     plugin = WeightAndBlend(
         "model_id", "dict", weighting_coord="forecast_period",
         wts_dict=MODEL_WEIGHTS)
     self.assertEqual(plugin.blend_coord, "model_id")
     self.assertEqual(plugin.wts_calc_method, "dict")
     self.assertEqual(plugin.weighting_coord, "forecast_period")
     self.assertDictEqual(plugin.wts_dict, MODEL_WEIGHTS)
Esempio n. 14
0
 def test_blend_with_zero_weight(self):
     """Test plugin produces correct values and attributes when some models read
     into the plugin have zero weighting"""
     plugin = WeightAndBlend(
         "model_id",
         "dict",
         weighting_coord="forecast_period",
         wts_dict=MODEL_WEIGHTS_WITH_ZERO,
     )
     expected_data = np.array([[[0.85]], [[0.45]], [[0.1]]],
                              dtype=np.float32)
     result = plugin.process(
         [self.ukv_cube, self.enukx_cube, self.nowcast_cube],
         model_id_attr="mosg__model_configuration",
         cycletime=self.cycletime,
     )
     self.assertArrayAlmostEqual(result.data, expected_data)
     self.assertEqual(result.attributes["mosg__model_configuration"],
                      "nc_det uk_ens")
def process(
    *cubes: cli.inputcube,
    cycletime: str = None,
):
    """Runs equal-weighted blending for a specific scenario.

    Calculates an equal-weighted blend of input cube data across the realization and
    forecast_reference_time coordinates.

    Args:
        cubes (iris.cube.CubeList):
            Cubelist of cubes to be blended.
        cycletime (str):
            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.

    Returns:
        iris.cube.Cube:
            Merged and blended Cube.
    """
    from iris.cube import CubeList

    from improver.blending.calculate_weights_and_blend import WeightAndBlend
    from improver.utilities.cube_manipulation import collapse_realizations

    cubelist = CubeList()
    for cube in cubes:
        cubelist.append(collapse_realizations(cube))

    plugin = WeightAndBlend(
        "forecast_reference_time",
        "linear",
        y0val=0.5,
        ynval=0.5,
    )
    cube = plugin(
        cubelist,
        cycletime=cycletime,
    )
    return cube
Esempio n. 16
0
def main(argv=None):
    """Load in arguments and get going."""
    parser = ArgParser(
        description="Calculate the threshold truth value of input data "
        "relative to the provided threshold value. By default data are "
        "tested to be above the thresholds, though the --below_threshold "
        "flag enables testing below thresholds. A fuzzy factor or fuzzy "
        "bounds may be provided to capture data that is close to the "
        "threshold.")
    parser.add_argument("input_filepath",
                        metavar="INPUT_FILE",
                        help="A path to an input NetCDF file to be processed")
    parser.add_argument("output_filepath",
                        metavar="OUTPUT_FILE",
                        help="The output path for the processed NetCDF")
    parser.add_argument("threshold_values",
                        metavar="THRESHOLD_VALUES",
                        nargs="*",
                        type=float,
                        help="Threshold value or values about which to "
                        "calculate the truth values; e.g. 270 300. "
                        "Must be omitted if --threshold_config is used.")
    parser.add_argument("--threshold_config",
                        metavar="THRESHOLD_CONFIG",
                        type=str,
                        help="Threshold configuration JSON file containing "
                        "thresholds and (optionally) fuzzy bounds. Best used "
                        "in combination  with --threshold_units. "
                        "It should contain a dictionary of strings that can "
                        "be interpreted as floats with the structure: "
                        " \"THRESHOLD_VALUE\": [LOWER_BOUND, UPPER_BOUND] "
                        "e.g: {\"280.0\": [278.0, 282.0], "
                        "\"290.0\": [288.0, 292.0]}, or with structure "
                        " \"THRESHOLD_VALUE\": \"None\" (no fuzzy bounds). "
                        "Repeated thresholds with different bounds are not "
                        "handled well. Only the last duplicate will be used.")
    parser.add_argument("--threshold_units",
                        metavar="THRESHOLD_UNITS",
                        default=None,
                        type=str,
                        help="Units of the threshold values. If not provided "
                        "the units are assumed to be the same as those of the "
                        "input dataset. Specifying the units here will allow "
                        "a suitable conversion to match the input units if "
                        "possible.")
    parser.add_argument("--below_threshold",
                        default=False,
                        action='store_true',
                        help="By default truth values of 1 are returned for "
                        "data ABOVE the threshold value(s). Using this flag "
                        "changes this behaviour to return 1 for data below "
                        "the threshold values.")
    parser.add_argument("--fuzzy_factor",
                        metavar="FUZZY_FACTOR",
                        default=None,
                        type=float,
                        help="A decimal fraction defining the factor about "
                        "the threshold value(s) which should be treated as "
                        "fuzzy. Data which fail a test against the hard "
                        "threshold value may return a fractional truth value "
                        "if they fall within this fuzzy factor region. Fuzzy "
                        "factor must be in the range 0-1, with higher values "
                        "indicating a narrower fuzzy factor region / sharper "
                        "threshold. NB A fuzzy factor cannot be used with a "
                        "zero threshold or a threshold_config file.")
    parser.add_argument("--collapse-coord",
                        type=str,
                        metavar="COLLAPSE-COORD",
                        default="None",
                        help="An optional ability to set which coordinate "
                        "we want to collapse over. The default is set "
                        "to None.")
    parser.add_argument("--vicinity",
                        type=float,
                        default=None,
                        help="If set,"
                        " distance in metres used to define the vicinity "
                        "within which to search for an occurrence.")

    args = parser.parse_args(args=argv)

    # Deal with mutual-exclusions that ArgumentParser can't handle:
    if args.threshold_values and args.threshold_config:
        raise parser.error("--threshold_config option is not compatible "
                           "with THRESHOLD_VALUES list.")
    if args.fuzzy_factor and args.threshold_config:
        raise parser.error("--threshold_config option is not compatible "
                           "with --fuzzy_factor option.")

    cube = load_cube(args.input_filepath)

    if args.threshold_config:
        try:
            # Read in threshold configuration from JSON file.
            with open(args.threshold_config, 'r') as input_file:
                thresholds_from_file = json.load(input_file)
            thresholds = []
            fuzzy_bounds = []
            is_fuzzy = True
            for key in thresholds_from_file.keys():
                thresholds.append(float(key))
                if is_fuzzy:
                    # If the first threshold has no bounds, fuzzy_bounds is
                    # set to None and subsequent bounds checks are skipped
                    if thresholds_from_file[key] == "None":
                        is_fuzzy = False
                        fuzzy_bounds = None
                    else:
                        fuzzy_bounds.append(tuple(thresholds_from_file[key]))
        except ValueError as err:
            # Extend error message with hint for common JSON error.
            raise type(err)(err + " in JSON file {}. \nHINT: Try "
                            "adding a zero after the decimal point.".format(
                                args.threshold_config))
        except Exception as err:
            # Extend any errors with message about WHERE this occurred.
            raise type(err)(err +
                            " in JSON file {}".format(args.threshold_config))
    else:
        thresholds = args.threshold_values
        fuzzy_bounds = None

    result_no_collapse_coord = BasicThreshold(
        thresholds,
        fuzzy_factor=args.fuzzy_factor,
        fuzzy_bounds=fuzzy_bounds,
        threshold_units=args.threshold_units,
        below_thresh_ok=args.below_threshold).process(cube)

    if args.vicinity is not None:
        # smooth thresholded occurrences over local vicinity
        result_no_collapse_coord = OccurrenceWithinVicinity(
            args.vicinity).process(result_no_collapse_coord)

        new_cube_name = in_vicinity_name_format(
            result_no_collapse_coord.name())

        result_no_collapse_coord.rename(new_cube_name)

    if args.collapse_coord == "None":
        save_netcdf(result_no_collapse_coord, args.output_filepath)
    else:
        # Raise warning if result_no_collapse_coord is masked array
        if np.ma.isMaskedArray(result_no_collapse_coord.data):
            warnings.warn("Collapse-coord option not fully tested with "
                          "masked data.")
        # Take a weighted mean across realizations with equal weights
        plugin = WeightAndBlend(args.collapse_coord,
                                "linear",
                                y0val=1.0,
                                ynval=1.0)
        result_collapse_coord = plugin.process(result_no_collapse_coord)
        save_netcdf(result_collapse_coord, args.output_filepath)
Esempio n. 17
0
 def test_error_blend_coord_absent(self):
     """Test error is raised if blend coord is not present on input cubes"""
     plugin = WeightAndBlend("kittens", "linear", y0val=1, ynval=1)
     msg = "kittens coordinate is not present on all input cubes"
     with self.assertRaisesRegex(ValueError, msg):
         plugin.process([self.ukv_cube, self.ukv_cube_latest])
Esempio n. 18
0
class Test_process(IrisTest):
    """Test the process method"""
    def setUp(self):
        """Set up test cubes (each with a single point and 3 thresholds)"""
        thresholds = np.array([0.5, 1, 2], dtype=np.float32)
        units = "mm h-1"
        name = "lwe_precipitation_rate"
        datatime = dt(2018, 9, 10, 7)

        # a UKV cube with some rain and a 4 hr forecast period
        rain_data = np.array([[[0.9]], [[0.5]], [[0]]], dtype=np.float32)
        self.ukv_cube = set_up_probability_cube(
            rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 3),
            standard_grid_metadata="uk_det",
        )

        # a UKV cube from a more recent cycle with more rain
        more_rain_data = np.array([[[1]], [[0.6]], [[0.2]]], dtype=np.float32)
        self.ukv_cube_latest = set_up_probability_cube(
            more_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 4),
            standard_grid_metadata="uk_det",
        )

        # a nowcast cube with more rain and a 2 hr forecast period
        self.nowcast_cube = set_up_probability_cube(
            more_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 5),
            attributes={"mosg__model_configuration": "nc_det"},
        )

        # a MOGREPS-UK cube with less rain and a 4 hr forecast period
        less_rain_data = np.array([[[0.7]], [[0.3]], [[0]]], dtype=np.float32)
        self.enukx_cube = set_up_probability_cube(
            less_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 3),
            standard_grid_metadata="uk_ens",
        )

        # cycletime from the most recent forecast (simulates current cycle)
        self.cycletime = "20180910T0500Z"
        self.expected_frt = self.nowcast_cube.coord(
            "forecast_reference_time").copy()
        self.expected_fp = self.nowcast_cube.coord("forecast_period").copy()

        self.plugin_cycle = WeightAndBlend("forecast_reference_time",
                                           "linear",
                                           y0val=1,
                                           ynval=1)
        self.plugin_model = WeightAndBlend(
            "model_id",
            "dict",
            weighting_coord="forecast_period",
            wts_dict=MODEL_WEIGHTS,
        )

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_basic(self):
        """Test output is a cube"""
        result = self.plugin_cycle.process(
            [self.ukv_cube, self.ukv_cube_latest],
            cycletime=self.cycletime,
        )
        self.assertIsInstance(result, iris.cube.Cube)

    @ManageWarnings(record=True)
    def test_masked_blending_warning(self, warning_list=None):
        """Test a warning is raised if blending masked data with non-spatial
        weights."""
        ukv_cube = self.ukv_cube.copy(data=np.ma.masked_where(
            self.ukv_cube.data < 0.5, self.ukv_cube.data))
        self.plugin_cycle.process(
            [ukv_cube, self.ukv_cube_latest],
            cycletime=self.cycletime,
        )
        message = "Blending masked data without spatial weights"
        self.assertTrue(any(message in str(item) for item in warning_list))

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_cycle_blend_linear(self):
        """Test plugin produces correct cycle blended output with equal
        linear weightings"""
        expected_data = np.array([[[0.95]], [[0.55]], [[0.1]]],
                                 dtype=np.float32)
        result = self.plugin_cycle.process(
            [self.ukv_cube, self.ukv_cube_latest],
            cycletime=self.cycletime,
        )
        self.assertArrayAlmostEqual(result.data, expected_data)
        self.assertArrayEqual(
            result.coord("time").points,
            self.ukv_cube_latest.coord("time").points)
        self.assertArrayEqual(
            result.coord("forecast_reference_time").points,
            self.expected_frt.points)
        self.assertArrayEqual(
            result.coord("forecast_period").points, self.expected_fp.points)
        for coord in ["forecast_reference_time", "forecast_period"]:
            self.assertIn("deprecation_message",
                          result.coord(coord).attributes)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_model_blend(self):
        """Test plugin produces correct output for UKV-ENUKX model blend
        with 50-50 weightings defined by dictionary"""
        expected_data = np.array([[[0.8]], [[0.4]], [[0]]], dtype=np.float32)
        result = self.plugin_model.process(
            [self.ukv_cube, self.enukx_cube],
            model_id_attr="mosg__model_configuration",
            cycletime=self.cycletime,
        )
        self.assertArrayAlmostEqual(result.data, expected_data)
        self.assertEqual(result.attributes["mosg__model_configuration"],
                         "uk_det uk_ens")
        result_coords = [coord.name() for coord in result.coords()]
        self.assertNotIn("model_id", result_coords)
        self.assertNotIn("model_configuration", result_coords)
        for coord in ["forecast_reference_time", "forecast_period"]:
            self.assertIn("deprecation_message",
                          result.coord(coord).attributes)
            self.assertIn(
                "will be removed",
                result.coord(coord).attributes["deprecation_message"])

    @ManageWarnings(ignored_messages=[
        "Collapsing a non-contiguous coordinate",
        "Deleting unmatched attribute",
    ])
    def test_attributes_dict(self):
        """Test output attributes can be updated through argument"""
        attribute_changes = {
            "mosg__model_configuration": "remove",
            "source": "IMPROVER",
            "title": "IMPROVER Post-Processed Multi-Model Blend",
        }
        expected_attributes = {
            "source": "IMPROVER",
            "title": "IMPROVER Post-Processed Multi-Model Blend",
            "institution": MANDATORY_ATTRIBUTE_DEFAULTS["institution"],
        }
        result = self.plugin_model.process(
            [self.ukv_cube, self.nowcast_cube],
            model_id_attr="mosg__model_configuration",
            attributes_dict=attribute_changes,
            cycletime=self.cycletime,
        )
        self.assertDictEqual(result.attributes, expected_attributes)

    @ManageWarnings(ignored_messages=[
        "Collapsing a non-contiguous coordinate",
        "Deleting unmatched attribute",
    ])
    def test_blend_three_models(self):
        """Test plugin produces correct output for 3-model blend when all
        models have (equal) non-zero weights. Each model in WEIGHTS_DICT has
        a weight of 0.5 at 4 hours lead time, and the total weights are
        re-normalised during the process, so the final blend contains 1/3
        contribution from each of the three models."""
        expected_data = np.array([[[0.8666667]], [[0.4666667]], [[0.0666667]]],
                                 dtype=np.float32)
        result = self.plugin_model.process(
            [self.ukv_cube, self.enukx_cube, self.nowcast_cube],
            model_id_attr="mosg__model_configuration",
            cycletime=self.cycletime,
        )
        self.assertArrayAlmostEqual(result.data, expected_data)
        # make sure output cube has the forecast reference time and period
        # from the most recent contributing model
        for coord in ["time", "forecast_period", "forecast_reference_time"]:
            self.assertArrayEqual(
                result.coord(coord).points,
                self.nowcast_cube.coord(coord).points)

    def test_forecast_coord_deprecation(self):
        """Test model blending works if some (but not all) inputs have previously
        been cycle blended"""
        for cube in [self.ukv_cube, self.enukx_cube]:
            for coord in ["forecast_period", "forecast_reference_time"]:
                cube.coord(coord).attributes.update(
                    {"deprecation_message": "blah"})
        result = self.plugin_model.process(
            [self.ukv_cube, self.enukx_cube, self.nowcast_cube],
            model_id_attr="mosg__model_configuration",
            cycletime=self.cycletime,
        )
        for coord in ["forecast_reference_time", "forecast_period"]:
            self.assertIn("deprecation_message",
                          result.coord(coord).attributes)
            self.assertIn(
                "will be removed",
                result.coord(coord).attributes["deprecation_message"])

    def test_one_cube(self):
        """Test the plugin returns a single input cube with updated attributes
        and time coordinates"""
        expected_coords = {coord.name() for coord in self.enukx_cube.coords()}
        expected_coords.update({"blend_time"})
        result = self.plugin_model.process(
            [self.enukx_cube],
            model_id_attr="mosg__model_configuration",
            cycletime=self.cycletime,
            attributes_dict={"source": "IMPROVER"},
        )
        self.assertArrayAlmostEqual(result.data, self.enukx_cube.data)
        self.assertSetEqual({coord.name()
                             for coord in result.coords()}, expected_coords)
        self.assertEqual(result.attributes["source"], "IMPROVER")

    def test_one_cube_error_no_cycletime(self):
        """Test an error is raised if no cycletime is provided for model blending"""
        msg = "Current cycle time is required"
        with self.assertRaisesRegex(ValueError, msg):
            self.plugin_model.process(
                [self.enukx_cube], model_id_attr="mosg__model_configuration")

    def test_one_cube_with_cycletime_model_blending(self):
        """Test the plugin returns a single input cube with an updated forecast
        reference time and period if given the "cycletime" option."""
        expected_frt = self.enukx_cube.coord(
            "forecast_reference_time").points[0] + 3600
        expected_fp = self.enukx_cube.coord("forecast_period").points[0] - 3600
        result = self.plugin_model.process(
            [self.enukx_cube],
            model_id_attr="mosg__model_configuration",
            cycletime="20180910T0400Z",
        )
        self.assertEqual(
            result.coord("forecast_reference_time").points[0], expected_frt)
        self.assertEqual(
            result.coord("forecast_period").points[0], expected_fp)

    def test_one_cube_with_cycletime_cycle_blending(self):
        """Test the plugin returns a single input cube with an updated forecast
        reference time and period if given the "cycletime" option."""
        expected_frt = self.enukx_cube.coord(
            "forecast_reference_time").points[0] + 3600
        expected_fp = self.enukx_cube.coord("forecast_period").points[0] - 3600
        result = self.plugin_cycle.process([self.enukx_cube],
                                           cycletime="20180910T0400Z")
        self.assertEqual(
            result.coord("forecast_reference_time").points[0], expected_frt)
        self.assertEqual(
            result.coord("forecast_period").points[0], expected_fp)

    def test_error_blend_coord_absent(self):
        """Test error is raised if blend coord is not present on input cubes"""
        plugin = WeightAndBlend("kittens", "linear", y0val=1, ynval=1)
        msg = "kittens coordinate is not present on all input cubes"
        with self.assertRaisesRegex(ValueError, msg):
            plugin.process([self.ukv_cube, self.ukv_cube_latest])
Esempio n. 19
0
class Test_process(IrisTest):
    """Test the process method"""
    def setUp(self):
        """Set up test cubes (each with a single point and 3 thresholds)"""
        thresholds = np.array([0.5, 1, 2], dtype=np.float32)
        units = "mm h-1"
        name = "lwe_precipitation_rate"
        datatime = dt(2018, 9, 10, 7)
        cycletime = dt(2018, 9, 10, 3)

        # a UKV cube with some rain and a 4 hr forecast period
        rain_data = np.array([[[0.9]], [[0.5]], [[0]]], dtype=np.float32)
        self.ukv_cube = set_up_probability_cube(
            rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=cycletime,
            standard_grid_metadata="uk_det")

        # a UKV cube from a more recent cycle with more rain
        more_rain_data = np.array([[[1]], [[0.6]], [[0.2]]], dtype=np.float32)
        self.ukv_cube_latest = set_up_probability_cube(
            more_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 4),
            standard_grid_metadata="uk_det")

        # a nowcast cube with more rain and a 2 hr forecast period
        self.nowcast_cube = set_up_probability_cube(
            more_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=dt(2018, 9, 10, 5),
            attributes={"mosg__model_configuration": "nc_det"})

        # a MOGREPS-UK cube with less rain and a 4 hr forecast period
        less_rain_data = np.array([[[0.7]], [[0.3]], [[0]]], dtype=np.float32)
        self.enukx_cube = set_up_probability_cube(
            less_rain_data,
            thresholds,
            variable_name=name,
            threshold_units=units,
            time=datatime,
            frt=cycletime,
            standard_grid_metadata="uk_ens")

        self.plugin_cycle = WeightAndBlend("forecast_reference_time",
                                           "linear",
                                           y0val=1,
                                           ynval=1)
        self.plugin_model = WeightAndBlend("model_id",
                                           "dict",
                                           weighting_coord="forecast_period",
                                           wts_dict=MODEL_WEIGHTS)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_basic(self):
        """Test output is a cube"""
        result = self.plugin_cycle.process(
            [self.ukv_cube, self.ukv_cube_latest])
        self.assertIsInstance(result, iris.cube.Cube)

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_cycle_blend_linear(self):
        """Test plugin produces correct cycle blended output with equal
        linear weightings"""
        expected_data = np.array([[[0.95]], [[0.55]], [[0.1]]],
                                 dtype=np.float32)
        result = self.plugin_cycle.process(
            [self.ukv_cube, self.ukv_cube_latest])
        self.assertArrayAlmostEqual(result.data, expected_data)
        # make sure output cube has the forecast reference time and period
        # from the most recent contributing cycle
        for coord in ["time", "forecast_reference_time", "forecast_period"]:
            self.assertEqual(result.coord(coord),
                             self.ukv_cube_latest.coord(coord))

    @ManageWarnings(
        ignored_messages=["Collapsing a non-contiguous coordinate"])
    def test_model_blend(self):
        """Test plugin produces correct output for UKV-ENUKX model blend
        with 50-50 weightings defined by dictionary"""
        expected_data = np.array([[[0.8]], [[0.4]], [[0]]], dtype=np.float32)
        result = self.plugin_model.process(
            [self.ukv_cube, self.enukx_cube],
            model_id_attr="mosg__model_configuration")
        self.assertArrayAlmostEqual(result.data, expected_data)
        self.assertEqual(result.attributes["mosg__model_configuration"],
                         "blend")
        result_coords = [coord.name() for coord in result.coords()]
        self.assertNotIn("model_id", result_coords)
        self.assertNotIn("model_configuration", result_coords)

    @ManageWarnings(ignored_messages=[
        "Collapsing a non-contiguous coordinate",
        "Deleting unmatched attribute"
    ])
    def test_blend_three_models(self):
        """Test plugin produces correct output for 3-model blend when all
        models have (equal) non-zero weights. Each model in WEIGHTS_DICT has
        a weight of 0.5 at 4 hours lead time, and the total weights are
        re-normalised during the process, so the final blend contains 1/3
        contribution from each of the three models."""
        expected_data = np.array([[[0.8666667]], [[0.4666667]], [[0.0666667]]],
                                 dtype=np.float32)
        result = self.plugin_model.process(
            [self.ukv_cube, self.enukx_cube, self.nowcast_cube],
            model_id_attr="mosg__model_configuration")
        self.assertArrayAlmostEqual(result.data, expected_data)
        # make sure output cube has the forecast reference time and period
        # from the most recent contributing model
        for coord in ["time", "forecast_period", "forecast_reference_time"]:
            self.assertEqual(result.coord(coord),
                             self.nowcast_cube.coord(coord))

    def test_one_cube(self):
        """Test the plugin returns a single input cube with identical data and
        suitably updated metadata"""
        result = self.plugin_model.process(
            [self.enukx_cube], model_id_attr="mosg__model_configuration")
        self.assertArrayAlmostEqual(result.data, self.enukx_cube.data)
        self.assertEqual(result.attributes['mosg__model_configuration'],
                         'blend')
        self.assertEqual(result.attributes['title'], 'IMPROVER Model Forecast')

    def test_one_cube_with_cycletime(self):
        """Test the plugin returns a single input cube with an updated forecast
        reference time and period if given the "cycletime" option."""
        expected_frt = (
            self.enukx_cube.coord("forecast_reference_time").points[0] + 3600)
        expected_fp = self.enukx_cube.coord("forecast_period").points[0] - 3600
        result = self.plugin_model.process(
            [self.enukx_cube],
            model_id_attr="mosg__model_configuration",
            cycletime='20180910T0400Z')
        self.assertEqual(
            result.coord("forecast_reference_time").points[0], expected_frt)
        self.assertEqual(
            result.coord("forecast_period").points[0], expected_fp)

    def test_error_blend_coord_absent(self):
        """Test error is raised if blend coord is not present on input cubes"""
        plugin = WeightAndBlend("kittens", "linear", y0val=1, ynval=1)
        msg = "kittens coordinate is not present on all input cubes"
        with self.assertRaisesRegex(ValueError, msg):
            plugin.process([self.ukv_cube, self.ukv_cube_latest])
Esempio n. 20
0
 def test_unrecognised_weighting_type_error(self):
     """Test error if wts_calc_method is unrecognised"""
     msg = "Weights calculation method 'kludge' unrecognised"
     with self.assertRaisesRegex(ValueError, msg):
         WeightAndBlend("forecast_period", "kludge")
Esempio n. 21
0
def process(cube: cli.inputcube,
            *,
            threshold_values: cli.comma_separated_list = None,
            threshold_config: cli.inputjson = None,
            threshold_units: str = None,
            comparison_operator='>',
            fuzzy_factor: float = None,
            collapse_coord: str = None,
            vicinity: float = None):
    """Module to apply thresholding to a parameter dataset.

    Calculate the threshold truth values of input data relative to the
    provided threshold value. A fuzzy factor or fuzzy bounds may be provided
    to smooth probabilities where values are close to the threshold.

    Args:
        cube (iris.cube.Cube):
            A cube to be processed.
        threshold_values (list of float):
            Threshold value or values about which to calculate the truth
            values; e.g. 270,300. Must be omitted if 'threshold_config'
            is used.
        threshold_config (dict):
            Threshold configuration containing threshold values and
            (optionally) fuzzy bounds. Best used in combination with
            'threshold_units' It should contain a dictionary of strings that
            can be interpreted as floats with the structure:
            "THRESHOLD_VALUE": [LOWER_BOUND, UPPER_BOUND]
            e.g: {"280.0": [278.0, 282.0], "290.0": [288.0, 292.0]},
            or with structure "THRESHOLD_VALUE": "None" (no fuzzy bounds).
            Repeated thresholds with different bounds are ignored; only the
            last duplicate will be used.
        threshold_units (str):
            Units of the threshold values. If not provided the units are
            assumed to be the same as those of the input cube. Specifying
            the units here will allow a suitable conversion to match
            the input units if possible.
        comparison_operator (str):
            Indicates the comparison_operator to use with the threshold.
            e.g. 'ge' or '>=' to evaluate data >= threshold or '<' to
            evaluate data < threshold. When using fuzzy thresholds, there is
            no difference between < and <= or > and >=.
            Options: > >= < <= gt ge lt le.
        fuzzy_factor (float of None):
            A decimal fraction defining the factor about the threshold value(s)
            which should be treated as fuzzy. Data which fail a test against
            the hard threshold value may return a fractional truth value if
            they fall within this fuzzy factor region.
            Fuzzy factor must be in the range 0-1, with higher values
            indicating a narrower fuzzy factor region / sharper threshold.
            A fuzzy factor cannot be used with a zero threshold or a
            threshold_config file.
        collapse_coord (str):
            An optional ability to set which coordinate we want to collapse
            over.
        vicinity (float):
            Distance in metres used to define the vicinity within which to
            search for an occurrence

    Returns:
        iris.cube.Cube:
            Cube of probabilities relative to the given thresholds

    Raises:
        ValueError: If threshold_config and threshold_values are both set
        ValueError: If threshold_config is used for fuzzy thresholding

     Warns:
        UserWarning: If collapsing coordinates with a masked array

    """
    import warnings
    import numpy as np

    from improver.blending.calculate_weights_and_blend import WeightAndBlend
    from improver.metadata.probabilistic import in_vicinity_name_format
    from improver.threshold import BasicThreshold
    from improver.utilities.spatial import OccurrenceWithinVicinity

    if threshold_config and threshold_values:
        raise ValueError(
            "--threshold-config and --threshold-values are mutually exclusive "
            "- please set one or the other, not both")
    if threshold_config and fuzzy_factor:
        raise ValueError(
            "--threshold-config cannot be used for fuzzy thresholding")

    if threshold_config:
        thresholds = []
        fuzzy_bounds = []
        for key in threshold_config.keys():
            thresholds.append(np.float32(key))
            # If the first threshold has no bounds, fuzzy_bounds is
            # set to None and subsequent bounds checks are skipped
            if threshold_config[key] == "None":
                fuzzy_bounds = None
                continue
            fuzzy_bounds.append(tuple(threshold_config[key]))
    else:
        thresholds = [np.float32(x) for x in threshold_values]
        fuzzy_bounds = None

    result_no_collapse_coord = BasicThreshold(
        thresholds,
        fuzzy_factor=fuzzy_factor,
        fuzzy_bounds=fuzzy_bounds,
        threshold_units=threshold_units,
        comparison_operator=comparison_operator)(cube)

    if vicinity is not None:
        # smooth thresholded occurrences over local vicinity
        result_no_collapse_coord = OccurrenceWithinVicinity(vicinity)(
            result_no_collapse_coord)
        new_cube_name = in_vicinity_name_format(
            result_no_collapse_coord.name())
        result_no_collapse_coord.rename(new_cube_name)

    if collapse_coord is None:
        return result_no_collapse_coord

    # Raise warning if result_no_collapse_coord is masked array
    if np.ma.isMaskedArray(result_no_collapse_coord.data):
        warnings.warn("Collapse-coord option not fully tested with "
                      "masked data.")
    # Take a weighted mean across realizations with equal weights
    plugin = WeightAndBlend(collapse_coord, "linear", y0val=1.0, ynval=1.0)

    return plugin(result_no_collapse_coord)
Esempio n. 22
0
def process(cubelist,
            wts_calc_method,
            coordinate,
            cycletime,
            weighting_coord,
            weights_dict=None,
            y0val=None,
            ynval=None,
            cval=None,
            model_id_attr='mosg__model_configuration',
            spatial_weights_from_mask=False,
            fuzzy_length=20000.0):
    """Module to run weighted blending.

    Load in arguments and ensure they are set correctly.
    Then load in the data to blend and calculate weights
    using the method chosen before carrying out the blending.

    Args:
        cubelist (iris.cube.CubeList):
            Cubelist of cubes to be blended.
        wts_calc_method (str):
            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 coordinates.
            "dicts": calculate weights using a dictionary passed in.
        coordinate (str):
            The coordinate over which the blending will be applied.
        cycletime (str):
            The forecast reference time to be used after blending has been
            applied, in the format YYYYMMDDTHHMMZ. If not provided, the
            blended file take the latest available forecast reference time
            from the input cubes supplied.
        weighting_coord (str):
            Name of coordinate over which linear weights should be scaled.
            This coordinate must be available in the weights dictionary.
        weights_dict (dict):
            Dictionary from which to calculate blending weights. Dictionary
            format is as specified in the
            improver.blending.weights.ChoosingWeightsLinear
        y0val (float):
            The relative value of the weighting start point (lowest value of
            blend coord) for choosing default linear weights.
            If used this must be a positive float or 0.
        ynval (float):
            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).
        cval (float):
            Factor used to determine how skewed the non-linear weights will be.
            A value of 1 implies equal weighting.
        model_id_attr (str):
            The name of the cube attribute to be used to identify the source
            model for multi-model blends. Default assume Met Office model
            metadata. Must be present on all if blending over models.
            Default is 'mosg__model_configuration'.
        spatial_weights_from_mask (bool):
            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.
            Default is False.
        fuzzy_length (float):
            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.
            Default is 20000.0.

    Returns:
        iris.cube.Cube:
            Merged and blended Cube.

    Raises:
        RuntimeError:
            If calc_method is linear and cval is not None.
        RuntimeError:
            If calc_method is nonlinear and either y0val and ynval is not None.
        RuntimeError:
            If calc_method is dict and weights_dict is None.
    """

    if (wts_calc_method == "linear") and cval:
        raise RuntimeError('Method: linear does not accept arguments: cval')
    elif (wts_calc_method == "nonlinear") and np.any([y0val, ynval]):
        raise RuntimeError('Method: non-linear does not accept arguments:'
                           ' y0val, ynval')
    elif (wts_calc_method == "dict") and weights_dict is None:
        raise RuntimeError('Dictionary is required if wts_calc_method="dict"')

    plugin = WeightAndBlend(coordinate,
                            wts_calc_method,
                            weighting_coord=weighting_coord,
                            wts_dict=weights_dict,
                            y0val=y0val,
                            ynval=ynval,
                            cval=cval)
    result = plugin.process(cubelist,
                            cycletime=cycletime,
                            model_id_attr=model_id_attr,
                            spatial_weights=spatial_weights_from_mask,
                            fuzzy_length=fuzzy_length)
    return result
Esempio n. 23
0
def process(cube,
            threshold_values=None,
            threshold_dict=None,
            threshold_units=None,
            comparison_operator='>',
            fuzzy_factor=None,
            collapse_coord="None",
            vicinity=None):
    """Module to apply thresholding to a parameter dataset.

    Calculate the threshold truth values of input data relative to the
    provided threshold value. By default data are tested to be above the
    threshold, though the below_threshold boolean enables testing below
    thresholds.
    A fuzzy factor or fuzzy bounds may be provided to capture data that is
    close to the threshold.

    Args:
        cube (iris.cube.Cube):
             A cube to be processed.
        threshold_values (float):
            Threshold value or values about which to calculate the truth
            values; e.g. 270 300. Must be omitted if 'threshold_config'
            is used.
            Default is None.
        threshold_dict (dict):
            Threshold configuration containing threshold values and
            (optionally) fuzzy bounds. Best used in combination with
            'threshold_units' It should contain a dictionary of strings that
            can be interpreted as floats with the structure:
            "THRESHOLD_VALUE": [LOWER_BOUND, UPPER_BOUND]
            e.g: {"280.0": [278.0, 282.0], "290.0": [288.0, 292.0]},
            or with structure
            "THRESHOLD_VALUE": "None" (no fuzzy bounds).
            Repeated thresholds with different bounds are not
            handled well. Only the last duplicate will be used.
            Default is None.
        threshold_units (str):
            Units of the threshold values. If not provided the units are
            assumed to be the same as those of the input cube. Specifying
            the units here will allow a suitable conversion to match
            the input units if possible.
        comparison_operator (str):
            Indicates the comparison_operator to use with the threshold.
            e.g. 'ge' or '>=' to evaluate data >= threshold or '<' to
            evaluate data < threshold. When using fuzzy thresholds, there is
            no difference between < and <= or > and >=.
            Default is >. Valid choices: > >= < <= gt ge lt le.
        fuzzy_factor (float):
            A decimal fraction defining the factor about the threshold value(s)
            which should be treated as fuzzy. Data which fail a test against
            the hard threshold value may return a fractional truth value if
            they fall within this fuzzy factor region.
            Fuzzy factor must be in the range 0-1, with higher values
            indicating a narrower fuzzy factor region / sharper threshold.
            N.B. A fuzzy factor cannot be used with a zero threshold or a
            threshold_dict.
        collapse_coord (str):
            An optional ability to set which coordinate we want to collapse
            over. The default is set to None.
        vicinity (float):
            If True, distance in metres used to define the vicinity within
            which to search for an occurrence.

    Returns:
        iris.cube.Cube:
            processed Cube.

    Raises:
        RuntimeError:
            If threshold_dict and threshold_values are both used.

     Warns:
        warning:
            If collapsing coordinates with a masked array.

    """
    if threshold_dict and threshold_values:
        raise RuntimeError('threshold_dict cannot be used '
                           'with threshold_values')
    if threshold_dict:
        try:
            thresholds = []
            fuzzy_bounds = []
            is_fuzzy = True
            for key in threshold_dict.keys():
                thresholds.append(float(key))
                if is_fuzzy:
                    # If the first threshold has no bounds, fuzzy_bounds is
                    # set to None and subsequent bounds checks are skipped
                    if threshold_dict[key] == "None":
                        is_fuzzy = False
                        fuzzy_bounds = None
                    else:
                        fuzzy_bounds.append(tuple(threshold_dict[key]))
        except ValueError as err:
            # Extend error message with hint for common JSON error.
            raise type(err)(
                "{} in threshold dictionary file. \nHINT: Try adding a zero "
                "after the decimal point.".format(err))
        except Exception as err:
            # Extend any errors with message about WHERE this occurred.
            raise type(err)("{} in dictionary file.".format(err))
    else:
        thresholds = threshold_values
        fuzzy_bounds = None

    result_no_collapse_coord = BasicThreshold(
        thresholds,
        fuzzy_factor=fuzzy_factor,
        fuzzy_bounds=fuzzy_bounds,
        threshold_units=threshold_units,
        comparison_operator=comparison_operator).process(cube)

    if vicinity is not None:
        # smooth thresholded occurrences over local vicinity
        result_no_collapse_coord = OccurrenceWithinVicinity(vicinity).process(
            result_no_collapse_coord)
        new_cube_name = in_vicinity_name_format(
            result_no_collapse_coord.name())
        result_no_collapse_coord.rename(new_cube_name)

    if collapse_coord == "None":
        result = result_no_collapse_coord
    else:
        # Raise warning if result_no_collapse_coord is masked array
        if np.ma.isMaskedArray(result_no_collapse_coord.data):
            warnings.warn("Collapse-coord option not fully tested with "
                          "masked data.")
        # Take a weighted mean across realizations with equal weights
        plugin = WeightAndBlend(collapse_coord, "linear", y0val=1.0, ynval=1.0)
        result_collapse_coord = plugin.process(result_no_collapse_coord)
        result = result_collapse_coord
    return result
Esempio n. 24
0
def process(*cubes: cli.inputcube,
            coordinate,
            weighting_method='linear',
            weighting_coord='forecast_period',
            weighting_config: cli.inputjson = None,
            attributes_config: cli.inputjson = None,
            cycletime: str = None,
            y0val: float = None,
            ynval: float = None,
            cval: float = None,
            model_id_attr: str = None,
            spatial_weights_from_mask=False,
            fuzzy_length=20000.0):
    """Runs weighted blending.

    Check for inconsistent arguments, then calculate a weighted blend
    of input cube data using the options specified.

    Args:
        cubes (iris.cube.CubeList):
            Cubelist of cubes to be blended.
        coordinate (str):
            The coordinate over which the blending will be applied.
        weighting_method (str):
            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 coordinates.
            "dict": calculate weights using a dictionary passed in.
        weighting_coord (str):
            Name of coordinate over which linear weights should be scaled.
            This coordinate must be available in the weights dictionary.
        weighting_config (dict or None):
            Dictionary from which to calculate blending weights. Dictionary
            format is as specified in
            improver.blending.weights.ChoosingWeightsLinear
        attributes_config (dict):
            Dictionary describing required changes to attributes after blending
        cycletime (str):
            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.
        y0val (float):
            The relative value of the weighting start point (lowest value of
            blend coord) for choosing default linear weights.
            If used this must be a positive float or 0.
        ynval (float):
            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).
        cval (float):
            Factor used to determine how skewed the non-linear weights will be.
            A value of 1 implies equal weighting.
        model_id_attr (str):
            The name of the dataset attribute to be used to identify the source
            model when blending data from different models.
        spatial_weights_from_mask (bool):
            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 (float):
            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.

    Returns:
        iris.cube.Cube:
            Merged and blended Cube.

    Raises:
        RuntimeError:
            If calc_method is linear and cval is not None.
        RuntimeError:
            If calc_method is nonlinear and either y0val and ynval is not None.
        RuntimeError:
            If calc_method is dict and weights_dict is None.
    """
    from improver.blending.calculate_weights_and_blend import WeightAndBlend

    if (weighting_method == "linear") and cval:
        raise RuntimeError('Method: linear does not accept arguments: cval')
    if (weighting_method == "nonlinear") and any([y0val, ynval]):
        raise RuntimeError('Method: non-linear does not accept arguments:'
                           ' y0val, ynval')
    if (weighting_method == "dict") and weighting_config is None:
        raise RuntimeError('Dictionary is required if wts_calc_method="dict"')
    if "model" in coordinate and model_id_attr is None:
        raise RuntimeError('model_id_attr must be specified for '
                           'model blending')

    plugin = WeightAndBlend(coordinate,
                            weighting_method,
                            weighting_coord=weighting_coord,
                            wts_dict=weighting_config,
                            y0val=y0val,
                            ynval=ynval,
                            cval=cval)

    return plugin(cubes,
                  cycletime=cycletime,
                  model_id_attr=model_id_attr,
                  spatial_weights=spatial_weights_from_mask,
                  fuzzy_length=fuzzy_length,
                  attributes_dict=attributes_config)
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('--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('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 available in the '
                          'weights dictionary.')

    args = parser.parse_args(args=argv)

    # reject incorrect argument combinations
    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"')

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

    if args.wts_calc_method == "dict":
        with open(args.wts_dict, 'r') as wts:
            weights_dict = json.load(wts)
    else:
        weights_dict = None

    plugin = WeightAndBlend(
        args.coordinate, args.wts_calc_method,
        weighting_coord=args.weighting_coord, wts_dict=weights_dict,
        y0val=args.y0val, ynval=args.ynval, cval=args.cval)
    result = plugin.process(
        cubelist, cycletime=args.cycletime,
        model_id_attr=args.model_id_attr,
        spatial_weights=args.spatial_weights_from_mask,
        fuzzy_length=args.fuzzy_length)

    save_netcdf(result, args.output_filepath)