Exemple #1
0
    def find_coord_names(self, cube: Cube) -> Tuple[str, str, str, str]:
        """Extract x, y, z, and time coordinate names.

        Args:
            cube:
                some iris cube to find coordinate names from

        Returns:
            - name of the axis name in x-direction
            - name of the axis name in y-direction
            - name of the axis name in z-direction
            - name of the axis name in t-direction
        """
        clist = {cube.coords()[i].name() for i in range(len(cube.coords()))}
        try:
            xname = cube.coord(axis="x").name()
        except CoordinateNotFoundError as exc:
            print("'{0}' while xname setting. Args: {1}.".format(
                exc, exc.args))
        try:
            yname = cube.coord(axis="y").name()
        except CoordinateNotFoundError as exc:
            print("'{0}' while yname setting. Args: {1}.".format(
                exc, exc.args))
        if clist.intersection(self.zcoordnames):
            zname = list(clist.intersection(self.zcoordnames))[0]
        else:
            zname = None

        if clist.intersection(self.tcoordnames):
            tname = list(clist.intersection(self.tcoordnames))[0]
        else:
            tname = None
        return xname, yname, zname, tname
Exemple #2
0
def check_for_x_and_y_axes(cube: Cube,
                           require_dim_coords: bool = False) -> None:
    """
    Check whether the cube has an x and y axis, otherwise raise an error.

    Args:
        cube:
            Cube to be checked for x and y axes.
        require_dim_coords:
            If true the x and y coordinates must be dimension coordinates.

    Raises:
        ValueError : Raise an error if non-uniform increments exist between
                      grid points.
    """
    for axis in ["x", "y"]:
        if require_dim_coords:
            coord = cube.coords(axis=axis, dim_coords=True)
        else:
            coord = cube.coords(axis=axis)

        if coord:
            pass
        else:
            msg = "The cube does not contain the expected {}" "coordinates.".format(
                axis)
            raise ValueError(msg)
Exemple #3
0
    def check_probability_cube_metadata(self, cube: Cube) -> None:
        """Checks probability-specific metadata"""
        if cube.units != "1":
            self.errors.append(
                f"Expected units of 1 on probability data, got {cube.units}")

        try:
            self.diagnostic = get_diagnostic_cube_name_from_probability_name(
                cube.name())
        except ValueError as cause:
            # if the probability name is not valid
            self.errors.append(str(cause))

        expected_threshold_name = get_threshold_coord_name_from_probability_name(
            cube.name())

        if not cube.coords(expected_threshold_name):
            msg = f"Cube does not have expected threshold coord '{expected_threshold_name}'; "
            try:
                threshold_name = find_threshold_coordinate(cube).name()
            except CoordinateNotFoundError:
                coords = [coord.name() for coord in cube.coords()]
                msg += (
                    f"no coord with var_name='threshold' found in all coords: {coords}"
                )
                self.errors.append(msg)
            else:
                msg += f"threshold coord has incorrect name '{threshold_name}'"
                self.errors.append(msg)
                self.check_threshold_coordinate_properties(
                    cube.name(), cube.coord(threshold_name))
        else:
            threshold_coord = cube.coord(expected_threshold_name)
            self.check_threshold_coordinate_properties(cube.name(),
                                                       threshold_coord)
Exemple #4
0
def _create_cube_with_padded_data(source_cube: Cube, data: ndarray,
                                  coord_x: DimCoord,
                                  coord_y: DimCoord) -> Cube:
    """
    Create a cube with newly created data where the metadata is copied from
    the input cube and the supplied x and y coordinates are added to the
    cube.

    Args:
        source_cube:
            Template cube used for copying metadata and non x and y axes
            coordinates.
        data:
            Data to be put into the new cube.
        coord_x:
            Coordinate to be added to the new cube to represent the x axis.
        coord_y:
            Coordinate to be added to the new cube to represent the y axis.

    Returns:
        Cube built from the template cube using the requested data and
        the supplied x and y axis coordinates.
    """
    check_for_x_and_y_axes(source_cube)

    yname = source_cube.coord(axis="y").name()
    xname = source_cube.coord(axis="x").name()
    ycoord_dim = source_cube.coord_dims(yname)
    xcoord_dim = source_cube.coord_dims(xname)

    # inherit metadata (cube name, units, attributes etc)
    metadata_dict = deepcopy(source_cube.metadata._asdict())
    new_cube = iris.cube.Cube(data, **metadata_dict)

    # inherit non-spatial coordinates
    for coord in source_cube.coords():
        if coord.name() not in [yname, xname]:
            if source_cube.coords(coord, dim_coords=True):
                coord_dim = source_cube.coord_dims(coord)
                new_cube.add_dim_coord(coord, coord_dim)
            else:
                new_cube.add_aux_coord(coord)

    # update spatial coordinates
    if len(xcoord_dim) > 0:
        new_cube.add_dim_coord(coord_x, xcoord_dim)
    else:
        new_cube.add_aux_coord(coord_x)

    if len(ycoord_dim) > 0:
        new_cube.add_dim_coord(coord_y, ycoord_dim)
    else:
        new_cube.add_aux_coord(coord_y)

    return new_cube
Exemple #5
0
def forecast_period_coord(
    cube: Cube, force_lead_time_calculation: bool = False
) -> Coord:
    """
    Return the lead time coordinate (forecast_period) from a cube, either by
    reading an existing forecast_period coordinate, or by calculating the
    difference between time and forecast_reference_time.

    Args:
        cube:
            Cube from which the lead times will be determined.
        force_lead_time_calculation:
            Force the lead time to be calculated from the
            forecast_reference_time and the time coordinate, even if
            the forecast_period coordinate exists. Default is False.

    Returns:
        New forecast_period coord. A DimCoord is returned if the
        forecast_period coord is already present in the cube as a
        DimCoord and this coord does not need changing, otherwise
        it will be an AuxCoord.
    """
    create_dim_coord = False
    if cube.coords("forecast_period"):
        if isinstance(cube.coord("forecast_period"), DimCoord):
            create_dim_coord = True

    if cube.coords("forecast_period") and not force_lead_time_calculation:
        result_coord = cube.coord("forecast_period").copy()

    elif cube.coords("time") and cube.coords("forecast_reference_time"):
        # Cube must adhere to mandatory standards for safe time calculations
        check_mandatory_standards(cube)
        # Try to calculate forecast period from forecast reference time and
        # time coordinates
        result_coord = _calculate_forecast_period(
            cube.coord("time"),
            cube.coord("forecast_reference_time"),
            dim_coord=create_dim_coord,
        )

    else:
        msg = (
            "The forecast period coordinate is not available within {}."
            "The time coordinate and forecast_reference_time "
            "coordinate were also not available for calculating "
            "the forecast_period.".format(cube)
        )
        raise CoordinateNotFoundError(msg)

    return result_coord
Exemple #6
0
def get_coords_to_remove(cube: Cube, blend_coord: str) -> Optional[List[str]]:
    """
    Generate a list of coordinate names associated with the blend
    dimension.  Unless these are time-related coordinates, they should be
    removed after blending.

    Args:
        cube:
            Cube to be blended
        blend_coord:
            Name of coordinate over which the blend will be performed

    Returns:
        List of names of coordinates to remove
    """
    try:
        (blend_dim, ) = cube.coord_dims(blend_coord)
    except ValueError:
        # occurs if the blend coordinate is scalar
        if blend_coord == MODEL_BLEND_COORD:
            return [MODEL_BLEND_COORD, MODEL_NAME_COORD]
        return None

    crds_to_remove = []
    for coord in cube.coords():
        if coord.name() in TIME_COORDS:
            continue
        if blend_dim in cube.coord_dims(coord):
            crds_to_remove.append(coord.name())
    return crds_to_remove
Exemple #7
0
def _set_blended_time_coords(blended_cube: Cube,
                             cycletime: Optional[str]) -> None:
    """
    For cycle and model blending:
    - Add a "blend_time" coordinate equal to the current cycletime
    - Update the forecast reference time and forecast period coordinate points
    to reflect the current cycle time (behaviour is DEPRECATED)
    - Remove any bounds from the forecast reference time (behaviour is DEPRECATED)
    - Mark the forecast reference time and forecast period as DEPRECATED

    Modifies cube in place.

    Args:
        blended_cube
        cycletime:
            Current cycletime in YYYYMMDDTHHmmZ format
    """
    try:
        cycletime_point = _get_cycletime_point(blended_cube, cycletime)
    except TypeError:
        raise ValueError(
            "Current cycle time is required for cycle and model blending")

    add_blend_time(blended_cube, cycletime)
    blended_cube.coord("forecast_reference_time").points = [cycletime_point]
    blended_cube.coord("forecast_reference_time").bounds = None
    if blended_cube.coords("forecast_period"):
        blended_cube.remove_coord("forecast_period")
    new_forecast_period = forecast_period_coord(blended_cube)
    time_dim = blended_cube.coord_dims("time")
    blended_cube.add_aux_coord(new_forecast_period, data_dims=time_dim)
    for coord in ["forecast_period", "forecast_reference_time"]:
        msg = f"{coord} will be removed in future and should not be used"
        blended_cube.coord(coord).attributes.update(
            {"deprecation_message": msg})
    def enforce_time_coords_dtype(cube: Cube) -> Cube:
        """
        Enforce the data type of the time, forecast_reference_time and
        forecast_period within the cube, so that time coordinates do not
        become mis-represented. The units of the time and
        forecast_reference_time are enforced to be
        "seconds since 1970-01-01 00:00:00" with a datatype of int64.
        The units of forecast_period are enforced to be seconds with a datatype
        of int32. This functions modifies the cube in-place.

        Args:
            cube:
                The cube that will have the datatype and units for the
                time, forecast_reference_time and forecast_period coordinates
                enforced.

        Returns:
            Cube where the datatype and units for the
            time, forecast_reference_time and forecast_period coordinates
            have been enforced.
        """
        for coord_name in [
                "time", "forecast_reference_time", "forecast_period"
        ]:
            coord_spec = TIME_COORDS[coord_name]
            if cube.coords(coord_name):
                coord = cube.coord(coord_name)
                coord.convert_units(coord_spec.units)
                coord.points = round_close(coord.points,
                                           dtype=coord_spec.dtype)
                if hasattr(coord, "bounds") and coord.bounds is not None:
                    coord.bounds = round_close(coord.bounds,
                                               dtype=coord_spec.dtype)
        return cube
Exemple #9
0
    def check_input_cube_dims(self, input_cube: Cube, timezone_cube: Cube) -> None:
        """Ensures input cube has at least three dimensions: time, y, x. Promotes time
        to be the inner-most dimension (dim=-1). Does the same for the timezone_cube
        UTC_offset dimension.

        Raises:
            ValueError:
                If the input cube does not have exactly the expected three coords.
                If the spatial coords on input_cube and timezone_cube do not match.
        """
        expected_coords = ["time"] + [input_cube.coord(axis=n).name() for n in "yx"]
        cube_coords = [coord.name() for coord in input_cube.coords(dim_coords=True)]
        if not all(
            [expected_coord in cube_coords for expected_coord in expected_coords]
        ):
            raise ValueError(
                f"Expected coords on input_cube: time, y, x ({expected_coords})."
                f"Found {cube_coords}"
            )
        enforce_coordinate_ordering(input_cube, ["time"], anchor_start=False)
        self.timezone_cube = timezone_cube.copy()
        enforce_coordinate_ordering(
            self.timezone_cube, ["UTC_offset"], anchor_start=False
        )
        if not spatial_coords_match([input_cube, self.timezone_cube]):
            raise ValueError(
                "Spatial coordinates on input_cube and timezone_cube do not match."
            )
Exemple #10
0
    def _get_advection_time(cube1: Cube, cube2: Cube) -> None:
        """Get time over which the advection has occurred, in seconds, using the
        difference in time or forecast reference time between input cubes"""
        time_diff_seconds = (
            cube2.coord("time").cell(0).point -
            cube1.coord("time").cell(0).point).total_seconds()
        time_diff_seconds = int(time_diff_seconds)

        if time_diff_seconds == 0:
            # second cube should be an observation; first cube should have a
            # non-zero forecast period which describes the advection time
            if (cube2.coords("forecast_period")
                    and cube2.coord("forecast_period").points[0] != 0):
                raise InvalidCubeError(
                    "Second input cube must be a current observation")

            # get the time difference from the first cube's forecast period
            fp_coord = cube1.coord("forecast_period").copy()
            fp_coord.convert_units("seconds")
            (time_diff_seconds, ) = fp_coord.points

        if time_diff_seconds <= 0:
            error_msg = "Expected positive time difference cube2 - cube1: got {} s"
            raise InvalidCubeError(error_msg.format(time_diff_seconds))

        return time_diff_seconds
    def _create_template_cube(self, cube: Cube) -> Cube:
        """
        Create a template cube to store the timezone masks. This cube has only
        one scalar coordinate which is time, denoting when it is valid; this is
        only relevant if using daylight savings. The attribute
        includes_daylight_savings is set to indicate this.

        Args:
            cube:
                A cube with the desired grid from which coordinates are taken
                for inclusion in the template.

        Returns:
            A template cube in which each timezone mask can be stored.
        """
        time_point = np.array(self.time.timestamp(), dtype=np.int64)
        time_coord = iris.coords.DimCoord(
            time_point,
            "time",
            units=Unit("seconds since 1970-01-01 00:00:00", calendar="gregorian"),
        )

        for crd in cube.coords(dim_coords=False):
            cube.remove_coord(crd)
        cube.add_aux_coord(time_coord)

        attributes = generate_mandatory_attributes([cube])
        attributes["includes_daylight_savings"] = str(self.include_dst)

        return create_new_diagnostic_cube(
            "timezone_mask", 1, cube, attributes, dtype=np.int8
        )
def metadata_ok(updraught: Cube, baseline: Cube, model_id_attr=None) -> None:
    """
    Checks updraught Cube long_name, units and dtype are as expected.
    Compares updraught Cube with baseline to make sure everything else matches.

    Args:
        updraught: Result of VerticalUpdraught plugin
        baseline: A Precip or similar cube with the same coordinates and attributes.

    Raises:
        AssertionError: If anything doesn't match
    """
    assert updraught.long_name == "maximum_vertical_updraught"
    assert updraught.units == "m s-1"
    assert updraught.dtype == np.float32
    for coord in updraught.coords():
        base_coord = baseline.coord(coord.name())
        assert updraught.coord_dims(coord) == baseline.coord_dims(base_coord)
        assert coord == base_coord
    for attr in MANDATORY_ATTRIBUTES:
        assert updraught.attributes[attr] == baseline.attributes[attr]
    all_attr_keys = list(updraught.attributes.keys())
    if model_id_attr:
        assert updraught.attributes[model_id_attr] == baseline.attributes[
            model_id_attr]
        mandatory_attr_keys = [k for k in all_attr_keys if k != model_id_attr]
    else:
        mandatory_attr_keys = all_attr_keys
    assert sorted(mandatory_attr_keys) == sorted(MANDATORY_ATTRIBUTES)
Exemple #13
0
def check_data_sufficiency(
    historic_forecasts: Cube,
    truths: Cube,
    point_by_point: bool,
    proportion_of_nans: float,
):
    """Check whether there is sufficient valid data (i.e. values that are not NaN)
    within the historic forecasts and truths, in order to robustly compute EMOS
    coefficients.

    Args:
        historic_forecasts:
            Cube containing historic forcasts.
        truths:
            Cube containing truths.
        point_by_point:
            If True, coefficients are calculated independently for each
            point within the input cube by creating an initial guess and
            minimising each grid point independently.
        proportion_of_nans:
            The proportion of the matching historic forecast-truth pairs that
            are allowed to be NaN.

    Raises:
        ValueError: If the proportion of NaNs is higher than allowable for a site,
            if using point_by_point.
        ValueError: If the proportion of NaNs is higher than allowable when
            considering all sites.
    """
    if not historic_forecasts.coords("wmo_id"):
        return

    truths_data = np.broadcast_to(truths.data, historic_forecasts.shape)
    index = np.isnan(historic_forecasts.data) & np.isnan(truths_data)

    if point_by_point:
        wmo_id_axis = historic_forecasts.coord_dims("wmo_id")[0]
        non_wmo_id_axes = list(range(len(historic_forecasts.shape)))
        non_wmo_id_axes.pop(wmo_id_axis)
        detected_proportion = np.count_nonzero(
            index, axis=tuple(non_wmo_id_axes)) / np.prod(
                np.array(index.shape)[non_wmo_id_axes])
        if np.any(detected_proportion > proportion_of_nans):
            number_of_sites = np.sum(detected_proportion > proportion_of_nans)
            msg = (
                f"{number_of_sites} sites have a proportion of NaNs that is "
                f"higher than the allowable proportion of NaNs within the "
                "historic forecasts and truth pairs. The allowable proportion is "
                f"{proportion_of_nans}. The maximum proportion of NaNs is "
                f"{np.amax(detected_proportion)}.")
            raise ValueError(msg)
    else:
        detected_proportion = np.count_nonzero(index) / index.size
        if detected_proportion > proportion_of_nans:
            msg = (
                f"The proportion of NaNs detected is {detected_proportion}. "
                f"This is higher than the allowable proportion of NaNs within the "
                f"historic forecasts and truth pairs: {proportion_of_nans}.")
            raise ValueError(msg)
Exemple #14
0
def enforce_coordinate_ordering(cube: Cube,
                                coord_names: Union[List[str], str],
                                anchor_start: bool = True) -> None:
    """
    Function to reorder dimensions within a cube.
    Note that the input cube is modified in place.

    Args:
        cube:
            Cube where the ordering will be enforced to match the order within
            the coord_names. This input cube will be modified as part of this
            function.
        coord_names:
            List of the names of the coordinates to order. If a string is
            passed in, only the single specified coordinate is reordered.
        anchor_start:
            Define whether the specified coordinates should be moved to the
            start (True) or end (False) of the list of dimensions. If True, the
            coordinates are inserted as the first dimensions in the order in
            which they are provided. If False, the coordinates are moved to the
            end. For example, if the specified coordinate names are
            ["time", "realization"] then "realization" will be the last
            coordinate within the cube, whilst "time" will be the last but one.
    """
    if isinstance(coord_names, str):
        coord_names = [coord_names]

    # construct a list of dimensions on the cube to be reordered
    dim_coord_names = get_dim_coord_names(cube)
    coords_to_reorder = []
    for coord in coord_names:
        if coord == "threshold":
            try:
                coord = find_threshold_coordinate(cube).name()
            except CoordinateNotFoundError:
                continue
        if coord in dim_coord_names:
            coords_to_reorder.append(coord)

    original_coords = cube.coords(dim_coords=True)
    coord_dims = cube.coord_dims

    # construct list of reordered dimensions assuming start anchor
    new_dims = [coord_dims(coord)[0] for coord in coords_to_reorder]
    new_dims.extend([
        coord_dims(coord)[0] for coord in original_coords
        if coord_dims(coord)[0] not in new_dims
    ])

    # if anchor is end, reshuffle the list
    if not anchor_start:
        new_dims_end = new_dims[len(coords_to_reorder):]
        new_dims_end.extend(new_dims[:len(coords_to_reorder)])
        new_dims = new_dims_end

    # transpose cube using new coordinate order
    if new_dims != sorted(new_dims):
        cube.transpose(new_dims)
Exemple #15
0
class Test_convert_number_of_grid_cells_into_distance(IrisTest):

    """Test the convert_number_of_grid_cells_into_distance method"""

    def setUp(self):
        """Set up a cube with x and y coordinates"""
        data = np.ones((3, 4))
        self.cube = Cube(data, standard_name="air_temperature",)
        self.cube.add_dim_coord(
            DimCoord(np.linspace(2000.0, 6000.0, 3),
                     'projection_x_coordinate', units='m'), 0)
        self.cube.add_dim_coord(
            DimCoord(np.linspace(2000.0, 8000.0, 4),
                     "projection_y_coordinate", units='m'), 1)

    def test_basic(self):
        """Test the function does what it's meant to in a simple case."""
        result_radius = convert_number_of_grid_cells_into_distance(
            self.cube, 2)
        expected_result = 4000.0
        self.assertAlmostEqual(result_radius, expected_result)
        self.assertIs(type(expected_result), float)

    def test_check_input_in_km(self):
        """
        Test that the output is still in metres when the input coordinates
        are in a different unit.
        """
        result_radius = convert_number_of_grid_cells_into_distance(
            self.cube, 2)
        for coord in self.cube.coords():
            coord.convert_units("km")
        expected_result = 4000.0
        self.assertAlmostEqual(result_radius, expected_result)
        self.assertIs(type(expected_result), float)

    def test_not_equal_areas(self):
        """
        Check it raises an error when the input is not an equal areas grid.
        """

        self.cube.remove_coord("projection_x_coordinate")
        self.cube.add_dim_coord(
            DimCoord(np.linspace(200.0, 600.0, 3),
                     'projection_x_coordinate', units='m'), 0)
        with self.assertRaisesRegex(
                ValueError,
                "The size of the intervals along the x and y axis"
                " should be equal."):
            convert_number_of_grid_cells_into_distance(self.cube, 2)

    def test_check_different_input_radius(self):
        """Check it works for different input values."""
        result_radius = convert_number_of_grid_cells_into_distance(
            self.cube, 5)
        expected_result = 10000.0
        self.assertAlmostEqual(result_radius, expected_result)
        self.assertIs(type(expected_result), float)
Exemple #16
0
    def _set_blended_times(cube: Cube) -> None:
        """Updates time coordinates so that time point is at the end of the time bounds,
        blend_time and forecast_reference_time (if present) are set to the end of the
        bound period and bounds are removed, and forecast_period is updated to match."""
        cube.coord("time").points = cube.coord("time").bounds[0][-1]

        for coord_name in ["blend_time", "forecast_reference_time"]:
            if coord_name in [c.name() for c in cube.coords()]:
                coord = cube.coord(coord_name)
                if coord.has_bounds():
                    coord = coord.copy(coord.bounds[0][-1])
                    cube.replace_coord(coord)

        if "forecast_period" in [c.name() for c in cube.coords()]:
            calculated_coord = forecast_period_coord(
                cube, force_lead_time_calculation=True)
            new_coord = cube.coord("forecast_period").copy(
                points=calculated_coord.points, bounds=calculated_coord.bounds)
            cube.replace_coord(new_coord)
Exemple #17
0
def get_dim_coord_names(cube: Cube) -> List[str]:
    """
    Returns an ordered list of dimension coordinate names on the cube

    Args:
        cube

    Returns:
        List of dimension coordinate names
    """
    return [coord.name() for coord in cube.coords(dim_coords=True)]
Exemple #18
0
def get_coord_names(cube: Cube) -> List[str]:
    """
    Returns a list of all coordinate names on the cube

    Args:
        cube

    Returns:
        List of all coordinate names
    """
    return [coord.name() for coord in cube.coords()]
Exemple #19
0
def check_mandatory_standards(cube: Cube) -> None:
    """
    Checks for mandatory dtype and unit standards on a cube and raises a
    useful exception if any non-compliance is found.

    Args:
        cube:
            The cube to be checked for conformance with standards.

    Raises:
        ValueError:
            If the cube fails to meet any mandatory dtype and units standards
    """
    def check_dtype_and_units(obj: Union[Cube, Coord]) -> List[str]:
        """
        Check object meets the mandatory dtype and units.

        Args:
            obj:
                The object to be checked.

        Returns:
            Contains formatted strings describing each conformance breach.
        """
        dtype_ok = check_dtype(obj)
        units_ok = check_units(obj)

        errors = []
        if not dtype_ok:
            req_dtype = get_required_dtype(obj)
            msg = (f"{obj.name()} of type {type(obj)} does not have "
                   f"required dtype.\n"
                   f"Expected: {req_dtype}, ")
            if isinstance(obj, iris.coords.Coord):
                msg += f"Actual (points): {obj.points.dtype}"
                if obj.has_bounds():
                    msg += f", Actual (bounds): {obj.bounds.dtype}"
            else:
                msg += f"Actual: {obj.dtype}"
            errors.append(msg)
        if not units_ok:
            req_units = get_required_units(obj)
            msg = (f"{obj.name()} of type {type(obj)} does not have "
                   f"required units.\n"
                   f"Expected: {req_units}, Actual: {obj.units}")
            errors.append(msg)
        return errors

    error_list = []
    error_list.extend(check_dtype_and_units(cube))
    for coord in cube.coords():
        error_list.extend(check_dtype_and_units(coord))
    if error_list:
        raise ValueError("\n".join(error_list))
Exemple #20
0
    def _create_daynight_mask(self, cube: Cube) -> Cube:
        """
        Create blank daynight mask cube

        Args:
            cube:
                cube with the times and coordinates required for mask

        Returns:
            Blank daynight mask cube. The resulting cube will be the
            same shape as the time, y, and x coordinate, other coordinates
            will be ignored although they might appear as attributes
            on the cube as it is extracted from the first slice.
        """
        slice_coords = [cube.coord(axis="y"), cube.coord(axis="x")]
        if cube.coord("time") in cube.coords(dim_coords=True):
            slice_coords.insert(0, cube.coord("time"))

        template = next(cube.slices(slice_coords))
        demoted_coords = [
            crd
            for crd in cube.coords(dim_coords=True)
            if crd not in template.coords(dim_coords=True)
        ]
        for crd in demoted_coords:
            template.remove_coord(crd)
        attributes = generate_mandatory_attributes([template])
        title_attribute = {"title": "Day-Night mask"}
        data = np.full(template.data.shape, self.night, dtype=np.int32)
        daynight_mask = create_new_diagnostic_cube(
            "day_night_mask",
            1,
            template,
            attributes,
            optional_attributes=title_attribute,
            data=data,
            dtype=np.int32,
        )
        return daynight_mask
Exemple #21
0
    def build_weights_cube(cube: Cube, weights: ndarray, blending_coord: str,) -> Cube:
        """Build a cube containing weights for use in blending.

        Args:
            cube:
                The cube that is being blended over blending_coord.
            weights:
                Array of weights
            blending_coord:
                Name of the coordinate over which the weights will be used
                to blend data, e.g. across model name when grid blending.

        Returns:
            A cube containing the array of weights.

        Raises:
            ValueError : If weights array is not of the same length as the
                         coordinate being blended over on cube.
        """

        if len(weights) != len(cube.coord(blending_coord).points):
            msg = (
                "Weights array provided is not the same size as the "
                "blending coordinate; weights shape: {}, blending "
                "coordinate shape: {}".format(
                    len(weights), len(cube.coord(blending_coord).points)
                )
            )
            raise ValueError(msg)

        try:
            weights_cube = next(cube.slices(blending_coord))
        except ValueError:
            weights_cube = iris.util.new_axis(cube, blending_coord)
            weights_cube = next(weights_cube.slices(blending_coord))
        weights_cube.attributes = None
        # Find dim associated with blending_coord and don't remove any coords
        # associated with this dimension.
        blending_dim = cube.coord_dims(blending_coord)
        defunct_coords = [
            crd.name()
            for crd in cube.coords(dim_coords=True)
            if not cube.coord_dims(crd) == blending_dim
        ]
        for crd in defunct_coords:
            weights_cube.remove_coord(crd)
        weights_cube.data = weights
        weights_cube.rename("weights")
        weights_cube.units = 1

        return weights_cube
Exemple #22
0
class Test_number_of_grid_cells_to_distance(IrisTest):
    """Test the number_of_grid_cells_to_distance method"""
    def setUp(self):
        """Set up a cube with x and y coordinates"""
        data = np.ones((3, 4))
        self.cube = Cube(
            data,
            standard_name="air_temperature",
        )
        self.cube.add_dim_coord(
            DimCoord(np.linspace(2000.0, 6000.0, 3),
                     "projection_x_coordinate",
                     units="m"),
            0,
        )
        self.cube.add_dim_coord(
            DimCoord(np.linspace(2000.0, 8000.0, 4),
                     "projection_y_coordinate",
                     units="m"),
            1,
        )

    def test_basic(self):
        """Test the function does what it's meant to in a simple case."""
        result_radius = number_of_grid_cells_to_distance(self.cube, 2)
        expected_result = 4000.0
        self.assertAlmostEqual(result_radius, expected_result)
        self.assertIs(type(expected_result), float)

    def test_check_input_in_km(self):
        """
        Test that the output is still in metres when the input coordinates
        are in a different unit.
        """
        result_radius = number_of_grid_cells_to_distance(self.cube, 2)
        for coord in self.cube.coords():
            coord.convert_units("km")
        expected_result = 4000.0
        self.assertAlmostEqual(result_radius, expected_result)
        self.assertIs(type(expected_result), float)

    def test_check_different_input_radius(self):
        """Check it works for different input values."""
        result_radius = number_of_grid_cells_to_distance(self.cube, 5)
        expected_result = 10000.0
        self.assertAlmostEqual(result_radius, expected_result)
        self.assertIs(type(expected_result), float)
Exemple #23
0
    def process(self, cube: Cube) -> Cube:
        """
        Create a cube containing the percentiles as a new dimension.

        What's generated by default is:
            * 15 percentiles - (0%, 5%, 10%, 20%, 25%, 30%, 40%, 50%, 60%,
              70%, 75%, 80%, 90%, 95%, 100%)

        Args:
            cube:
                Given the collapse coordinate, convert the set of values
                along that coordinate into a PDF and extract percentiles.

        Returns:
            A single merged cube of all the cubes produced by each
            percentile collapse.
        """
        # Store data type and enforce the same type on return.
        data_type = cube.dtype
        # Test that collapse coords are present in cube before proceeding.
        n_collapse_coords = len(self.collapse_coord)
        n_valid_coords = sum([
            test_coord == coord.name() for coord in cube.coords()
            for test_coord in self.collapse_coord
        ])
        # Rename the percentile coordinate to "percentile" and also
        # makes sure that the associated unit is %.
        if n_valid_coords == n_collapse_coords:
            result = collapsed(
                cube,
                self.collapse_coord,
                iris.analysis.PERCENTILE,
                percent=self.percentiles,
                fast_percentile_method=self.fast_percentile_method,
            )

            result.data = result.data.astype(data_type)
            for coord in self.collapse_coord:
                result.remove_coord(coord)
            percentile_coord = find_percentile_coordinate(result)
            result.coord(percentile_coord).rename("percentile")
            result.coord(percentile_coord).units = "%"
            return result

        raise CoordinateNotFoundError(
            "Coordinate '{}' not found in cube passed to {}.".format(
                self.collapse_coord, self.__class__.__name__))
Exemple #24
0
    def _check_dim_coords(temperature: Cube, lapse_rate: Cube) -> None:
        """Throw an error if the dimension coordinates are not the same for
        temperature and lapse rate cubes

        Args:
            temperature
            lapse_rate
        """
        for crd in temperature.coords(dim_coords=True):
            try:
                if crd != lapse_rate.coord(crd.name()):
                    raise ValueError(
                        'Lapse rate cube coordinate "{}" does not match '
                        "temperature cube coordinate".format(crd.name()))
            except CoordinateNotFoundError:
                raise ValueError("Lapse rate cube has no coordinate "
                                 '"{}"'.format(crd.name()))
Exemple #25
0
    def _collapse_scalar_dimensions(cube: Cube) -> Cube:
        """
        Demote any scalar dimensions (excluding "realization") on the input
        cube to auxiliary coordinates.

        Args:
            cube: The cube

        Returns:
            The collapsed cube
        """
        coords_to_collapse = []
        for coord in cube.coords(dim_coords=True):
            if len(coord.points) == 1 and "realization" not in coord.name():
                coords_to_collapse.append(coord)
        for coord in coords_to_collapse:
            cube = next(cube.slices_over(coord))
        return cube
Exemple #26
0
    def process(self, radar_data: Cube, coverage: Cube) -> Cube:
        """
        Update the mask on the input rainrate cube to reflect where coverage
        is valid

        Args:
            radar_data:
                Radar data with mask corresponding to radar domains
            coverage:
                Radar coverage data containing values:
                    0: outside composite
                    1: precip detected
                    2: precip not detected & 1/32 mm/h detectable at this range
                    3: precip not detected & 1/32 mm/h NOT detectable

        Returns:
            Radar data with mask extended to mask out regions where
            1/32 mm/h are not detectable
        """
        # check cube coordinates match
        for crd in radar_data.coords():
            if coverage.coord(crd.name()) != crd:
                raise ValueError("Rain rate and coverage composites unmatched "
                                 "- coord {}".format(crd.name()))

        # accommodate data from multiple times
        radar_data_slices = radar_data.slices(
            [radar_data.coord(axis="y"),
             radar_data.coord(axis="x")])
        coverage_slices = coverage.slices(
            [coverage.coord(axis="y"),
             coverage.coord(axis="x")])

        cube_list = iris.cube.CubeList()
        for rad, cov in zip(radar_data_slices, coverage_slices):
            # create a new mask that is False wherever coverage is valid
            new_mask = ~np.isin(cov.data, self.coverage_valid)

            # remask rainrate data
            remasked_data = np.ma.MaskedArray(rad.data.data, mask=new_mask)
            cube_list.append(rad.copy(remasked_data))

        return cube_list.merge_cube()
Exemple #27
0
def update_daynight(cubewx: Cube) -> Cube:
    """ Update weather cube depending on whether it is day or night

    Args:
        cubewx:
            Cube containing only daytime weather symbols.

    Returns:
        Cube containing day and night weather symbols

    Raises:
        CoordinateNotFoundError : cube must have time coordinate.
    """
    import numpy as np
    from iris.exceptions import CoordinateNotFoundError

    import improver.utilities.solar as solar

    if not cubewx.coords("time"):
        msg = "cube must have time coordinate "
        raise CoordinateNotFoundError(msg)

    cubewx_daynight = cubewx.copy()
    daynightplugin = solar.DayNightMask()
    daynight_mask = daynightplugin(cubewx_daynight)

    # Loop over the codes which decrease by 1 if a night time value
    # e.g. 1 - sunny day becomes 0 - clear night.
    for val in DAYNIGHT_CODES:
        index = np.where(cubewx_daynight.data == val)
        # Where day leave as is, where night correct weather
        # code to value  - 1.
        cubewx_daynight.data[index] = np.where(
            daynight_mask.data[index] == daynightplugin.day,
            cubewx_daynight.data[index],
            cubewx_daynight.data[index] - 1,
        )

    return cubewx_daynight
Exemple #28
0
    def _standardise_dtypes_and_units(cube: Cube) -> None:
        """
        Modify input cube in place to conform to mandatory dtype and unit
        standards.

        Args:
            cube:
                Cube to be updated in place
        """
        def as_correct_dtype(obj: ndarray, required_dtype: dtype) -> ndarray:
            """
            Returns an object updated if necessary to the required dtype

            Args:
                obj:
                    The object to be updated
                required_dtype:
                    The dtype required

            Returns:
                The updated object
            """
            if obj.dtype != required_dtype:
                return obj.astype(required_dtype)
            return obj

        cube.data = as_correct_dtype(cube.data, get_required_dtype(cube))
        for coord in cube.coords():
            if coord.name() in TIME_COORDS and not check_units(coord):
                coord.convert_units(get_required_units(coord))
            req_dtype = get_required_dtype(coord)
            # ensure points and bounds have the same dtype
            if np.issubdtype(req_dtype, np.integer):
                coord.points = round_close(coord.points)
            coord.points = as_correct_dtype(coord.points, req_dtype)
            if coord.has_bounds():
                if np.issubdtype(req_dtype, np.integer):
                    coord.bounds = round_close(coord.bounds)
                coord.bounds = as_correct_dtype(coord.bounds, req_dtype)
    def create_coefficient_cube(
        self, data: ndarray, template: Cube, cube_name: str, attributes: Dict
    ) -> Cube:
        """
        Update metadata in smoothing_coefficients cube. Remove any time
        coordinates and rename.

        Args:
            data:
                The smoothing coefficient data to store in the cube.
            template:
                A gradient cube, the dimensions of which are used as a template
                for the coefficient cube.
            cube_name:
                A name for the resultant cube
            attributes:
                A dictionary of attributes for the new cube.

        Returns:
            A new cube of smoothing_coefficients
        """
        for coord in template.coords(dim_coords=False):
            for coord_name in ["time", "period", "realization"]:
                if coord_name in coord.name():
                    template.remove_coord(coord)

        attributes["title"] = "Recursive filter smoothing coefficients"
        attributes.pop("history", None)
        attributes["power"] = self.power

        return create_new_diagnostic_cube(
            cube_name,
            "1",
            template,
            MANDATORY_ATTRIBUTE_DEFAULTS.copy(),
            optional_attributes=attributes,
            data=data,
        )
Exemple #30
0
def find_percentile_coordinate(cube: Cube) -> Coord:
    """Find percentile coord in cube.

    Args:
        cube:
            Cube contain one or more percentiles.

    Returns:
        Percentile coordinate.

    Raises:
        TypeError: If cube is not of type iris.cube.Cube.
        CoordinateNotFoundError: If no percentile coordinate is found in cube.
        ValueError: If there is more than one percentile coords in the cube.
    """
    if not isinstance(cube, iris.cube.Cube):
        msg = (
            "Expecting data to be an instance of "
            "iris.cube.Cube but is {0}.".format(type(cube))
        )
        raise TypeError(msg)
    standard_name = cube.name()
    perc_coord = None
    perc_found = 0
    for coord in cube.coords():
        if coord.name().find("percentile") >= 0:
            perc_found += 1
            perc_coord = coord

    if perc_found == 0:
        msg = "No percentile coord found on {0:s} data".format(standard_name)
        raise CoordinateNotFoundError(msg)

    if perc_found > 1:
        msg = "Too many percentile coords found on {0:s} data".format(standard_name)
        raise ValueError(msg)

    return perc_coord
Exemple #31
0
def make_mock_cube(lat_dim_length=5, lon_dim_length=3, lon_range=None, alt_dim_length=0, pres_dim_length=0,
                   time_dim_length=0,
                   horizontal_offset=0, altitude_offset=0, pressure_offset=0, time_offset=0, data_offset=0,
                   surf_pres_offset=0,
                   hybrid_ht_len=0, hybrid_pr_len=0, geopotential_height=False, dim_order=None, mask=False):
    """
    Makes a cube of any shape required, with coordinate offsets from the default available. If no arguments are
    given get a 5x3 cube of the form:
        array([[1,2,3],
               [4,5,6],
               [7,8,9],
               [10,11,12],
               [13,14,15]])
        and coordinates in latitude:
            array([ -10, -5, 0, 5, 10 ])
        longitude:
            array([ -5, 0, 5 ])
    :param lat_dim_length: Latitude grid length
    :param lon_dim_length: Longitude grid length
    :param alt_dim_length: Altitude grid length
    :param pres_dim_length: Pressure grid length
    :param time_dim_length: Time grid length
    :param horizontal_offset: Offset from the default grid, in degrees, in lat and lon
    :param altitude_offset: Offset from the default grid in altitude
    :param pressure_offset: Offset from the default grid in pressure
    :param time_offset: Offset from the default grid in time
    :param data_offset: Offset from the default data values
    :param surf_pres_offset: Offset for the optional surface pressure field
    :param hybrid_ht_len: Hybrid height grid length
    :param hybrid_pr_len: Hybrid pressure grid length
    :param geopotential_height: Include a geopotential height field when calcluting a hybrid pressure? (default False)
    :param dim_order: List of 'lat', 'lon', 'alt', 'pres', 'time' in the order in which the dimensions occur
    :param mask: A mask to apply to the data, this should be either a scalar or the same shape as the data
    :return: A cube with well defined data.
    """
    import iris
    from iris.aux_factory import HybridHeightFactory, HybridPressureFactory

    data_size = 1
    DIM_NAMES = ['lat', 'lon', 'alt', 'pres', 'time', 'hybrid_ht', 'hybrid_pr']
    dim_lengths = [lat_dim_length, lon_dim_length, alt_dim_length, pres_dim_length, time_dim_length, hybrid_ht_len,
                   hybrid_pr_len]
    lon_range = lon_range or (-5., 5.)

    if dim_order is None:
        dim_order = list(DIM_NAMES)

    if any([True for d in dim_order if d not in DIM_NAMES]):
        raise ValueError("dim_order contains unrecognised name")

    for idx, dim in enumerate(DIM_NAMES):
        if dim_lengths[idx] == 0 and dim in dim_order:
            del dim_order[dim_order.index(dim)]

    coord_map = {}
    for idx, dim in enumerate(dim_order):
        coord_map[dim] = dim_order.index(dim)
    coord_list = [None] * len(coord_map)

    if lat_dim_length:
        coord_list[coord_map['lat']] = (DimCoord(np.linspace(-10., 10., lat_dim_length) + horizontal_offset,
                                                 standard_name='latitude', units='degrees', var_name='lat'),
                                        coord_map['lat'])
        data_size *= lat_dim_length

    if lon_dim_length:
        coord_list[coord_map['lon']] = (
            DimCoord(np.linspace(lon_range[0], lon_range[1], lon_dim_length) + horizontal_offset,
                     standard_name='longitude', units='degrees', var_name='lon'), coord_map['lon'])
        data_size *= lon_dim_length

    if alt_dim_length:
        coord_list[coord_map['alt']] = (DimCoord(np.linspace(0., 7., alt_dim_length) + altitude_offset,
                                                 standard_name='altitude', units='metres', var_name='alt'),
                                        coord_map['alt'])
        data_size *= alt_dim_length

    if pres_dim_length:
        coord_list[coord_map['pres']] = (DimCoord(np.linspace(0., 7., pres_dim_length) + pressure_offset,
                                                  standard_name='air_pressure', units='hPa', var_name='pres'),
                                         coord_map['pres'])
        data_size *= pres_dim_length

    if time_dim_length:
        t0 = datetime.datetime(1984, 8, 27)
        times = np.array([t0 + datetime.timedelta(days=d + time_offset) for d in range(time_dim_length)])
        time_nums = convert_datetime_to_std_time(times)
        time_bounds = None
        if time_dim_length == 1:
            time_bounds = convert_datetime_to_std_time(np.array([times[0] - datetime.timedelta(days=0.5),
                                                                       times[0] + datetime.timedelta(days=0.5)]))
        coord_list[coord_map['time']] = (DimCoord(time_nums, standard_name='time',
                                                  units='days since 1600-01-01 00:00:00', var_name='time',
                                                  bounds=time_bounds),
                                         coord_map['time'])
        data_size *= time_dim_length

    if hybrid_ht_len:
        coord_list[coord_map['hybrid_ht']] = (DimCoord(np.arange(hybrid_ht_len, dtype='i8') + 10,
                                                       "model_level_number", units="1"), coord_map['hybrid_ht'])
        data_size *= hybrid_ht_len

    if hybrid_pr_len:
        coord_list[coord_map['hybrid_pr']] = (DimCoord(np.arange(hybrid_pr_len, dtype='i8'),
                                                       "atmosphere_hybrid_sigma_pressure_coordinate", units="1"),
                                              coord_map['hybrid_pr'])
        data_size *= hybrid_pr_len

    data = np.reshape(np.arange(data_size) + data_offset + 1., tuple(len(i[0].points) for i in coord_list))

    if mask:
        data = np.ma.asarray(data)
        data.mask = mask

    return_cube = Cube(data, dim_coords_and_dims=coord_list, var_name='rain', standard_name='rainfall_rate',
                       long_name="TOTAL RAINFALL RATE: LS+CONV KG/M2/S", units="kg m-2 s-1")

    if hybrid_ht_len:
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 40,
                                                       long_name="level_height",
                                                       units="m", var_name='hybrid_ht'), coord_map['hybrid_ht'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 50,
                                                       long_name="sigma", units="1", var_name='sigma'),
                                  coord_map['hybrid_ht'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(
            np.arange(lat_dim_length * lon_dim_length, dtype='i8').reshape(lat_dim_length, lon_dim_length) + 100,
            long_name="surface_altitude",
            units="m"), [coord_map['lat'], coord_map['lon']])

        return_cube.add_aux_factory(HybridHeightFactory(
            delta=return_cube.coord("level_height"),
            sigma=return_cube.coord("sigma"),
            orography=return_cube.coord("surface_altitude")))
    elif hybrid_pr_len:
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='i8') + 40,
                                                       long_name="hybrid A coefficient at layer midpoints",
                                                       units="Pa", var_name='a'), coord_map['hybrid_pr'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='f8') + 50,
                                                       long_name="hybrid B coefficient at layer midpoints", units="1",
                                                       var_name='b'),
                                  coord_map['hybrid_pr'])
        return_cube.add_aux_coord(
            iris.coords.AuxCoord(np.arange(lat_dim_length * lon_dim_length * time_dim_length, dtype='i8')
                                 .reshape(lat_dim_length, lon_dim_length, time_dim_length) * 100000 + surf_pres_offset,
                                 "surface_air_pressure", units="Pa"),
            [coord_map['lat'], coord_map['lon'], coord_map['time']])

        if geopotential_height:
            return_cube.add_aux_coord(iris.coords.AuxCoord(
                np.arange(lat_dim_length * lon_dim_length * time_dim_length * hybrid_pr_len, dtype='i8')
                .reshape(lat_dim_length, lon_dim_length, time_dim_length, hybrid_pr_len) + 10,
                "altitude", long_name="Geopotential height at layer midpoints", units="meter"),
                [coord_map['lat'], coord_map['lon'], coord_map['time'], coord_map['hybrid_pr']])

        return_cube.add_aux_factory(HybridPressureFactory(
            delta=return_cube.coord("hybrid A coefficient at layer midpoints"),
            sigma=return_cube.coord("hybrid B coefficient at layer midpoints"),
            surface_air_pressure=return_cube.coord("surface_air_pressure")))

    for coord in return_cube.coords(dim_coords=True):
        if coord.bounds is None:
            coord.guess_bounds()

    return return_cube