def test_basic(self): """Test that the utility returns a list.""" cube1 = self.cube.copy() cube2 = self.cube.copy() cubelist = iris.cube.CubeList([cube1, cube2]) result = compare_coords(cubelist) self.assertIsInstance(result, list)
def resolve_metadata_diff(cube1, cube2, warnings_on=False): """Resolve any differences in metadata between cubes. Args: cube1 (iris.cube.Cube): Cube containing data to be combined. cube2 (iris.cube.Cube): Cube containing data to be combined. Keyword Args: warnings_on (bool): If True output warnings for mismatching metadata. Returns: (tuple): tuple containing **result1** (iris.cube.Cube): Cube with corrected Metadata. **result2** (iris.cube.Cube): Cube with corrected Metadata. """ result1 = cube1 result2 = cube2 cubes = iris.cube.CubeList([result1, result2]) # Processing will be based on cube1 so any unmatching # attributes will be ignored # Find mismatching coords unmatching_coords = compare_coords(cubes) # If extra dim coord length 1 on cube1 then add to cube2 for coord in unmatching_coords[0]: if coord not in unmatching_coords[1]: if len(result1.coord(coord).points) == 1: if result1.coord_dims(coord) is not None: coord_dict = dict() coord_dict['points'] = result1.coord(coord).points coord_dict['bounds'] = result1.coord(coord).bounds coord_dict['units'] = result1.coord(coord).units coord_dict['metatype'] = 'DimCoord' result2 = add_coord(result2, coord, coord_dict, warnings_on=warnings_on) result2 = iris.util.as_compatible_shape(result2, result1) # If extra dim coord length 1 on cube2 then delete from cube2 for coord in unmatching_coords[1]: if coord not in unmatching_coords[0]: if len(result2.coord(coord).points) == 1: result2 = update_coord(result2, coord, 'delete', warnings_on=warnings_on) # If shapes still do not match Raise an error if result1.data.shape != result2.data.shape: msg = "Can not combine cubes, mismatching shapes" raise ValueError(msg) return result1, result2
def test_catch_warning(self, warning_list=None): """Test warning is raised if the input is cubelist of length 1.""" cube = self.cube.copy() result = compare_coords(iris.cube.CubeList([cube])) self.assertTrue(any(item.category == UserWarning for item in warning_list)) warning_msg = "Only a single cube so no differences will be found " self.assertTrue(any(warning_msg in str(item) for item in warning_list)) self.assertEqual(result, [])
def process(self, wind_speed, wind_dir): """ Convert wind speed and direction into u,v components along input cube projection axes. Args: wind_speed (iris.cube.Cube): Cube containing wind speed values wind_dir (iris.cube.Cube): Cube containing wind direction values relative to true North Returns: (tuple): tuple containing **ucube** (iris.cube.Cube): Cube containing wind speeds in the positive projection x-axis direction, with units and projection matching wind_speed cube. **vcube** (iris.cube.Cube): Cube containing wind speeds in the positive projection y-axis direction, with units and projection matching wind_speed cube. """ # check cubes contain the correct data (assuming CF standard names) if "wind_speed" not in wind_speed.name(): msg = '{} cube does not contain wind speeds' raise ValueError('{} {}'.format(wind_speed.name(), msg)) if "wind" not in wind_dir.name() or "direction" not in wind_dir.name(): msg = '{} cube does not contain wind directions' raise ValueError('{} {}'.format(wind_dir.name(), msg)) # check input cube coordinates match unmatched_coords = compare_coords([wind_speed, wind_dir]) if unmatched_coords != [{}, {}]: msg = 'Wind speed and direction cubes have unmatched coordinates' raise ValueError('{} {}'.format(msg, unmatched_coords)) # calculate angle adjustments for wind direction wind_dir_slice = next( wind_dir.slices([ wind_dir.coord(axis='y').name(), wind_dir.coord(axis='x').name() ])) adj = self.calc_true_north_offset(wind_dir_slice) # calculate grid eastward and northward speeds ucube, vcube = self.resolve_wind_components(wind_speed, wind_dir, adj) # relabel final cubes with CF compliant data names corresponding to # positive wind speeds along the x and y axes ucube.rename("grid_eastward_wind") vcube.rename("grid_northward_wind") return ucube, vcube
def process(self, wind_speed: Cube, wind_dir: Cube) -> Tuple[Cube, Cube]: """ Convert wind speed and direction into u,v components along input cube projection axes. Args: wind_speed: Cube containing wind speed values wind_dir: Cube containing wind direction values relative to true North Returns: - Cube containing wind speeds in the positive projection x-axis direction, with units and projection matching wind_speed cube. - Cube containing wind speeds in the positive projection y-axis direction, with units and projection matching wind_speed cube. """ # check cubes contain the correct data (assuming CF standard names) if "wind_speed" not in wind_speed.name(): msg = "{} cube does not contain wind speeds" raise ValueError("{} {}".format(wind_speed.name(), msg)) if "wind" not in wind_dir.name() or "direction" not in wind_dir.name(): msg = "{} cube does not contain wind directions" raise ValueError("{} {}".format(wind_dir.name(), msg)) # check input cube coordinates match ignored_coords = ["wind_from_direction status_flag", "wind_speed status_flag"] unmatched_coords = compare_coords( [wind_speed, wind_dir], ignored_coords=ignored_coords ) if unmatched_coords != [{}, {}]: msg = "Wind speed and direction cubes have unmatched coordinates" raise ValueError("{} {}".format(msg, unmatched_coords)) # calculate angle adjustments for wind direction wind_dir_slice = next( wind_dir.slices( [wind_dir.coord(axis="y").name(), wind_dir.coord(axis="x").name()] ) ) adj = self.calc_true_north_offset(wind_dir_slice) # calculate grid eastward and northward speeds ucube, vcube = self.resolve_wind_components(wind_speed, wind_dir, adj) # relabel final cubes with CF compliant data names corresponding to # positive wind speeds along the x and y axes ucube.rename("grid_eastward_wind") vcube.rename("grid_northward_wind") return ucube, vcube
def test_second_cube_has_extra_ignored_coordinate(self): """Test for comparing coordinate between cubes, where the second cube in the list has an extra dimension coordinate which is explicitly ignored in the comparison.""" cube1 = self.cube.copy() cube2 = self.cube.copy() cube2.add_aux_coord(self.extra_dim_coord) cube2 = iris.util.new_axis(cube2, "height") cubelist = iris.cube.CubeList([cube1, cube2]) result = compare_coords(cubelist, ignored_coords=["height"]) self.assertIsInstance(result, list) self.assertEqual(result, [{}, {}])
def test_second_cube_has_extra_auxiliary_coordinates(self): """Test for comparing coordinate between cubes, where the second cube in the list has extra auxiliary coordinates.""" cube1 = self.cube.copy() cube2 = self.cube.copy() cube2.add_aux_coord(self.extra_aux_coord, data_dims=0) cubelist = iris.cube.CubeList([cube1, cube2]) result = compare_coords(cubelist) self.assertIsInstance(result, list) self.assertEqual(len(result[0]), 0) self.assertEqual(len(result[1]), 1) self.assertEqual(result[1]["model"]["coord"], self.extra_aux_coord) self.assertEqual(result[1]["model"]["data_dims"], None) self.assertEqual(result[1]["model"]["aux_dims"], 0)
def test_second_cube_has_extra_dimension_coordinates(self): """Test for comparing coordinate between cubes, where the second cube in the list has extra dimension coordinates.""" cube1 = self.cube.copy() cube2 = self.cube.copy() cube2.add_aux_coord(self.extra_dim_coord) cube2 = iris.util.new_axis(cube2, "height") cubelist = iris.cube.CubeList([cube1, cube2]) result = compare_coords(cubelist) self.assertIsInstance(result, list) self.assertEqual(len(result[0]), 0) self.assertEqual(len(result[1]), 1) self.assertEqual(result[1]["height"]["coord"], self.extra_dim_coord) self.assertEqual(result[1]["height"]["data_dims"], 0) self.assertEqual(result[1]["height"]["aux_dims"], None)
def test_second_cube_has_extra_dimension_coordinates(self): """Test for comparing coordinate between cubes, where the second cube in the list has extra dimension coordinates.""" cube1 = self.cube.copy() cube2 = self.cube.copy() height_coord = DimCoord([5.0], standard_name="height", units="m") cube2.add_aux_coord(height_coord) cube2 = iris.util.new_axis(cube2, "height") cubelist = iris.cube.CubeList([cube1, cube2]) result = compare_coords(cubelist) self.assertIsInstance(result, list) self.assertEqual(len(result[0]), 0) self.assertEqual(len(result[1]), 1) self.assertEqual(result[1]["height"]["coord"].points, np.array([5.])) self.assertEqual(result[1]["height"]["coord"].standard_name, "height") self.assertEqual(result[1]["height"]["coord"].units, Unit("m")) self.assertEqual(result[1]["height"]["data_dims"], 0) self.assertEqual(result[1]["height"]["aux_dims"], None)
def test_second_cube_has_extra_auxiliary_coordinates(self): """Test for comparing coordinate between cubes, where the second cube in the list has extra auxiliary coordinates.""" cube1 = self.cube.copy() cube2 = self.cube.copy() fp_coord = AuxCoord( [3.0], standard_name="forecast_period", units="hours") cube2.add_aux_coord(fp_coord, data_dims=1) cubelist = iris.cube.CubeList([cube1, cube2]) result = compare_coords(cubelist) self.assertIsInstance(result, list) self.assertEqual(len(result[0]), 0) self.assertEqual(len(result[1]), 1) self.assertEqual(result[1]["forecast_period"]["coord"].points, np.array([3.0])) self.assertEqual(result[1]["forecast_period"]["coord"].standard_name, "forecast_period") self.assertEqual(result[1]["forecast_period"]["coord"].units, Unit("hours")) self.assertEqual(result[1]["forecast_period"]["data_dims"], None) self.assertEqual(result[1]["forecast_period"]["aux_dims"], 1)
def process(self, temperature, humidity, pressure, uwind, vwind, topography): """ Calculate precipitation enhancement over orography on standard and high resolution grids. Input variables are expected to be on the same grid (either standard or high resolution). 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 high resolution (1 km) UKPP domain grid Returns: (tuple): tuple containing: **orogenh** (iris.cube.Cube): Precipitation enhancement due to orography in mm/h on the 1 km Transverse Mercator UKPP grid domain **orogenh_standard_grid** (iris.cube.Cube): Precipitation enhancement due to orography in mm/h on the UK standard grid, padded with masked np.nans where outside the UKPP domain """ # check input variable cube coordinates match unmatched_coords = compare_coords( [temperature, pressure, humidity, uwind, vwind]) if any(item.keys() for item in unmatched_coords): msg = 'Input cube coordinates {} are unmatched' raise ValueError(msg.format(unmatched_coords)) # check one of the input variable cubes is a 2D spatial field (this is # equivalent to checking all cubes whose coords are matched above) msg = 'Require 2D fields as input; found {} dimensions' if temperature.ndim > 2: raise ValueError(msg.format(temperature.ndim)) check_for_x_and_y_axes(temperature) # check the topography cube is a 2D spatial field if topography.ndim > 2: raise ValueError(msg.format(topography.ndim)) check_for_x_and_y_axes(topography) # regrid variables to match topography and populate class instance self._regrid_and_populate(temperature, humidity, pressure, uwind, vwind, topography) # calculate saturation vapour pressure wbt = WetBulbTemperature() self.svp = wbt.pressure_correct_svp(wbt.lookup_svp(self.temperature), self.temperature, self.pressure) # calculate site-specific orographic enhancement point_orogenh_data = self._point_orogenh() # integrate upstream component grid_coord_km = self.topography.coord(axis='x').copy() grid_coord_km.convert_units('km') self.grid_spacing_km = (grid_coord_km.points[1] - grid_coord_km.points[0]) orogenh_data = self._add_upstream_component(point_orogenh_data) # create data cubes on the two required output grids orogenh, orogenh_standard_grid = self._create_output_cubes( orogenh_data, temperature) return orogenh, orogenh_standard_grid
def process(self, temperature, humidity, pressure, uwind, vwind, topography): """ Calculate precipitation enhancement over orography on high resolution grid. Input diagnostics are all expected to be on the same grid, and are regridded to match the orography. 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 high resolution (1 km) UKPP domain grid Returns: iris.cube.Cube: Precipitation enhancement due to orography in m/s. """ # check input variable cube coordinates match unmatched_coords = compare_coords( [temperature, pressure, humidity, uwind, vwind]) if any(item.keys() for item in unmatched_coords): msg = 'Input cube coordinates {} are unmatched' raise ValueError(msg.format(unmatched_coords)) # check one of the input variable cubes is a 2D spatial field (this is # equivalent to checking all cubes whose coords are matched above) msg = 'Require 2D fields as input; found {} dimensions' if temperature.ndim > 2: raise ValueError(msg.format(temperature.ndim)) check_for_x_and_y_axes(temperature) # check the topography cube is a 2D spatial field if topography.ndim > 2: raise ValueError(msg.format(topography.ndim)) check_for_x_and_y_axes(topography) # regrid variables to match topography and populate class instance self._regrid_and_populate(temperature, humidity, pressure, uwind, vwind, topography) # calculate saturation vapour pressure self.svp = calculate_svp_in_air( self.temperature.data, self.pressure.data) # calculate site-specific orographic enhancement point_orogenh_data = self._point_orogenh() # integrate upstream component grid_coord_km = self.topography.coord(axis='x').copy() grid_coord_km.convert_units('km') self.grid_spacing_km = ( grid_coord_km.points[1] - grid_coord_km.points[0]) orogenh_data = self._add_upstream_component(point_orogenh_data) # create data cubes on the two required output grids orogenh = self._create_output_cube(orogenh_data, temperature) return orogenh
def resolve_metadata_diff(cube1, cube2, warnings_on=False): """Resolve any differences in metadata between cubes. This involves identifying coordinates that are mismatching between the cubes and attempting to add this coordinate where it is missing. This makes use of the points, bounds, units and attributes, as well as the coordinate type i.e. DimCoord or AuxCoord. Args: cube1 (iris.cube.Cube): Cube containing data to be combined. cube2 (iris.cube.Cube): Cube containing data to be combined. warnings_on (bool): If True output warnings for mismatching metadata. Returns: (tuple): tuple containing: **result1** (iris.cube.Cube): Cube with corrected Metadata. **result2** (iris.cube.Cube): Cube with corrected Metadata. """ result1 = cube1 result2 = cube2 cubes = iris.cube.CubeList([result1, result2]) # Processing will be based on cube1 so any unmatching # attributes will be ignored # Find mismatching coords unmatching_coords = compare_coords(cubes) # If extra dim coord length 1 on cube1 then add to cube2 for coord in unmatching_coords[0]: if coord not in unmatching_coords[1]: if len(result1.coord(coord).points) == 1: if len(result1.coord_dims(coord)) > 0: coord_dict = dict() coord_dict['points'] = result1.coord(coord).points coord_dict['bounds'] = result1.coord(coord).bounds coord_dict['units'] = result1.coord(coord).units coord_dict['attributes'] = result1.coord(coord).attributes coord_dict['metatype'] = 'DimCoord' if result1.coord(coord).var_name is not None: coord_dict['var_name'] = result1.coord(coord).var_name result2 = add_coord(result2, coord, coord_dict, warnings_on=warnings_on) result2 = iris.util.as_compatible_shape(result2, result1) # If extra dim coord length 1 on cube2 then delete from cube2 for coord in unmatching_coords[1]: if coord not in unmatching_coords[0]: if len(result2.coord(coord).points) == 1: result2 = _update_coord(result2, coord, 'delete', warnings_on=warnings_on) # If shapes still do not match Raise an error if result1.data.shape != result2.data.shape: msg = "Can not combine cubes, mismatching shapes" raise ValueError(msg) return result1, result2
def _align_feature_variables(self, feature_cubes: CubeList, forecast_cube: Cube) -> Tuple[CubeList, Cube]: """Ensure that feature cubes have consistent dimension coordinates. If realization dimension present in any cube, all cubes lacking this dimension will have realization dimension added and broadcast along this new dimension. This situation occurs when derived fields (such as accumulated solar radiation) are used as predictors. As these fields do not contain a realization dimension, they must be broadcast to match the NWP fields that do contain realization, so that all features have consistent shape. In the case of deterministic models (those without a realization dimension), a realization dimension is added to allow consistent behaviour between ensemble and deterministic models. Args: feature_cubes: Cubelist containing feature variables to align. forecast_cube: Cube containing the forecast variable to align. Returns: - feature_cubes with realization coordinate added to each cube if absent - forecast_cube with realization coordinate added if absent Raises: ValueError: if feature/forecast variables have inconsistent dimension coordinates (excluding realization dimension), or if feature/forecast variables have different length realization coordinate over cubes containing a realization coordinate. """ combined_cubes = CubeList(list([*feature_cubes, forecast_cube])) # Compare feature cube coordinates, raise error if dim-coords don't match compare_feature_coords = compare_coords(combined_cubes, ignored_coords=["realization"]) for misaligned_coords in compare_feature_coords: for coord_info in misaligned_coords.values(): if coord_info["data_dims"] is not None: raise ValueError( "Mismatch between non-realization dimension coords.") # Compare realization coordinates across cubes where present; # raise error if realization coordinates don't match, otherwise set # common_realization_coord to broadcast over. realization_coords = { variable.name(): variable.coord("realization") for variable in combined_cubes if variable.coords("realization") } if not realization_coords: # Case I: realization_coords is empty. Add single realization dim to all cubes. common_realization_coord = DimCoord([0], standard_name="realization", units=1) else: # Case II: realization_coords is not empty. # Note: In future, another option here could be to filter to common realization # values using filter_realizations() in utilities.cube_manipulation. variables_with_realization = list(realization_coords.keys()) sample_realization = realization_coords[ variables_with_realization[0]] for feature in variables_with_realization[1:]: if realization_coords[feature] != sample_realization: raise ValueError( "Mismatch between realization dimension coords.") common_realization_coord = sample_realization # Add realization coord to cubes where absent by broadcasting along this dimension aligned_cubes = CubeList() for cube in combined_cubes: if not cube.coords("realization"): expanded_cube = add_coordinate_to_cube( cube, new_coord=common_realization_coord) aligned_cubes.append(expanded_cube) else: aligned_cubes.append(cube) return aligned_cubes[:-1], aligned_cubes[-1]