def ensure_monotonic_increase_in_chosen_direction(self, cube): """Ensure that the chosen coordinate is monotonically increasing in the specified direction. Args: cube (Iris.cube.Cube): The cube containing the coordinate to check. Note that the input cube will be modified by this method. Returns: cube (Iris.cube.Cube): The cube containing a coordinate that is monotonically increasing in the desired direction. """ coord_name = self.coord_name_to_integrate direction = self.direction_of_integration increasing_order = np.all(np.diff(cube.coord(coord_name).points) > 0) if increasing_order and direction == "positive": pass elif increasing_order and direction == "negative": cube = sort_coord_in_cube(cube, coord_name, order="descending") elif not increasing_order and direction == "positive": cube = sort_coord_in_cube(cube, coord_name) elif not increasing_order and direction == "negative": pass return cube
def process( self, cube: Cube, coord_name: str, inverse_ordering: bool = False, ) -> Cube: """ Calculate nonlinear weights for a given cube and coord. Args: cube: Cube to be blended across the coord. coord_name: Name of coordinate in the cube to be blended. inverse_ordering: The input cube blend coordinate will be in ascending order, so that calculated blend weights decrease with increasing value. For eg cycle blending by forecast reference time, we wish to weight more recent cubes more highly. This flag gives the option to reverse the blend coordinate order so as to have higher weights for the higher values. Returns: 1D cube of normalised (sum = 1.0) weights matching input dimension to be blended Raises: TypeError : input is not a cube """ if not isinstance(cube, iris.cube.Cube): msg = ("The first argument must be an instance of " "iris.cube.Cube but is" " {0:s}".format(str(type(cube)))) raise TypeError(msg) if inverse_ordering: # make a copy of the input cube from which to calculate weights inverted_cube = cube.copy() inverted_cube = sort_coord_in_cube(inverted_cube, coord_name, descending=True) cube = inverted_cube weights = self.nonlinear_weights(len(cube.coord(coord_name).points)) weights_cube = WeightsUtilities.build_weights_cube( cube, weights, coord_name) if inverse_ordering: # re-sort the weights cube so that it is in ascending order of # blend coordinate (and hence matches the input cube) weights_cube = sort_coord_in_cube(weights_cube, coord_name) return weights_cube
def _regrid_variable(self, var_cube, unit): """ Sorts spatial coordinates in ascending order, regrids the input variable onto the topography grid and converts to the required units. This function does not modify the input variable cube. Args: var_cube (iris.cube.Cube): Cube containing input variable data unit (str): Required unit for this variable Returns: out_cube (iris.cube.Cube): Cube containing regridded variable data """ for axis in ['x', 'y']: var_cube = sort_coord_in_cube(var_cube, var_cube.coord(axis=axis)) var_cube = enforce_coordinate_ordering( var_cube, [var_cube.coord(axis='y').name(), var_cube.coord(axis='x').name()]) regridder = iris.analysis.Linear() out_cube = (var_cube.copy(var_cube.data.astype(np.float32))).regrid( self.topography, regridder) out_cube.convert_units(unit) return out_cube
def test_metadata(self): """Check output metadata on both cubes is as expected""" hi_res_attributes = self.temperature.attributes for key, val in self.plugin.topography.attributes.items(): hi_res_attributes[key] = val tref = sort_coord_in_cube(self.temperature, self.temperature.coord(axis='y')) output, regridded_output = self.plugin._create_output_cubes( self.orogenh, self.temperature) for axis in ['x', 'y']: self.assertEqual(output.coord(axis=axis), self.plugin.topography.coord(axis=axis)) self.assertEqual(regridded_output.coord(axis=axis), tref.coord(axis=axis)) for cube in [output, regridded_output]: self.assertEqual(cube.name(), 'orographic_enhancement') self.assertEqual(cube.units, 'mm h-1') for t_coord in [ 'time', 'forecast_period', 'forecast_reference_time' ]: self.assertEqual(cube.coord(t_coord), self.temperature.coord(t_coord)) self.assertDictEqual(regridded_output.attributes, self.temperature.attributes) self.assertDictEqual(output.attributes, hi_res_attributes)
def test_warn_raised_for_circular_coordinate(self, warning_list=None): """Test that a warning is successfully raised when circular coordinates are sorted.""" self.ascending_cube.data[:, 0, 0] = 6.0 coord_name = "latitude" self.ascending_cube.coord(coord_name).circular = True result = sort_coord_in_cube(self.ascending_cube, coord_name, descending=True) self.assertTrue(any(item.category == UserWarning for item in warning_list)) warning_msg = "The latitude coordinate is circular." self.assertTrue(any(warning_msg in str(item) for item in warning_list)) self.assertIsInstance(result, iris.cube.Cube)
def setUp(self): """Set up input cubes""" temperature = np.arange(6).reshape(2, 3) self.temperature_cube = set_up_variable_cube(temperature) orography = np.array([[20., 30., 40., 30., 25., 25.], [30., 50., 80., 60., 50., 45.], [50., 65., 90., 70., 60., 50.], [45., 60., 85., 65., 55., 45.]]) orography_cube = set_up_orography_cube(orography) self.plugin = OrographicEnhancement() self.plugin.topography = sort_coord_in_cube( orography_cube, orography_cube.coord(axis='y'))
def test_ascending_then_ascending(self): """Test that the sorting successfully sorts the cube based on the points within the given coordinate. The points in the resulting cube should now be in ascending order.""" expected_data = self.data coord_name = "height" result = sort_coord_in_cube(self.ascending_cube, coord_name) self.assertIsInstance(result, iris.cube.Cube) self.assertEqual(self.ascending_cube.coord_dims(coord_name), result.coord_dims(coord_name)) self.assertArrayAlmostEqual(self.ascending_height_points, result.coord(coord_name).points) self.assertArrayAlmostEqual(result.data, expected_data)
def ensure_ascending_coord(cube: Cube) -> Cube: """ Check if cube coordinates ascending. if not, make it ascending Args: cube: Input source cube. Returns: Cube with ascending coordinates """ for ax in ("x", "y"): if cube.coord(axis=ax).points[0] > cube.coord(axis=ax).points[-1]: cube = sort_coord_in_cube(cube, cube.coord(axis=ax).standard_name) return cube
def ensure_monotonic_increase_in_chosen_direction(self, cube: Cube) -> Cube: """Ensure that the chosen coordinate is monotonically increasing in the specified direction. Args: cube: The cube containing the coordinate to check. Note that the input cube will be modified by this method. Returns: The cube containing a coordinate that is monotonically increasing in the desired direction. """ coord_name = self.coord_name_to_integrate increasing_order = np.all(np.diff(cube.coord(coord_name).points) > 0) if increasing_order and not self.positive_integration: cube = sort_coord_in_cube(cube, coord_name, descending=True) if not increasing_order and self.positive_integration: cube = sort_coord_in_cube(cube, coord_name) return cube
def test_auxcoord(self): """Test that the above sorting is successful when an AuxCoord is used.""" expected_data = self.data coord_name = "height_aux" height_coord = self.ascending_cube.coord("height") (height_coord_index, ) = self.ascending_cube.coord_dims("height") new_coord = AuxCoord(height_coord.points, long_name=coord_name) self.ascending_cube.add_aux_coord(new_coord, height_coord_index) result = sort_coord_in_cube(self.ascending_cube, coord_name) self.assertIsInstance(result, iris.cube.Cube) self.assertEqual(self.ascending_cube.coord_dims(coord_name), result.coord_dims(coord_name)) self.assertArrayAlmostEqual(self.ascending_height_points, result.coord(coord_name).points) self.assertArrayAlmostEqual(result.data, expected_data)
def _set_up_height_cube(height_points, ascending=True): """Create cube of temperatures decreasing with height""" data = 280 * np.ones((3, 3, 3), dtype=np.float32) data[1, :] = 278 data[2, :] = 276 cube = set_up_variable_cube(data[0].astype(np.float32)) height_points = np.sort(height_points) cube = add_coordinate(cube, height_points, "height", coord_units="m") cube.coord("height").attributes["positive"] = "up" cube.data = data.astype(np.float32) if not ascending: cube = sort_coord_in_cube(cube, "height", descending=True) cube.coord("height").attributes["positive"] = "down" return cube
def setUp(self): """Set up a plugin instance, data array and cubes""" self.plugin = OrographicEnhancement() topography = set_up_orography_cube(np.zeros((3, 4), dtype=np.float32)) self.plugin.topography = sort_coord_in_cube(topography, topography.coord(axis='y')) self.temperature = set_up_variable_cube(np.full((2, 4), 280.15), units='kelvin', xo=398000.) self.temperature.attributes['institution'] = 'Met Office' self.temperature.attributes['source'] = 'Met Office Unified Model' self.temperature.attributes['mosg__grid_type'] = 'standard' self.temperature.attributes['mosg__grid_version'] = '1.2.0' self.temperature.attributes['mosg__grid_domain'] = 'uk_extended' self.temperature.attributes['mosg__model_configuration'] = 'uk_det' self.orogenh = np.array([[1.1, 1.2, 1.5, 1.4], [1.0, 1.3, 1.4, 1.6], [0.8, 0.9, 1.2, 0.9]])
def test_descending_then_ascending(self): """Test that the sorting successfully sorts the cube based on the points within the given coordinate. The points in the resulting cube should now be in ascending order.""" expected_data = np.array([[[[3.00, 3.00, 3.00], [3.00, 3.00, 3.00], [3.00, 3.00, 3.00]]], [[[2.00, 2.00, 2.00], [2.00, 2.00, 2.00], [2.00, 2.00, 2.00]]], [[[1.00, 1.00, 1.00], [1.00, 1.00, 1.00], [1.00, 1.00, 1.00]]]]) coord_name = "height" result = sort_coord_in_cube(self.descending_cube, coord_name) self.assertIsInstance(result, iris.cube.Cube) self.assertEqual(self.ascending_cube.coord_dims(coord_name), result.coord_dims(coord_name)) self.assertArrayAlmostEqual(self.ascending_height_points, result.coord(coord_name).points) self.assertDictEqual( result.coord(coord_name).attributes, {"positive": "up"}) self.assertArrayAlmostEqual(result.data, expected_data)
def test_latitude(self): """Test that the sorting successfully sorts the cube based on the points within the given coordinate (latitude). The points in the resulting cube should now be in descending order.""" expected_data = np.array([ [[1.00, 1.00, 1.00], [1.00, 1.00, 1.00], [6.00, 1.00, 1.00]], [[2.00, 2.00, 2.00], [2.00, 2.00, 2.00], [6.00, 2.00, 2.00]], [[3.00, 3.00, 3.00], [3.00, 3.00, 3.00], [6.00, 3.00, 3.00]], ]) self.ascending_cube.data[:, 0, 0] = 6.0 expected_points = np.flip(self.ascending_cube.coord("latitude").points) coord_name = "latitude" result = sort_coord_in_cube(self.ascending_cube, coord_name, descending=True) self.assertIsInstance(result, iris.cube.Cube) self.assertEqual(self.ascending_cube.coord_dims(coord_name), result.coord_dims(coord_name)) self.assertArrayAlmostEqual(expected_points, result.coord(coord_name).points) self.assertArrayAlmostEqual(result.data, expected_data)
def setUp(self): """Set up a plugin instance, data array and cubes""" self.plugin = OrographicEnhancement() topography = set_up_orography_cube(np.zeros((3, 4), dtype=np.float32)) self.plugin.topography = sort_coord_in_cube(topography, topography.coord(axis="y")) t_attributes = { "institution": "Met Office", "source": "Met Office Unified Model", "mosg__grid_type": "standard", "mosg__grid_version": "1.2.0", "mosg__grid_domain": "uk_extended", "mosg__model_configuration": "uk_det", } self.temperature = set_up_variable_cube( np.full((2, 4), 280.15, dtype=np.float32), units="kelvin", xo=398000.0, attributes=t_attributes, ) self.orogenh = np.array([[1.1, 1.2, 1.5, 1.4], [1.0, 1.3, 1.4, 1.6], [0.8, 0.9, 1.2, 0.9]])
def process(self, cube, weights=None): """Calculate weighted blend across the chosen coord, for either probabilistic or percentile data. If there is a percentile coordinate on the cube, it will blend using the PercentileBlendingAggregator but the percentile coordinate must have at least two points. Args: cube (iris.cube.Cube): Cube to blend across the coord. weights (iris.cube.Cube): Cube of blending weights. This will have 1 or 3 dimensions, corresponding either to blend dimension on the input cube with or without and additional 2 spatial dimensions. If None, the input cube is blended with equal weights across the blending dimension. Returns: iris.cube.Cube: Containing the weighted blend across the chosen coordinate (typically forecast reference time or model). Raises: TypeError : If the first argument not a cube. CoordinateNotFoundError : If coordinate to be collapsed not found in cube. CoordinateNotFoundError : If coordinate to be collapsed not found in provided weights cube. ValueError : If coordinate to be collapsed is not a dimension. """ if not isinstance(cube, iris.cube.Cube): msg = ("The first argument must be an instance of iris.cube.Cube " "but is {}.".format(type(cube))) raise TypeError(msg) if not cube.coords(self.blend_coord): msg = "Coordinate to be collapsed not found in cube." raise CoordinateNotFoundError(msg) output_dims = get_dim_coord_names( next(cube.slices_over(self.blend_coord))) self.blend_coord = find_blend_dim_coord(cube, self.blend_coord) # Ensure input cube and weights cube are ordered equivalently along # blending coordinate. cube = sort_coord_in_cube(cube, self.blend_coord) if weights is not None: if not weights.coords(self.blend_coord): msg = "Coordinate to be collapsed not found in weights cube." raise CoordinateNotFoundError(msg) weights = sort_coord_in_cube(weights, self.blend_coord) # Check that the time coordinate is single valued if required. self.check_compatible_time_points(cube) # Do blending and update metadata if self.check_percentile_coord(cube): enforce_coordinate_ordering(cube, [self.blend_coord, "percentile"]) result = self.percentile_weighted_mean(cube, weights) else: enforce_coordinate_ordering(cube, [self.blend_coord]) result = self.weighted_mean(cube, weights) # Reorder resulting dimensions to match input enforce_coordinate_ordering(result, output_dims) return result
def setup_cubes_for_process(self, spatial_grid="equalarea"): data = np.ones((5, 5), dtype=np.float32) data[2, 2] = 100.0 self.orog = set_up_variable_cube( data, name="surface_altitude", units="m", spatial_grid=spatial_grid ) self.land_sea = set_up_variable_cube( np.ones_like(data, dtype=np.int8), name="land_binary_mask", units=1, spatial_grid=spatial_grid, ) # Note the values below are ordered at [5, 195, 200] m. wbt_0 = np.full_like(data, fill_value=271.46216) wbt_0[2, 2] = 270.20343 wbt_1 = np.full_like(data, fill_value=274.4207) wbt_1[2, 2] = 271.46216 wbt_2 = np.full_like(data, fill_value=275.0666) wbt_2[2, 2] = 274.4207 wbt_data = np.array( [ np.broadcast_to(wbt_0, (3, 5, 5)), np.broadcast_to(wbt_1, (3, 5, 5)), np.broadcast_to(wbt_2, (3, 5, 5)), ], dtype=np.float32, ) # Note the values below are ordered at [5, 195] m. wbti_0 = np.full_like(data, fill_value=128.68324) wbti_0[2, 2] = 3.1767120 wbti_0[1:4, 1:4] = 100.0 wbti_1 = np.full_like(data, fill_value=7.9681854) wbti_1[2, 2] = 3.1767120 wbti_data = np.array( [np.broadcast_to(wbti_0, (3, 5, 5)), np.broadcast_to(wbti_1, (3, 5, 5))], dtype=np.float32, ) height_points = [5.0, 195.0, 200.0] height_attribute = {"positive": "up"} wet_bulb_temperature = set_up_variable_cube( data, spatial_grid=spatial_grid, name="wet_bulb_temperature" ) wet_bulb_temperature = add_coordinate( wet_bulb_temperature, [0, 1, 2], "realization" ) self.wet_bulb_temperature_cube = add_coordinate( wet_bulb_temperature, height_points, "height", coord_units="m", attributes=height_attribute, ) self.wet_bulb_temperature_cube.data = wbt_data # Note that the iris cubelist merge_cube operation sorts the coordinate # being merged into ascending order. The cube created below is thus # in the incorrect height order, i.e. [5, 195] instead of [195, 5]. # There is a function in the the PhaseChangeLevel plugin that ensures # the height coordinate is in descending order. This is tested here by # creating test cubes with both orders. height_attribute = {"positive": "down"} wet_bulb_integral = set_up_variable_cube( data, spatial_grid=spatial_grid, name="wet_bulb_temperature_integral", units="K m", ) wet_bulb_integral = add_coordinate(wet_bulb_integral, [0, 1, 2], "realization") self.wet_bulb_integral_cube_inverted = add_coordinate( wet_bulb_integral, height_points[0:2], "height", coord_units="m", attributes=height_attribute, ) self.wet_bulb_integral_cube_inverted.data = wbti_data self.wet_bulb_integral_cube = sort_coord_in_cube( self.wet_bulb_integral_cube_inverted, "height", descending=True ) self.expected_snow_sleet = np.full( (3, 5, 5), fill_value=66.88566, dtype=np.float32 ) self.expected_snow_sleet[:, 1:4, 1:4] = 26.645035 self.expected_snow_sleet[:, 2, 2] = 124.623375
def process(self, cube, weights=None, cycletime=None, attributes_dict=None): """Calculate weighted blend across the chosen coord, for either probabilistic or percentile data. If there is a percentile coordinate on the cube, it will blend using the PercentileBlendingAggregator but the percentile coordinate must have at least two points. Args: cube (iris.cube.Cube): Cube to blend across the coord. weights (iris.cube.Cube): Cube of blending weights. If None, the diagnostic cube is blended with equal weights across the blending dimension. cycletime (str): The cycletime in a YYYYMMDDTHHMMZ format e.g. 20171122T0100Z. This can be used to manually set the forecast reference time on the output blended cube. If not set, the most recent forecast reference time from the contributing cubes is used. attributes_dict (dict or None): Changes to cube attributes to be applied after blending. See :func:`~improver.metadata.amend.amend_attributes` for required format. If mandatory attributes are not set here, default values are used. Returns: iris.cube.Cube: containing the weighted blend across the chosen coord. Raises: TypeError : If the first argument not a cube. CoordinateNotFoundError : If coordinate to be collapsed not found in cube. CoordinateNotFoundError : If coordinate to be collapsed not found in provided weights cube. ValueError : If coordinate to be collapsed is not a dimension. """ if not isinstance(cube, iris.cube.Cube): msg = ("The first argument must be an instance of iris.cube.Cube " "but is {}.".format(type(cube))) raise TypeError(msg) if not cube.coords(self.blend_coord): msg = "Coordinate to be collapsed not found in cube." raise CoordinateNotFoundError(msg) blend_coord_dims = cube.coord_dims(self.blend_coord) if not blend_coord_dims: raise ValueError("Blending coordinate {} has no associated " "dimension".format(self.blend_coord)) # Ensure input cube and weights cube are ordered equivalently along # blending coordinate. cube = sort_coord_in_cube(cube, self.blend_coord) if weights is not None: if not weights.coords(self.blend_coord): msg = "Coordinate to be collapsed not found in weights cube." raise CoordinateNotFoundError(msg) weights = sort_coord_in_cube(weights, self.blend_coord) # Check that the time coordinate is single valued if required. self.check_compatible_time_points(cube) # Check to see if the data is percentile data perc_coord = self.check_percentile_coord(cube) # Establish metadata changes to be made after blending self.cycletime_point = ( self._get_cycletime_point(cube, cycletime) if self.blend_coord in ["forecast_reference_time", "model_id"] else None) self._set_coords_to_remove(cube) # Do blending and update metadata if perc_coord: result = self.percentile_weighted_mean(cube, weights, perc_coord) else: result = self.weighted_mean(cube, weights) self._update_blended_metadata(result, attributes_dict) # Checks the coordinate dimensions match the first relevant cube in the unblended cubeList. result = check_cube_coordinates( next(cube.slices_over(self.blend_coord)), result) return result
def process(self, wet_bulb_temperature, wet_bulb_integral, orog, land_sea_mask): """ Use the wet bulb temperature integral to find the altitude at which a phase change occurs (e.g. snow to sleet). This is achieved by finding the height above sea level at which the integral matches an empirical threshold that is expected to correspond with the phase change. This empirical threshold is the falling_level_threshold. Fill in missing data appropriately. Args: wet_bulb_temperature (iris.cube.Cube): Cube of wet bulb temperatures on height levels. wet_bulb_integral (iris.cube.Cube): Cube of wet bulb temperature integral (Kelvin-metres). orog (iris.cube.Cube): Cube of orography (m). land_sea_mask (iris.cube.Cube): Cube containing a binary land-sea mask. Returns: iris.cube.Cube: Cube of phase change level above sea level (asl). """ wet_bulb_temperature.convert_units('celsius') wet_bulb_integral.convert_units('K m') # Ensure the wet bulb integral cube's height coordinate is in # descending order wet_bulb_integral = sort_coord_in_cube(wet_bulb_integral, 'height', order='descending') # Find highest height from height bounds. height_bounds = wet_bulb_integral.coord('height').bounds heights = wet_bulb_temperature.coord('height').points if height_bounds is None: highest_height = heights[-1] else: highest_height = height_bounds[0][-1] # Firstly we need to slice over height, x and y x_coord = wet_bulb_integral.coord(axis='x').name() y_coord = wet_bulb_integral.coord(axis='y').name() orography = next(orog.slices([y_coord, x_coord])) orog_data = orography.data land_sea_data = next(land_sea_mask.slices([y_coord, x_coord])).data phase_change = iris.cube.CubeList([]) slice_list = ['height', y_coord, x_coord] for wb_integral, wet_bulb_temp in zip( wet_bulb_integral.slices(slice_list), wet_bulb_temperature.slices(slice_list)): height_points = wb_integral.coord('height').points # Calculate phase change level above sea level. phase_change_cube = wb_integral[0] phase_change_cube.rename('altitude_of_{}_level'.format( self.phase_change_name)) phase_change_cube.units = 'm' phase_change_cube.remove_coord('height') phase_change_cube.data = self.find_falling_level( wb_integral.data, orog_data, height_points) # Fill in missing data self.fill_in_high_phase_change_falling_levels( phase_change_cube.data, orog_data, wb_integral.data.max(axis=0), highest_height) self.fill_in_sea_points(phase_change_cube.data, land_sea_data, wb_integral.data.max(axis=0), wet_bulb_temp.data, heights) max_nbhood_orog = self.find_max_in_nbhood_orography(orography) updated_phase_cl = self.fill_in_by_horizontal_interpolation( phase_change_cube.data, max_nbhood_orog.data, orog_data) points = np.where(~np.isfinite(phase_change_cube.data)) phase_change_cube.data[points] = updated_phase_cl[points] # Fill in any remaining points with missing data: remaining_points = np.where(np.isnan(phase_change_cube.data)) phase_change_cube.data[remaining_points] = self.missing_data phase_change.append(phase_change_cube) phase_change_level = phase_change.merge_cube() return phase_change_level
def process(self, cubes): """ Use the wet bulb temperature integral to find the altitude at which a phase change occurs (e.g. snow to sleet). This is achieved by finding the height above sea level at which the integral matches an empirical threshold that is expected to correspond with the phase change. This empirical threshold is the falling_level_threshold. Fill in missing data appropriately. Args: cubes (iris.cube.CubeList or list of iris.cube.Cube) containing: wet_bulb_temperature (iris.cube.Cube): Cube of wet bulb temperatures on height levels. wet_bulb_integral (iris.cube.Cube): Cube of wet bulb temperature integral (Kelvin-metres). orog (iris.cube.Cube): Cube of orography (m). land_sea_mask (iris.cube.Cube): Cube containing a binary land-sea mask, with land points set to one and sea points set to zero. Returns: iris.cube.Cube: Cube of phase change level above sea level (asl). """ names_to_extract = [ "wet_bulb_temperature", "wet_bulb_temperature_integral", "surface_altitude", "land_binary_mask", ] if len(cubes) != len(names_to_extract): raise ValueError( f"Expected {len(names_to_extract)} cubes, found {len(cubes)}") wet_bulb_temperature, wet_bulb_integral, orog, land_sea_mask = tuple( CubeList(cubes).extract_strict(n) for n in names_to_extract) wet_bulb_temperature.convert_units("celsius") wet_bulb_integral.convert_units("K m") # Ensure the wet bulb integral cube's height coordinate is in # descending order wet_bulb_integral = sort_coord_in_cube(wet_bulb_integral, "height", descending=True) # Find highest height from height bounds. wbt_height_points = wet_bulb_temperature.coord("height").points if wet_bulb_integral.coord("height").bounds is None: highest_height = wbt_height_points[-1] else: highest_height = wet_bulb_integral.coord("height").bounds[0][-1] # Firstly we need to slice over height, x and y x_coord = wet_bulb_integral.coord(axis="x").name() y_coord = wet_bulb_integral.coord(axis="y").name() orography = next(orog.slices([y_coord, x_coord])) land_sea_data = next(land_sea_mask.slices([y_coord, x_coord])).data max_nbhood_orog = self.find_max_in_nbhood_orography(orography) phase_change = None slice_list = ["height", y_coord, x_coord] for wb_integral, wet_bulb_temp in zip( wet_bulb_integral.slices(slice_list), wet_bulb_temperature.slices(slice_list), ): phase_change_data = self._calculate_phase_change_level( wet_bulb_temp.data, wb_integral.data, orography.data, max_nbhood_orog.data, land_sea_data, wbt_height_points, wb_integral.coord("height").points, highest_height, ) # preserve dimensionality of input cube (in case of scalar or # length 1 dimensions) if phase_change is None: phase_change = phase_change_data elif not isinstance(phase_change, list): phase_change = [phase_change] phase_change.append(phase_change_data) else: phase_change.append(phase_change_data) phase_change_level = self.create_phase_change_level_cube( wet_bulb_temperature, np.ma.masked_array(phase_change, dtype=np.float32)) return phase_change_level
def setUp(self): """Set up orography and land-sea mask cubes. Also create temperature, pressure, and relative humidity cubes that contain multiple height levels.""" data = np.ones((3, 3), dtype=np.float32) self.orog = set_up_variable_cube( data, name='surface_altitude', units='m', spatial_grid='equalarea') self.land_sea = set_up_variable_cube( data, name='land_binary_mask', units=1, spatial_grid='equalarea') wbt_data = np.array( [[[[271.46216, 271.46216, 271.46216], [271.46216, 270.20343, 271.46216], [271.46216, 271.46216, 271.46216]], [[271.46216, 271.46216, 271.46216], [271.46216, 270.20343, 271.46216], [271.46216, 271.46216, 271.46216]]], [[[274.4207, 274.4207, 274.4207], [274.4207, 271.46216, 274.4207], [274.4207, 274.4207, 274.4207]], [[274.4207, 274.4207, 274.4207], [274.4207, 271.46216, 274.4207], [274.4207, 274.4207, 274.4207]]], [[[275.0666, 275.0666, 275.0666], [275.0666, 274.4207, 275.0666], [275.0666, 275.0666, 275.0666]], [[275.0666, 275.0666, 275.0666], [275.0666, 274.4207, 275.0666], [275.0666, 275.0666, 275.0666]]]], dtype=np.float32) # Note the values below are ordered at [5, 195] m. wbti_data = np.array( [[[[128.68324, 128.68324, 128.68324], [128.68324, 3.176712, 128.68324], [128.68324, 128.68324, 128.68324]], [[128.68324, 128.68324, 128.68324], [128.68324, 3.176712, 128.68324], [128.68324, 128.68324, 128.68324]]], [[[7.9681854, 7.9681854, 7.9681854], [7.9681854, 3.176712, 7.9681854], [7.9681854, 7.9681854, 7.9681854]], [[7.9681854, 7.9681854, 7.9681854], [7.9681854, 3.176712, 7.9681854], [7.9681854, 7.9681854, 7.9681854]]]], dtype=np.float32) height_points = [5., 195., 200.] height_attribute = {"positive": "up"} wet_bulb_temperature = set_up_variable_cube( data, spatial_grid='equalarea', name='wet_bulb_temperature') wet_bulb_temperature = add_coordinate( wet_bulb_temperature, [0, 1], 'realization') self.wet_bulb_temperature_cube = add_coordinate( wet_bulb_temperature, height_points, 'height', coord_units='m', attributes=height_attribute) self.wet_bulb_temperature_cube.data = wbt_data # Note that the iris cubelist merge_cube operation sorts the coordinate # being merged into ascending order. The cube created below is thus # in the incorrect height order, i.e. [5, 195] instead of [195, 5]. # There is a function in the the PhaseChangeLevel plugin that ensures # the height coordinate is in descending order. This is tested here by # creating test cubes with both orders. height_attribute = {"positive": "down"} wet_bulb_integral = set_up_variable_cube( data, spatial_grid='equalarea', name='wet_bulb_temperature_integral', units='K m',) wet_bulb_integral = add_coordinate( wet_bulb_integral, [0, 1], 'realization') self.wet_bulb_integral_cube_inverted = add_coordinate( wet_bulb_integral, height_points[0:2], 'height', coord_units='m', attributes=height_attribute) self.wet_bulb_integral_cube_inverted.data = wbti_data self.wet_bulb_integral_cube = sort_coord_in_cube( self.wet_bulb_integral_cube_inverted, 'height', order='descending')
def setUp(self): """Set up orography and land-sea mask cubes. Also create temperature, pressure, and relative humidity cubes that contain multiple height levels.""" data = np.ones((3, 3), dtype=np.float32) self.orog = set_up_variable_cube(data, name="surface_altitude", units="m", spatial_grid="equalarea") self.land_sea = set_up_variable_cube(data, name="land_binary_mask", units=1, spatial_grid="equalarea") wbt_0 = np.array([ [271.46216, 271.46216, 271.46216], [271.46216, 270.20343, 271.46216], [271.46216, 271.46216, 271.46216], ]) wbt_1 = np.array([ [274.4207, 274.4207, 274.4207], [274.4207, 271.46216, 274.4207], [274.4207, 274.4207, 274.4207], ]) wbt_2 = np.array([ [275.0666, 275.0666, 275.0666], [275.0666, 274.4207, 275.0666], [275.0666, 275.0666, 275.0666], ]) wbt_data = np.array( [ np.broadcast_to(wbt_0, (3, 3, 3)), np.broadcast_to(wbt_1, (3, 3, 3)), np.broadcast_to(wbt_2, (3, 3, 3)), ], dtype=np.float32, ) # Note the values below are ordered at [5, 195] m. wbti_0 = np.array([ [128.68324, 128.68324, 128.68324], [128.68324, 3.176712, 128.68324], [128.68324, 128.68324, 128.68324], ]) wbti_1 = np.array([ [7.9681854, 7.9681854, 7.9681854], [7.9681854, 3.176712, 7.9681854], [7.9681854, 7.9681854, 7.9681854], ]) wbti_data = np.array( [ np.broadcast_to(wbti_0, (3, 3, 3)), np.broadcast_to(wbti_1, (3, 3, 3)) ], dtype=np.float32, ) height_points = [5.0, 195.0, 200.0] height_attribute = {"positive": "up"} wet_bulb_temperature = set_up_variable_cube( data, spatial_grid="equalarea", name="wet_bulb_temperature") wet_bulb_temperature = add_coordinate(wet_bulb_temperature, [0, 1, 2], "realization") self.wet_bulb_temperature_cube = add_coordinate( wet_bulb_temperature, height_points, "height", coord_units="m", attributes=height_attribute, ) self.wet_bulb_temperature_cube.data = wbt_data # Note that the iris cubelist merge_cube operation sorts the coordinate # being merged into ascending order. The cube created below is thus # in the incorrect height order, i.e. [5, 195] instead of [195, 5]. # There is a function in the the PhaseChangeLevel plugin that ensures # the height coordinate is in descending order. This is tested here by # creating test cubes with both orders. height_attribute = {"positive": "down"} wet_bulb_integral = set_up_variable_cube( data, spatial_grid="equalarea", name="wet_bulb_temperature_integral", units="K m", ) wet_bulb_integral = add_coordinate(wet_bulb_integral, [0, 1, 2], "realization") self.wet_bulb_integral_cube_inverted = add_coordinate( wet_bulb_integral, height_points[0:2], "height", coord_units="m", attributes=height_attribute, ) self.wet_bulb_integral_cube_inverted.data = wbti_data self.wet_bulb_integral_cube = sort_coord_in_cube( self.wet_bulb_integral_cube_inverted, "height", descending=True) self.expected_snow_sleet = np.ones( (3, 3, 3), dtype=np.float32) * 66.88566
def _create_output_cubes(self, orogenh_data, reference_cube): """ Create two output cubes of orographic enhancement on different grids. Casts coordinate points and bounds explicitly to np.float32. Args: orogenh_data (numpy.ndarray): Orographic enhancement value in mm h-1 reference_cube (iris.cube.Cube): Cube with the correct time and forecast period coordinates on the UK standard grid Returns: (tuple): tuple containing: **orogenh** (iris.cube.Cube): Orographic enhancement cube on 1 km UKPP grid (m s-1) **orogenh_standard_grid** (iris.cube.Cube): Orographic enhancement cube on the UK standard grid, padded with masked np.nans where outside the UKPP domain (m s-1) """ # create cube containing high resolution data in mm/h x_coord = self.topography.coord(axis='x') y_coord = self.topography.coord(axis='y') for coord in [x_coord, y_coord]: coord.points = coord.points.astype(np.float32) if coord.bounds is not None: coord.bounds = coord.bounds.astype(np.float32) aux_coords = [] for coord in ['time', 'forecast_reference_time', 'forecast_period']: aux_coords.append((reference_cube.coord(coord), None)) attributes = {} for attr in ['institution', 'source', 'mosg__model_configuration']: try: attributes[attr] = reference_cube.attributes[attr] except KeyError: continue orogenh = iris.cube.Cube(orogenh_data, long_name="orographic_enhancement", units="mm h-1", attributes=attributes, dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)], aux_coords_and_dims=aux_coords) orogenh.convert_units("m s-1") # regrid the orographic enhancement cube onto the standard grid and # mask extrapolated points orogenh_standard_grid = orogenh.regrid( reference_cube, iris.analysis.Linear(extrapolation_mode='mask')) for axis in ['x', 'y']: orogenh_standard_grid = sort_coord_in_cube( orogenh_standard_grid, orogenh_standard_grid.coord(axis=axis)) orogenh_standard_grid.coord( axis=axis).points = (orogenh_standard_grid.coord( axis=axis).points.astype(np.float32)) if orogenh_standard_grid.coord(axis=axis).bounds is not None: orogenh_standard_grid.coord( axis=axis).bounds = (orogenh_standard_grid.coord( axis=axis).bounds.astype(np.float32)) # add any relevant grid definition attributes for data_cube, grid_cube in zip([orogenh, orogenh_standard_grid], [self.topography, reference_cube]): for key, val in grid_cube.attributes.items(): if 'mosg__grid' in key: data_cube.attributes[key] = val return orogenh, orogenh_standard_grid
def _regrid_and_populate(self, temperature, humidity, pressure, uwind, vwind, topography): """ Regrids input variables onto the high resolution orography field, then populates the class instance with regridded variables before converting to SI units. Also calculates V.gradZ as a class member. Args: temperature (iris.cube.Cube): Temperature at top of boundary layer humidity (iris.cube.Cube): Relative humidity at top of boundary layer pressure (iris.cube.Cube): Pressure at top of boundary layer uwind (iris.cube.Cube): Positive eastward wind vector component at top of boundary layer vwind (iris.cube.Cube): Positive northward wind vector component at top of boundary layer topography (iris.cube.Cube): Height of topography above sea level on 1 km UKPP domain grid """ # convert topography grid, datatype and units for axis in ['x', 'y']: topography = sort_coord_in_cube(topography, topography.coord(axis=axis)) topography = enforce_coordinate_ordering(topography, [ topography.coord(axis='y').name(), topography.coord(axis='x').name() ]) self.topography = topography.copy( data=topography.data.astype(np.float32)) self.topography.convert_units('m') # rotate winds try: uwind, vwind = rotate_winds(uwind, vwind, topography.coord_system()) except ValueError as err: if 'Duplicate coordinates are not permitted' in str(err): # ignore error raised if uwind and vwind do not need rotating pass else: raise ValueError(str(err)) else: # remove auxiliary spatial coordinates from rotated winds for cube in [uwind, vwind]: for axis in ['x', 'y']: cube.remove_coord(cube.coord(axis=axis, dim_coords=False)) # regrid and convert input variables self.temperature = self._regrid_variable(temperature, 'kelvin') self.humidity = self._regrid_variable(humidity, '1') self.pressure = self._regrid_variable(pressure, 'Pa') self.uwind = self._regrid_variable(uwind, 'm s-1') self.vwind = self._regrid_variable(vwind, 'm s-1') # calculate orography gradients gradx, grady = self._orography_gradients() # calculate v.gradZ self.vgradz = (np.multiply(gradx.data, self.uwind.data) + np.multiply(grady.data, self.vwind.data))
def process(self, cube, weights=None): """Calculate weighted blend across the chosen coord, for either probabilistic or percentile data. If there is a percentile coordinate on the cube, it will blend using the PercentileBlendingAggregator but the percentile coordinate must have at least two points. Args: cube (iris.cube.Cube): Cube to blend across the coord. weights (iris.cube.Cube): Cube of blending weights. If None, the diagnostic cube is blended with equal weights across the blending dimension. Returns: result (iris.cube.Cube): containing the weighted blend across the chosen coord. Raises: TypeError : If the first argument not a cube. CoordinateNotFoundError : If coordinate to be collapsed not found in cube. CoordinateNotFoundError : If coordinate to be collapsed not found in provided weights cube. ValueError : If coordinate to be collapsed is not a dimension. """ if not isinstance(cube, iris.cube.Cube): msg = ('The first argument must be an instance of iris.cube.Cube ' 'but is {}.'.format(type(cube))) raise TypeError(msg) if not cube.coords(self.coord): msg = 'Coordinate to be collapsed not found in cube.' raise CoordinateNotFoundError(msg) coord_dim = cube.coord_dims(self.coord) if not coord_dim: raise ValueError('Blending coordinate {} has no associated ' 'dimension'.format(self.coord)) # Ensure input cube and weights cube are ordered equivalently along # blending coordinate. cube = sort_coord_in_cube(cube, self.coord, order="ascending") if weights is not None: if not weights.coords(self.coord): msg = 'Coordinate to be collapsed not found in weights cube.' raise CoordinateNotFoundError(msg) weights = sort_coord_in_cube(weights, self.coord, order="ascending") # Check that the time coordinate is single valued if required. self.check_compatible_time_points(cube) # Check to see if the data is percentile data perc_coord = self.check_percentile_coord(cube) # Percentile aggregator if perc_coord: cube_new = self.percentile_weighted_mean(cube, weights, perc_coord) # Weighted mean else: cube_new = self.weighted_mean(cube, weights) # Modify the cube metadata and add to the cubelist. result = conform_metadata(cube_new, cube, coord=self.coord, cycletime=self.cycletime) if isinstance(cube.data, np.ma.core.MaskedArray): result.data = np.ma.array(result.data) return result
def calculate_blending_weights(cube, blend_coord, method, wts_dict=None, weighting_coord=None, coord_unit=None, y0val=None, ynval=None, cval=None, dict_coord=None): """ Wrapper for plugins to calculate blending weights using the command line options specified. Args: cube (iris.cube.Cube): Cube of input data to be blended blend_coord (str): Coordinate over which blending will be performed (eg "model" for grid blending) method (str): Weights calculation method ("linear", "nonlinear", "dict" or "mask") Kwargs: wts_dict (str): File path to json file with parameters for linear weights calculation weighting_coord (str): Coordinate over which linear weights should be calculated from dict coord_unit (str or cf_units.Unit): Unit of blending coordinate (for default weights plugins) y0val (float): Intercept parameter for default linear weights plugin ynval (float): Gradient parameter for default linear weights plugin cval (float): Parameter for default non-linear weights plugin dict_coord (str): The coordinate that will be used when accessing the weights from the weights dictionary. Returns: weights (np.ndarray): 1D array of weights corresponding to slices in ascending order of blending coordinate. (Note: ChooseLinearWeights has the option to create a 3D array of spatially-varying weights with the "mask" option, however this is not currently supported by the blending plugin.) """ # sort input cube by blending coordinate cube = sort_coord_in_cube(cube, blend_coord, order="ascending") # calculate blending weights if method == "dict": # calculate linear weights from a dictionary with open(wts_dict, 'r') as wts: weights_dict = json.load(wts) weights_cube = ChooseWeightsLinear( weighting_coord, weights_dict, config_coord_name=dict_coord).process(cube) # sort weights cube by blending coordinate weights = sort_coord_in_cube(weights_cube, blend_coord, order="ascending") elif method == "linear": weights = ChooseDefaultWeightsLinear(y0val=y0val, ynval=ynval).process( cube, blend_coord, coord_unit=coord_unit) elif method == "nonlinear": # this is set here rather than in the CLI arguments in order to check # for invalid argument combinations cvalue = cval if cval else 0.85 weights = ChooseDefaultWeightsNonLinear(cvalue).process( cube, blend_coord, coord_unit=coord_unit) return weights