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." )
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)
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
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)
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)
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)
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()
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)
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)
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
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)
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
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
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
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
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]}")
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)
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")
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)
def test_copy(self): """Test when given one cube copied.""" result = spatial_coords_match(self.cube_a, self.cube_a.copy()) self.assertTrue(result)
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)
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)
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()
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
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)