Esempio n. 1
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."
            )
Esempio n. 2
0
 def test_other_coord_diffs(self):
     """Test when given cubes that differ in non-spatial coords."""
     cube_c = self.cube_a.copy()
     r_coord = cube_c.coord('realization')
     r_coord.points = [r * 2 for r in r_coord.points]
     result = spatial_coords_match(self.cube_a, cube_c)
     self.assertTrue(result)
Esempio n. 3
0
    def process(self, cube: Cube) -> Cube:
        """
        Ensure that the cube passed to the maximum_within_vicinity method is
        2d and subsequently merged back together.

        Args:
            cube:
                Thresholded cube.

        Returns:
            Cube containing the occurrences within a vicinity for each
            xy 2d slice, which have been merged back together.
        """
        if self.land_mask_cube and not spatial_coords_match(
            [cube, self.land_mask_cube]):
            raise ValueError(
                "Supplied cube do not have the same spatial coordinates and land mask"
            )
        max_cubes = CubeList([])
        for cube_slice in cube.slices(
            [cube.coord(axis="y"), cube.coord(axis="x")]):
            max_cubes.append(self.maximum_within_vicinity(cube_slice))
        result_cube = max_cubes.merge_cube()

        # Put dimensions back if they were there before.
        result_cube = check_cube_coordinates(cube, result_cube)
        return result_cube
Esempio n. 4
0
 def test_other_coord_bigger_diffs(self):
     """Test when given cubes that differ in shape on non-spatial coords."""
     cube_c = set_up_cube(num_grid_points=16, num_realization_points=4)
     r_coord = cube_c.coord('realization')
     r_coord.points = [r * 2 for r in r_coord.points]
     result = spatial_coords_match(self.cube_a, cube_c)
     self.assertTrue(result)
Esempio n. 5
0
 def test_unmatching_x(self):
     """Test when given two spatially different cubes of same length."""
     cube_c = self.cube_a.copy()
     x_coord = cube_c.coord(axis='x')
     x_coord.points = [x * 2. for x in x_coord.points]
     result = spatial_coords_match(self.cube_a, cube_c)
     self.assertFalse(result)
Esempio n. 6
0
 def test_unmatching_y(self):
     """Test when given two spatially different cubes of same length."""
     cube_c = self.cube_a.copy()
     y_coord = cube_c.coord(axis='y')
     y_coord.points = [y * 1.01 for y in y_coord.points]
     result = spatial_coords_match(self.cube_a, cube_c)
     self.assertFalse(result)
Esempio n. 7
0
    def process(self, temperature, lapse_rate, source_orog, dest_orog):
        """Applies lapse rate correction to temperature forecast.  All cubes'
        units are modified in place.

        Args:
            temperature (iris.cube.Cube):
                Input temperature field to be adjusted
            lapse_rate (iris.cube.Cube):
                Cube of pre-calculated lapse rates
            source_orog (iris.cube.Cube):
                2D cube of source orography heights
            dest_orog (iris.cube.Cube):
                2D cube of destination orography heights

        Returns:
            iris.cube.Cube:
                Lapse-rate adjusted temperature field, in Kelvin
        """
        lapse_rate.convert_units("K m-1")
        self.xy_coords = [
            lapse_rate.coord(axis="y"),
            lapse_rate.coord(axis="x")
        ]

        self._check_dim_coords(temperature, lapse_rate)

        if not spatial_coords_match(temperature, source_orog):
            raise ValueError(
                "Source orography spatial coordinates do not match "
                "temperature grid")

        if not spatial_coords_match(temperature, dest_orog):
            raise ValueError(
                "Destination orography spatial coordinates do not match "
                "temperature grid")

        orog_diff = self._calc_orog_diff(source_orog, dest_orog)

        adjusted_temperature = []
        for lr_slice, t_slice in zip(lapse_rate.slices(self.xy_coords),
                                     temperature.slices(self.xy_coords)):
            newcube = t_slice.copy()
            newcube.convert_units("K")
            newcube.data += np.multiply(orog_diff.data, lr_slice.data)
            adjusted_temperature.append(newcube)

        return iris.cube.CubeList(adjusted_temperature).merge_cube()
Esempio n. 8
0
 def test_unmatching_y(self):
     """Test when given two cubes of the same shape, but with differing
     y coordinate values."""
     cube_c = self.cube_a.copy()
     y_coord = cube_c.coord(axis="y")
     y_coord.points = [y * 1.01 for y in y_coord.points]
     result = spatial_coords_match([self.cube_a, cube_c])
     self.assertFalse(result)
Esempio n. 9
0
 def test_unmatching_x(self):
     """Test when given two cubes of the same shape, but with differing
     x coordinate values."""
     cube_c = self.cube_a.copy()
     x_coord = cube_c.coord(axis="x")
     x_coord.points = [x * 2.0 for x in x_coord.points]
     result = spatial_coords_match([self.cube_a, cube_c])
     self.assertFalse(result)
Esempio n. 10
0
    def process(self, cube: Cube, input_land: Cube, output_land: Cube) -> Cube:
        """
        Update cube.data so that output_land and sea points match an input_land
        or sea point respectively so long as one is present within the
        specified vicinity radius. Note that before calling this plugin the
        input land mask MUST be checked against the source grid, to ensure
        the grids match.

        Args:
            cube:
                Cube of data to be updated (on same grid as output_land).
            input_land:
                Cube of land_binary_mask data on the grid from which "cube" has
                been reprojected (it is expected that the iris.analysis.Nearest
                method would have been used). Land points should be set to one
                and sea points set to zero.
                This is used to determine where the input model data is
                representing land and sea points.
            output_land:
                Cube of land_binary_mask data on target grid.

        Returns:
            Cube of regridding results.
        """
        # Check cube and output_land are on the same grid:
        if not spatial_coords_match([cube, output_land]):
            raise ValueError("X and Y coordinates do not match for cubes {}"
                             "and {}".format(repr(cube), repr(output_land)))
        self.output_land = output_land

        # Regrid input_land to output_land grid.
        self.input_land = input_land.regrid(self.output_land, self.regridder)

        # Slice over x-y grids for multi-realization data.
        result = iris.cube.CubeList()

        # Reset cache as input_land and output_land have changed
        self._get_matches.cache_clear()
        for xyslice in cube.slices(
            [cube.coord(axis="y"), cube.coord(axis="x")]):
            # Store and copy cube ready for the output data
            self.nearest_cube = xyslice
            self.output_cube = self.nearest_cube.copy()

            # Update sea points that were incorrectly sourced from land points
            self.correct_where_input_true(0)

            # Update land points that were incorrectly sourced from sea points
            self.correct_where_input_true(1)

            result.append(self.output_cube)

        result = result.merge_cube()
        return result
Esempio n. 11
0
 def test_other_coord_bigger_diffs(self):
     """Test when given cubes that differ in shape on non-spatial coords."""
     data_c = np.ones((4, 16, 16), dtype=np.float32)
     data_c[:, 7, 7] = 0.0
     cube_c = set_up_variable_cube(
         data_c, "precipitation_amount", "kg m^-2", "equalarea",
     )
     r_coord = cube_c.coord("realization")
     r_coord.points = [r * 2 for r in r_coord.points]
     result = spatial_coords_match(self.cube_a, cube_c)
     self.assertTrue(result)
Esempio n. 12
0
 def _get_inputs(cubes: CubeList) -> Tuple[Cube, Cube]:
     """
     Separates CAPE and precipitation rate cubes and checks that the following
     match: forecast_reference_time, spatial coords, time-bound interval and
     that CAPE time is at the lower bound of precipitation rate time.
     The precipitation rate data must represent a period of 1 or 3 hours.
     """
     cape = cubes.extract(
         iris.Constraint(
             cube_func=lambda cube: "atmosphere_convective_available_potential_energy"
             in cube.name()
         )
     )
     if cape:
         cape = cape.merge_cube()
     else:
         raise ValueError(
             f"No cube named atmosphere_convective_available_potential_energy found "
             f"in {cubes}"
         )
     precip = cubes.extract(
         iris.Constraint(
             cube_func=lambda cube: "precipitation_rate_max" in cube.name()
         )
     )
     if precip:
         precip = precip.merge_cube()
     else:
         raise ValueError(f"No cube named precipitation_rate_max found in {cubes}")
     (cape_time,) = list(cape.coord("time").cells())
     (precip_time,) = list(precip.coord("time").cells())
     if cape_time.point != precip_time.bound[0]:
         raise ValueError(
             f"CAPE cube time ({cape_time.point}) should be valid at the "
             f"precipitation_rate_max cube lower bound ({precip_time.bound[0]})."
         )
     if np.diff(precip_time.bound) not in [timedelta(hours=1), timedelta(hours=3)]:
         raise ValueError(
             f"Precipitation_rate_max cube time window must be one or three hours, "
             f"not {np.diff(precip_time.bound)}."
         )
     if cape.coord("forecast_reference_time") != precip.coord(
         "forecast_reference_time"
     ):
         raise ValueError(
             "Supplied cubes must have the same forecast reference times"
         )
     if not spatial_coords_match([cape, precip]):
         raise ValueError("Supplied cubes do not have the same spatial coordinates")
     return cape, precip
Esempio n. 13
0
def grid_contains_cutout(grid, cutout):
    """
    Check that a spatial cutout is contained within a given grid

    Args:
        grid (iris.cube.Cube):
            A cube defining a data grid
        cutout (iris.cube.Cube):
            The cutout to search for within the grid

    Returns:
        bool:
            True if cutout is contained within grid, False otherwise
    """
    if spatial_coords_match(grid, cutout):
        return True

    # check whether "cutout" coordinate points match a subset of "grid"
    # points on both axes
    for axis in ["x", "y"]:
        grid_coord = grid.coord(axis=axis)
        cutout_coord = cutout.coord(axis=axis)
        # check coordinate metadata
        if (cutout_coord.name() != grid_coord.name()
                or cutout_coord.units != grid_coord.units
                or cutout_coord.coord_system != grid_coord.coord_system):
            return False

        # search for cutout coordinate points in larger grid
        cutout_start = cutout_coord.points[0]
        find_start = [
            np.isclose(cutout_start, grid_point)
            for grid_point in grid_coord.points
        ]
        if not np.any(find_start):
            return False

        start = find_start.index(True)
        end = start + len(cutout_coord.points)
        try:
            if not np.allclose(cutout_coord.points,
                               grid_coord.points[start:end]):
                return False
        except ValueError:
            # raised by np.allclose if "end" index overshoots edge of grid
            # domain - slicing does not raise IndexError
            return False

    return True
Esempio n. 14
0
    def process(self, cube, input_land, output_land):
        """
        Update cube.data so that output_land and sea points match an input_land
        or sea point respectively so long as one is present within the
        specified vicinity radius.

        Args:
            cube (iris.cube.Cube):
                Cube of data to be updated (on same grid as output_land).
            input_land (iris.cube.Cube):
                Cube of land_binary_mask data on the grid from which "cube" has
                been reprojected (it is expected that the iris.analysis.Nearest
                method would have been used).
                This is used to determine where the input model data is
                representing land and sea points.
            output_land (iris.cube.Cube):
                Cube of land_binary_mask data on target grid.
        """
        # Check cube and output_land are on the same grid:
        if not spatial_coords_match(cube, output_land):
            raise ValueError('X and Y coordinates do not match for cubes {}'
                             'and {}'.format(repr(cube), repr(output_land)))
        self.output_land = output_land

        # Regrid input_land to output_land grid.
        self.input_land = input_land.regrid(self.output_land, self.regridder)

        # Slice over x-y grids for multi-realization data.
        result = iris.cube.CubeList()
        for xyslice in cube.slices(
            [cube.coord(axis='y'), cube.coord(axis='x')]):

            # Store and copy cube ready for the output data
            self.nearest_cube = xyslice
            self.output_cube = self.nearest_cube.copy()

            # Update sea points that were incorrectly sourced from land points
            self.correct_where_input_true(0)

            # Update land points that were incorrectly sourced from sea points
            self.correct_where_input_true(1)

            result.append(self.output_cube)

        result = result.merge_cube()
        return result
Esempio n. 15
0
    def _get_input_cubes(self, input_cubes):
        """
        Separates out the rain and snow cubes from the input list and checks that
            * No other cubes are present
            * Cubes represent the same time quantity (instantaneous or accumulation length)
            * Cubes have compatible units
            * Cubes have same dimensions
            * Cubes are not masked (or are masked with an all-False mask)

        Args:
            input_cubes (iris.cube.CubeList):
                Contains exactly two cubes, one of rain and one of snow. Both must be
                either rates or accumulations of the same length and of compatible units.

        Returns:
            None

        Raises:
            ValueError:
                If any of the criteria above are not met.

        """
        if len(input_cubes) != 2:
            raise ValueError(
                f"Expected exactly 2 input cubes, found {len(input_cubes)}"
            )
        rain_name, snow_name = self._get_input_cube_names(input_cubes)
        self.rain = input_cubes.extract(rain_name).merge_cube()
        self.snow = input_cubes.extract(snow_name).merge_cube()
        self.snow.convert_units(self.rain.units)
        if not spatial_coords_match(self.rain, self.snow):
            raise ValueError("Rain and snow cubes are not on the same grid")
        if not self.rain.coord("time") == self.snow.coord("time"):
            raise ValueError("Rain and snow cubes do not have the same time coord")
        if np.ma.is_masked(self.rain.data) or np.ma.is_masked(self.snow.data):
            raise ValueError("Unexpected masked data in input cube(s)")
        if isinstance(self.rain.data, np.ma.masked_array):
            self.rain.data = self.rain.data.data
        if isinstance(self.snow.data, np.ma.masked_array):
            self.snow.data = self.snow.data.data
Esempio n. 16
0
    def _parse_inputs(self, inputs: List[Cube]) -> None:
        """
        Separates input CubeList into CAPE and precipitation rate objects with standard units
        and raises Exceptions if it can't, or finds excess data.

        Args:
            inputs:
                List of Cubes containing exactly one of CAPE and Precipitation rate.
        Raises:
            ValueError:
                If additional cubes are found
        """
        cubes = CubeList(inputs)
        try:
            (self.cape, self.precip) = cubes.extract(self.cube_names)
        except ValueError as e:
            raise ValueError(
                f"Expected to find cubes of {self.cube_names}, not {[c.name() for c in cubes]}"
            ) from e
        if len(cubes) > 2:
            extras = [
                c.name() for c in cubes if c.name() not in self.cube_names
            ]
            raise ValueError(f"Unexpected Cube(s) found in inputs: {extras}")
        if not spatial_coords_match(inputs):
            raise ValueError(
                f"Spatial coords of input Cubes do not match: {cubes}")
        time_error_msg = self._input_times_error()
        if time_error_msg:
            raise ValueError(time_error_msg)
        self.cape.convert_units("J kg-1")
        self.precip.convert_units("mm h-1")
        if self.model_id_attr:
            if (self.cape.attributes[self.model_id_attr] !=
                    self.precip.attributes[self.model_id_attr]):
                raise ValueError(
                    f"Attribute {self.model_id_attr} does not match on input cubes. "
                    f"{self.cape.attributes[self.model_id_attr]} != "
                    f"{self.precip.attributes[self.model_id_attr]}")
Esempio n. 17
0
 def test_unmatching(self):
     """Test when given two spatially different cubes of same resolution."""
     result = spatial_coords_match(self.cube_a, self.cube_b)
     self.assertFalse(result)
Esempio n. 18
0
    def _get_input_cubes(self, input_cubes: CubeList) -> None:
        """
        Separates out the rain, sleet, and temperature cubes, checking that:
            * No other cubes are present
            * Cubes have same dimensions
            * Cubes represent the same time quantity (instantaneous or accumulation length)
            * Precipitation cube threshold units are compatible
            * Precipitation cubes have the same set of thresholds
            * A 273.15K (0 Celsius) temperature threshold is available

        The temperature cube is also modified if necessary to return probabilties
        below threshold values. This data is then thinned to return only the
        probabilities of temperature being below the freezing point of water,
        0 Celsius.

        Args:
            input_cubes:
                Contains exactly three cubes, a rain rate or accumulation, a
                sleet rate or accumulation, and an instantaneous or period
                temperature. Accumulations and periods must all represent the
                same length of time.

        Raises:
            ValueError:
                If any of the criteria above are not met.
        """
        if len(input_cubes) != 3:
            raise ValueError(
                f"Expected exactly 3 input cubes, found {len(input_cubes)}")
        rain_name, sleet_name, temperature_name = self._get_input_cube_names(
            input_cubes)
        (self.rain, ) = input_cubes.extract(rain_name)
        (self.sleet, ) = input_cubes.extract(sleet_name)
        (self.temperature, ) = input_cubes.extract(temperature_name)

        if not spatial_coords_match([self.rain, self.sleet, self.temperature]):
            raise ValueError("Input cubes are not on the same grid")
        if (not self.rain.coord("time") == self.sleet.coord("time") ==
                self.temperature.coord("time")):
            raise ValueError("Input cubes do not have the same time coord")

        # Ensure rain and sleet cubes are compatible
        rain_threshold = self.rain.coord(var_name="threshold")
        sleet_threshold = self.sleet.coord(var_name="threshold")
        try:
            sleet_threshold.convert_units(rain_threshold.units)
        except ValueError:
            raise ValueError("Rain and sleet cubes have incompatible units")

        if not all(rain_threshold.points == sleet_threshold.points):
            raise ValueError(
                "Rain and sleet cubes have different threshold values")

        # Ensure probabilities relate to temperatures below a threshold
        temperature_threshold = self.temperature.coord(var_name="threshold")
        self.temperature = to_threshold_inequality(self.temperature,
                                                   above=False)

        # Simplify the temperature cube to the critical threshold of 273.15K,
        # the freezing point of water under typical pressures.
        self.temperature = extract_subcube(
            self.temperature, [f"{temperature_threshold.name()}=273.15"],
            units=["K"])
        if self.temperature is None:
            raise ValueError(
                "No 0 Celsius or equivalent threshold is available "
                "in the temperature data")
Esempio n. 19
0
    def _extract_input_cubes(self, cubes):
        """
        Separates the input list into the required cubes for this plugin,
        detects whether snow or rain are required from the input phase-level
        cube name and appropriately initialises the percentile_plugin, sets
        the appropriate comparator operator for comparing with orography and
        the unique part of the output cube name.

        Converts units of falling_level_cube to that of orography_cube if
        necessary. Sets flag for snow or rain depending on name of
        falling_level_cube.

        Args:
            cubes (iris.cube.CubeList or list):
                Contains cubes of the altitude of the phase-change level (this
                can be snow->sleet, or sleet->rain) and the altitude of the
                orography. The name of the phase-change level cube must be
                either "altitude_of_snow_falling_level" or
                "altitude_of_rain_falling_level". The name of the orography
                cube must be "surface_altitude".

        Raises:
            ValueError: If cubes with the expected names cannot be extracted.
            ValueError: If cubes does not have the expected length of 2.
            ValueError: If the extracted cubes do not have matching spatial
                        coordinates.

        """
        if isinstance(cubes, list):
            cubes = iris.cube.CubeList(cubes)
        if len(cubes) != 2:
            raise ValueError(f'Expected 2 cubes, found {len(cubes)}')

        if not spatial_coords_match(cubes[0], cubes[1]):
            raise ValueError('Spatial coords mismatch between '
                             f'{cubes[0]} and '
                             f'{cubes[1]}')

        extracted_cube = cubes.extract('altitude_of_snow_falling_level')
        if extracted_cube:
            self.falling_level_cube, = extracted_cube
            self.param = 'snow'
            self.comparator = operator.gt
            self.get_discriminating_percentile = self.percentile_plugin(
                self._nbhood_shape, self.radius, percentiles=[80.])
        else:
            extracted_cube = cubes.extract('altitude_of_rain_falling_level')
            if not extracted_cube:
                raise ValueError(
                    'Could not extract a rain or snow falling-level '
                    f'cube from {cubes}')
            self.falling_level_cube, = extracted_cube
            self.param = 'rain'
            self.comparator = operator.lt
            # We want rain at or above the surface, so inverse of 80th
            # centile is the 20th centile.
            self.get_discriminating_percentile = self.percentile_plugin(
                self._nbhood_shape, self.radius, percentiles=[20.])

        orography_name = 'surface_altitude'
        extracted_cube = cubes.extract(orography_name)
        if extracted_cube:
            self.orography_cube, = extracted_cube
        else:
            raise ValueError(f'Could not extract {orography_name} cube from '
                             f'{cubes}')

        if self.falling_level_cube.units != self.orography_cube.units:
            self.falling_level_cube = self.falling_level_cube.copy()
            self.falling_level_cube.convert_units(self.orography_cube.units)
Esempio n. 20
0
 def test_copy(self):
     """Test when given one cube copied."""
     result = spatial_coords_match(self.cube_a, self.cube_a.copy())
     self.assertTrue(result)
Esempio n. 21
0
 def test_basic(self):
     """Test bool return when given one cube twice."""
     result = spatial_coords_match(self.cube_a, self.cube_a)
     self.assertTrue(result)
    def _initialise_input_cubes(self, target_grid: Cube,
                                surface_altitude: Cube,
                                linke_turbidity: Cube) -> Tuple[Cube, Cube]:
        """Assign default values to input cubes where none have been passed, and ensure
        that all cubes are defined over consistent spatial grid.

        Args:
            target_grid:
                A cube containing the desired spatial grid.
            surface_altitude:
                Input surface altitude value.
            linke_turbidity:
                Input linke-turbidity value.

        Returns:
            - Cube containing surface altitude, defined on the same grid as target_grid.
            - Cube containing linke-turbidity, defined on the same grid as target_grid.

        Raises:
            ValueError:
                If surface_altitude or linke_turbidity have inconsistent spatial coords
                relative to target_grid.
        """
        if surface_altitude is None:
            # Create surface_altitude cube using target_grid as template.
            surface_altitude_data = np.zeros(shape=target_grid.shape,
                                             dtype=np.float32)
            surface_altitude = create_new_diagnostic_cube(
                name="surface_altitude",
                units="m",
                template_cube=target_grid,
                mandatory_attributes=generate_mandatory_attributes(
                    [target_grid]),
                optional_attributes=target_grid.attributes,
                data=surface_altitude_data,
            )
        else:
            if not spatial_coords_match([target_grid, surface_altitude]):
                raise ValueError(
                    "surface altitude spatial coordinates do not match target_grid"
                )

        if linke_turbidity is None:
            # Create linke_turbidity cube using target_grid as template.
            linke_turbidity_data = 3.0 * np.ones(shape=target_grid.shape,
                                                 dtype=np.float32)
            linke_turbidity = create_new_diagnostic_cube(
                name="linke_turbidity",
                units="1",
                template_cube=target_grid,
                mandatory_attributes=generate_mandatory_attributes(
                    [target_grid]),
                optional_attributes=target_grid.attributes,
                data=linke_turbidity_data,
            )
        else:
            if not spatial_coords_match([target_grid, linke_turbidity]):
                raise ValueError(
                    "linke-turbidity spatial coordinates do not match target_grid"
                )

        return surface_altitude, linke_turbidity
    def _extract_input_cubes(self, cubes: Union[CubeList, List[Cube]]) -> None:
        """
        Separates the input list into the required cubes for this plugin,
        detects whether snow, rain from hail or rain are required from the input phase-level
        cube name and appropriately initialises the percentile_plugin, sets
        the appropriate comparator operator for comparing with orography and
        the unique part of the output cube name.

        Converts units of falling_level_cube to that of orography_cube if
        necessary. Sets flag for snow, rain from hail or rain depending on name of
        falling_level_cube.

        Args:
            cubes:
                Contains cubes of the altitude of the phase-change level (this
                can be snow->sleet, hail->rain or sleet->rain) and the altitude of the
                orography. The name of the phase-change level cube must be
                "altitude_of_snow_falling_level", "altitude_of_rain_from_hail_falling_level" or
                "altitude_of_rain_falling_level". The name of the orography
                cube must be "surface_altitude".

        Raises:
            ValueError: If cubes with the expected names cannot be extracted.
            ValueError: If cubes does not have the expected length of 2.
            ValueError: If the extracted cubes do not have matching spatial
                        coordinates.
        """
        if isinstance(cubes, list):
            cubes = iris.cube.CubeList(cubes)
        if len(cubes) != 2:
            raise ValueError(f"Expected 2 cubes, found {len(cubes)}")

        if not spatial_coords_match(cubes):
            raise ValueError("Spatial coords mismatch between "
                             f"{cubes[0]} and "
                             f"{cubes[1]}")
        extracted_cube = cubes.extract("altitude_of_snow_falling_level")
        if extracted_cube:
            (self.falling_level_cube, ) = extracted_cube
            self.param = "snow"
            self.comparator = operator.gt
            self.get_discriminating_percentile = self.percentile_plugin(
                self.radius, percentiles=[80.0])
        elif cubes.extract("altitude_of_rain_falling_level"):
            extracted_cube = cubes.extract("altitude_of_rain_falling_level")
            (self.falling_level_cube, ) = extracted_cube
            self.param = "rain"
            self.comparator = operator.lt
            # We want rain that has come from sleet at or above the surface, so inverse of 80th
            # centile is the 20th centile.
            self.get_discriminating_percentile = self.percentile_plugin(
                self.radius, percentiles=[20.0])
        else:
            extracted_cube = cubes.extract(
                "altitude_of_rain_from_hail_falling_level")
            if not extracted_cube:
                raise ValueError(
                    "Could not extract a rain, rain from hail or snow falling-level "
                    f"cube from {cubes}")
            (self.falling_level_cube, ) = extracted_cube
            self.param = "rain_from_hail"
            self.comparator = operator.lt
            self.get_discriminating_percentile = self.percentile_plugin(
                self.radius, percentiles=[20.0])
        orography_name = "surface_altitude"
        extracted_cube = cubes.extract(orography_name)
        if extracted_cube:
            (self.orography_cube, ) = extracted_cube
        else:
            raise ValueError(f"Could not extract {orography_name} cube from "
                             f"{cubes}")

        if self.falling_level_cube.units != self.orography_cube.units:
            self.falling_level_cube = self.falling_level_cube.copy()
            self.falling_level_cube.convert_units(self.orography_cube.units)
Esempio n. 24
0
 def test_unmatching_multiple(self):
     """Test when given more than two cubes to test, these unmatching."""
     result = spatial_coords_match([self.cube_a, self.cube_b, self.cube_a])
     self.assertFalse(result)
Esempio n. 25
0
def apply_gridded_lapse_rate(temperature, lapse_rate, source_orog, dest_orog):
    """
    Function to apply a lapse rate adjustment to temperature data forecast
    at "source_orog" heights, to be applicable at "dest_orog" heights.

    Args:
        temperature (iris.cube.Cube):
            Input temperature field to be adjusted
        lapse_rate (iris.cube.Cube):
            Cube of pre-calculated lapse rates (units modified in place), which
            must match the temperature cube
        source_orog (iris.cube.Cube):
            2D cube of source orography heights (units modified in place)
        dest_orog (iris.cube.Cube):
            2D cube of destination orography heights (units modified in place)

    Returns:
        iris.cube.Cube:
            Lapse-rate adjusted temperature field
    """
    # check dimensions and coordinates match on input cubes
    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()))

    if not spatial_coords_match(temperature, source_orog):
        raise ValueError('Source orography spatial coordinates do not match '
                         'temperature grid')
    if not spatial_coords_match(temperature, dest_orog):
        raise ValueError(
            'Destination orography spatial coordinates do not match '
            'temperature grid')

    # calculate height difference (in m) on which to adjust
    source_orog.convert_units('m')
    dest_orog.convert_units('m')
    orog_diff = (
        next(
            dest_orog.slices(
                [dest_orog.coord(axis='y'),
                 dest_orog.coord(axis='x')])) -
        next(
            source_orog.slices(
                [source_orog.coord(axis='y'),
                 source_orog.coord(axis='x')])))

    # convert lapse rate cube to K m-1
    lapse_rate.convert_units('K m-1')

    # adjust temperatures
    adjusted_temperature = []
    for lrsubcube, tempsubcube in zip(
            lapse_rate.slices(
                [lapse_rate.coord(axis='y'),
                 lapse_rate.coord(axis='x')]),
            temperature.slices(
                [temperature.coord(axis='y'),
                 temperature.coord(axis='x')])):

        # calculate temperature adjustment in K
        adjustment = multiply(orog_diff, lrsubcube)

        # apply adjustment to each spatial slice of the temperature cube
        newcube = tempsubcube.copy()
        newcube.convert_units('K')
        newcube.data += adjustment.data
        adjusted_temperature.append(newcube)

    return iris.cube.CubeList(adjusted_temperature).merge_cube()
Esempio n. 26
0
    def process(self, cube: Cube) -> Cube:
        """
        Produces the vicinity processed data. The input data is sliced to
        yield y-x slices to which the maximum_within_vicinity method is applied.
        The different vicinity radii (if multiple) are looped over and a
        coordinate recording the radius used is added to each resulting cube.
        A single cube is returned with the leading coordinates of the input cube
        preserved. If a single vicinity radius is provided, a new scalar
        radius_of_vicinity coordinate will be found on the returned cube. If
        multiple radii are provided, this coordinate will be a dimension
        coordinate following any probabilistic / realization coordinates.

        Args:
            cube:
                Thresholded cube.

        Returns:
            Cube containing the occurrences within a vicinity for each radius,
            calculated for each yx slice, which have been merged to yield a
            single cube.

        Raises:
            ValueError: Cube and land mask have differing spatial coordinates.
        """
        if self.land_mask_cube and not spatial_coords_match(
            [cube, self.land_mask_cube]):
            raise ValueError(
                "Supplied cube do not have the same spatial coordinates and land mask"
            )

        if not self.native_grid_point_radius:
            grid_point_radii = [
                distance_to_number_of_grid_cells(cube, radius)
                for radius in self.radii
            ]
        else:
            grid_point_radii = self.radii

        radii_cubes = CubeList()

        # List of non-spatial dimensions to restore as leading on the output.
        leading_dimensions = [
            crd.name() for crd in cube.coords(dim_coords=True)
            if not crd.coord_system
        ]

        for radius, grid_point_radius in zip(self.radii, grid_point_radii):
            max_cubes = CubeList([])
            for cube_slice in cube.slices(
                [cube.coord(axis="y"),
                 cube.coord(axis="x")]):
                max_cubes.append(
                    self.maximum_within_vicinity(cube_slice,
                                                 grid_point_radius))
            result_cube = max_cubes.merge_cube()

            # Put dimensions back if they were there before.
            result_cube = check_cube_coordinates(cube, result_cube)

            # Add a coordinate recording the vicinity radius applied to the data.
            self._add_vicinity_coordinate(result_cube, radius)

            radii_cubes.append(result_cube)

        # Merge cubes produced for each vicinity radius.
        result_cube = radii_cubes.merge_cube()

        # Enforce order of leading dimensions on the output to match the input.
        enforce_coordinate_ordering(result_cube, leading_dimensions)

        if is_probability(result_cube):
            result_cube.rename(in_vicinity_name_format(result_cube.name()))
        else:
            result_cube.rename(f"{result_cube.name()}_in_vicinity")

        return result_cube
Esempio n. 27
0
 def test_single_cube(self):
     """Test that True is returned if a single cube is provided as input."""
     result = spatial_coords_match([self.cube_a])
     self.assertTrue(result)