Пример #1
0
 def test_less_than(self):
     """ Tests the case where spp__relative_threshold is less_than"""
     cube = set_up_probability_cube(
         self.data, self.threshold_points, spp__relative_to_threshold="less_than"
     )
     result = probability_is_above_or_below(cube)
     self.assertEqual(result, "below")
Пример #2
0
    def _get_multiplier(self) -> float:
        """
        Check whether the cube contains "above" or "below" threshold
        probabilities.  For "above", the probability of occurrence between
        thresholds is the difference between probabilities at the lower
        and higher thresholds: P(lower) - P(higher).  For "below" it is the
        inverse of this: P(higher) - P(lower), which is implemented by
        multiplying the difference by -1.

        Returns:
            1. or -1.

        Raises:
            ValueError: If the spp__relative_to_threshold attribute is
                not recognised
        """
        relative_to_threshold = probability_is_above_or_below(self.cube)
        if relative_to_threshold == "above":
            multiplier = 1.0
        elif relative_to_threshold == "below":
            multiplier = -1.0
        else:
            raise ValueError("Input cube must contain probabilities of "
                             "occurrence above or below threshold")
        return multiplier
Пример #3
0
 def test_incorrect_attribute(self):
     """Tests it returns None if the spp__relative_to_threshold
     attribute has an invalid value."""
     cube = set_up_probability_cube(self.data, self.threshold_points,)
     cube.coord("air_temperature").attributes = {
         "spp__relative_to_threshold": "higher"
     }
     result = probability_is_above_or_below(cube)
     self.assertEqual(result, None)
Пример #4
0
 def test_no_spp__relative_to_threshold(self):
     """Tests it returns None if there is no spp__relative_to_threshold
     attribute."""
     cube = set_up_probability_cube(self.data, self.threshold_points,)
     cube.coord("air_temperature").attributes = {
         "relative_to_threshold": "greater_than"
     }
     result = probability_is_above_or_below(cube)
     self.assertEqual(result, None)
Пример #5
0
 def test_greater_than_or_equal_to(self):
     """ Tests the case where spp__relative_threshold is
     greater_than_or_equal_to"""
     cube = set_up_probability_cube(
         self.data,
         self.threshold_points,
         spp__relative_to_threshold="greater_than_or_equal_to",
     )
     result = probability_is_above_or_below(cube)
     self.assertEqual(result, "above")
Пример #6
0
    def _update_metadata(self, cube: Cube) -> None:
        """Rename the cube and add attributes to the threshold coordinate
        after merging
        """
        threshold_coord = cube.coord(self.threshold_coord_name)
        threshold_coord.attributes.update(
            {"spp__relative_to_threshold": self.comparison_operator.spp_string}
        )
        if cube.cell_methods:
            format_cell_methods_for_probability(cube, self.threshold_coord_name)

        cube.rename(
            "probability_of_{parameter}_{relative_to}_threshold".format(
                parameter=self.threshold_coord_name,
                relative_to=probability_is_above_or_below(cube),
            )
        )
        cube.units = Unit(1)
Пример #7
0
    def _update_metadata(self, output_cube, original_units):
        """
        Update output cube name and threshold coordinate

        Args:
            output_cube (iris.cube.Cube):
                Cube containing new "between_thresholds" probabilities
            original_units (str):
                Required threshold-type coordinate units
        """
        new_name = self.cube.name().replace(
            "{}_threshold".format(probability_is_above_or_below(self.cube)),
            "between_thresholds",
        )
        output_cube.rename(new_name)

        new_thresh_coord = output_cube.coord(self.thresh_coord.name())
        new_thresh_coord.convert_units(original_units)
        new_thresh_coord.attributes[
            "spp__relative_to_threshold"] = "between_thresholds"
Пример #8
0
    def check_input_cubes(self, cubes: CubeList) -> Optional[Dict[str, Any]]:
        """
        Check that the input cubes contain all the diagnostics and thresholds
        required by the decision tree.  Sets self.coord_named_threshold to
        "True" if threshold-type coordinates have the name "threshold" (as
        opposed to the standard name of the diagnostic), for backward
        compatibility.

        Args:
            cubes:
                A CubeList containing the input diagnostic cubes.

        Returns:
            A dictionary of (keyword) nodes names where the diagnostic
            data is missing and (values) node associated with
            diagnostic_missing_action.

        Raises:
            IOError:
                Raises an IOError if any of the required input data is missing.
                The error includes details of which fields are missing.
        """
        optional_node_data_missing = {}
        missing_data = []
        for key, query in self.queries.items():
            diagnostics = get_parameter_names(
                expand_nested_lists(query, "diagnostic_fields")
            )
            thresholds = expand_nested_lists(query, "diagnostic_thresholds")
            conditions = expand_nested_lists(query, "diagnostic_conditions")
            for diagnostic, threshold, condition in zip(
                diagnostics, thresholds, conditions
            ):

                # First we check the diagnostic name and units, performing
                # a conversion is required and possible.
                test_condition = iris.Constraint(name=diagnostic)
                matched_cube = cubes.extract(test_condition)
                if not matched_cube:
                    if "diagnostic_missing_action" in query:
                        optional_node_data_missing.update(
                            {key: query[query["diagnostic_missing_action"]]}
                        )
                    else:
                        missing_data.append([diagnostic, threshold, condition])
                    continue

                cube_threshold_units = find_threshold_coordinate(matched_cube[0]).units
                threshold.convert_units(cube_threshold_units)

                # Then we check if the required threshold is present in the
                # cube, and that the thresholding is relative to it correctly.
                threshold = threshold.points.item()
                threshold_name = find_threshold_coordinate(matched_cube[0]).name()

                # Set flag to check for old threshold coordinate names
                if threshold_name == "threshold" and not self.coord_named_threshold:
                    self.coord_named_threshold = True

                # Check threshold == 0.0
                if abs(threshold) < self.float_abs_tolerance:
                    coord_constraint = {
                        threshold_name: lambda cell: np.isclose(
                            cell.point, 0, rtol=0, atol=self.float_abs_tolerance
                        )
                    }
                else:
                    coord_constraint = {
                        threshold_name: lambda cell: np.isclose(
                            cell.point, threshold, rtol=self.float_tolerance, atol=0
                        )
                    }

                # Checks whether the spp__relative_to_threshold attribute is above
                # or below a threshold and and compares to the diagnostic_condition.
                test_condition = iris.Constraint(
                    coord_values=coord_constraint,
                    cube_func=lambda cube: (
                        probability_is_above_or_below(cube) == condition
                    ),
                )
                matched_threshold = matched_cube.extract(test_condition)
                if not matched_threshold:
                    missing_data.append([diagnostic, threshold, condition])

        if missing_data:
            msg = (
                "Weather Symbols input cubes are missing"
                " the following required"
                " input fields:\n"
            )
            dyn_msg = "name: {}, threshold: {}, " "spp__relative_to_threshold: {}\n"
            for item in missing_data:
                msg = msg + dyn_msg.format(*item)
            raise IOError(msg)

        if not optional_node_data_missing:
            optional_node_data_missing = None
        return optional_node_data_missing
Пример #9
0
def test_leading_dimension(cube_type, spp__relative_to_threshold):
    """ Tests cube generated with leading dimension specified using percentile and
    probability flags, and different values for spp__relative_to_threshold """
    if cube_type == "other":
        # Tests that error is raised when cube type isn't supported
        msg = ("Cube type {} not supported. "
               'Specify one of "variable", "percentile" or "probability".'
               ).format(cube_type)

        with pytest.raises(ValueError, match=msg):
            generate_metadata(
                MANDATORY_ATTRIBUTE_DEFAULTS,
                cube_type=cube_type,
                spp__relative_to_threshold=spp__relative_to_threshold,
            )
    else:
        leading_dimension = [10, 20, 30, 40, 50, 60, 70, 80]

        if spp__relative_to_threshold is not None:
            cube = generate_metadata(
                MANDATORY_ATTRIBUTE_DEFAULTS,
                leading_dimension=leading_dimension,
                cube_type=cube_type,
                spp__relative_to_threshold=spp__relative_to_threshold,
            )
        else:
            cube = generate_metadata(
                MANDATORY_ATTRIBUTE_DEFAULTS,
                leading_dimension=leading_dimension,
                cube_type=cube_type,
            )
            spp__relative_to_threshold = RELATIVE_TO_THRESHOLD_DEFAULT

        if cube_type == "percentile":
            cube_name = NAME_DEFAULT
            coord_name = "percentile"
        elif cube_type == "probability":
            cube_name = "probability_of_{}_{}_threshold".format(
                NAME_DEFAULT, probability_is_above_or_below(cube))
            coord_name = NAME_DEFAULT

            assert cube.coord(coord_name).attributes == {
                "spp__relative_to_threshold": spp__relative_to_threshold
            }
            cube.coord(coord_name).attributes = {}
        else:
            cube_name = NAME_DEFAULT
            coord_name = "realization"

        assert cube.name() == cube_name
        assert cube.ndim == 3
        assert cube.shape == (len(leading_dimension), NPOINTS_DEFAULT,
                              NPOINTS_DEFAULT)
        assert cube.coords()[0].name() == coord_name
        np.testing.assert_array_equal(
            cube.coord(coord_name).points, leading_dimension)

        # Assert that no other values have unexpectedly changed by returning changed
        # values to defaults and comparing against default cube
        default_cube = generate_metadata(MANDATORY_ATTRIBUTE_DEFAULTS)

        cube.coord(coord_name).points = default_cube.coord(
            "realization").points
        cube.coord(coord_name).rename("realization")
        cube.coord("realization").units = "1"
        cube.rename(default_cube.standard_name)
        cube.units = default_cube.units

        assert cube == default_cube
Пример #10
0
    def process(self, input_cube):
        """Convert each point to a truth value based on provided threshold
        values. The truth value may or may not be fuzzy depending upon if
        fuzzy_bounds are supplied.  If the plugin has a "threshold_units"
        member, this is used to convert both thresholds and fuzzy bounds into
        the units of the input cube.

        Args:
            input_cube (iris.cube.Cube):
                Cube to threshold. The code is dimension-agnostic.

        Returns:
            iris.cube.Cube:
                Cube after a threshold has been applied. The data within this
                cube will contain values between 0 and 1 to indicate whether
                a given threshold has been exceeded or not.

                The cube meta-data will contain:
                * Input_cube name prepended with
                probability_of_X_above(or below)_threshold (where X is
                the diagnostic under consideration)
                * Threshold dimension coordinate with same units as input_cube
                * Threshold attribute ("greater_than",
                "greater_than_or_equal_to", "less_than", or
                less_than_or_equal_to" depending on the operator)
                * Cube units set to (1).

        Raises:
            ValueError: if a np.nan value is detected within the input cube.

        """
        # Record input cube data type to ensure consistent output, though
        # integer data must become float to enable fuzzy thresholding.
        input_cube_dtype = input_cube.dtype
        if input_cube.dtype.kind == "i":
            input_cube_dtype = FLOAT_DTYPE

        thresholded_cubes = iris.cube.CubeList()
        if np.isnan(input_cube.data).any():
            raise ValueError("Error: NaN detected in input cube data")

        # if necessary, convert thresholds and fuzzy bounds into cube units
        if self.threshold_units is not None:
            self.thresholds = [
                self.threshold_units.convert(threshold, input_cube.units)
                for threshold in self.thresholds
            ]
            self.fuzzy_bounds = [
                tuple([
                    self.threshold_units.convert(threshold, input_cube.units)
                    for threshold in bounds
                ]) for bounds in self.fuzzy_bounds
            ]

        # set name of threshold coordinate to match input diagnostic
        self.threshold_coord_name = input_cube.name()

        # apply fuzzy thresholding
        for threshold, bounds in zip(self.thresholds, self.fuzzy_bounds):
            cube = input_cube.copy()
            # if upper and lower bounds are equal, set a deterministic 0/1
            # probability based on exceedance of the threshold
            if bounds[0] == bounds[1]:
                truth_value = self.comparison_operator["function"](cube.data,
                                                                   threshold)
            # otherwise, scale exceedance probabilities linearly between 0/1
            # at the min/max fuzzy bounds and 0.5 at the threshold value
            else:
                truth_value = np.where(
                    cube.data < threshold,
                    rescale(
                        cube.data,
                        data_range=(bounds[0], threshold),
                        scale_range=(0.0, 0.5),
                        clip=True,
                    ),
                    rescale(
                        cube.data,
                        data_range=(threshold, bounds[1]),
                        scale_range=(0.5, 1.0),
                        clip=True,
                    ),
                )
                # if requirement is for probabilities less_than or
                # less_than_or_equal_to the threshold (rather than
                # greater_than or greater_than_or_equal_to), invert
                # the exceedance probability
                if "less_than" in self.comparison_operator["spp_string"]:
                    truth_value = 1.0 - truth_value
            truth_value = np.ma.masked_where(np.ma.getmask(cube.data),
                                             truth_value)
            truth_value = truth_value.astype(input_cube_dtype)

            cube.data = truth_value
            # Overwrite masked values that have been thresholded
            # with the un-thresholded values from the input cube.
            if np.ma.is_masked(cube.data):
                cube.data[input_cube.data.mask] = input_cube.data[
                    input_cube.data.mask]
            cube = self._add_threshold_coord(cube, threshold)

            for func in self.each_threshold_func:
                cube = func(cube)

            thresholded_cubes.append(cube)

        (cube, ) = thresholded_cubes.concatenate()
        if len(self.thresholds) == 1:
            # if only one threshold has been provided, this should be scalar
            cube = next(cube.slices_over(cube.coord(var_name="threshold")))

        cube.rename("probability_of_{}_{}_threshold".format(
            cube.name(), probability_is_above_or_below(cube)))
        cube.units = Unit(1)

        enforce_coordinate_ordering(cube, ["realization", "percentile"])

        return cube