def test_non_default(self): """Test that the __repr__ returns the expected string with non-default options.""" plugin = SpotExtraction(neighbour_selection_method="nearest_land") result = str(plugin) msg = "<SpotExtraction: neighbour_selection_method: nearest_land>" self.assertEqual(result, msg)
def test_basic(self): """Test that the __repr__ returns the expected string with defaults.""" plugin = SpotExtraction() result = str(plugin) msg = ('<SpotExtraction: neighbour_selection_method: nearest, ' 'grid_metadata_identifier: mosg>') self.assertEqual(result, msg)
def test_coordinate_without_bounds_extraction(self): """Test extraction of coordinate data for a 2-dimensional auxiliary coordinate. In this case the coordinate has no bounds.""" plugin = SpotExtraction() expected_points = self.expected_spot_time_coord.points expected_bounds = None x_indices, y_indices = self.coordinate_cube.data self.diagnostic_cube_2d_time.coord("time").bounds = None points, bounds = plugin.get_coordinate_data( self.diagnostic_cube_2d_time, x_indices, y_indices, coordinate="time") self.assertArrayEqual(points, expected_points) self.assertArrayEqual(bounds, expected_bounds)
def process(self, spot_data_cube: Cube, neighbour_cube: Cube, gridded_lapse_rate_cube: Cube) -> Cube: """ Extract lapse rates from the appropriate grid points and apply them to the spot extracted temperatures. The calculation is:: lapse_rate_adjusted_temperatures = temperatures + lapse_rate * vertical_displacement Args: spot_data_cube: A spot data cube of temperatures for the spot data sites, extracted from the gridded temperature field. These temperatures will have been extracted using the same neighbour_cube and neighbour_selection_method that are being used here. neighbour_cube: The neighbour_cube that contains the grid coordinates at which lapse rates should be extracted and the vertical displacement between those grid points on the model orography and the spot data sites actual altitudes. This cube is only updated when a new site is added. gridded_lapse_rate_cube: A cube of temperature lapse rates on the same grid as that from which the spot data temperatures were extracted. Returns: A copy of the input spot_data_cube with the data modified by the lapse rates to give a better representation of the site's temperatures. """ # Check the cubes are compatible. check_grid_match( [neighbour_cube, spot_data_cube, gridded_lapse_rate_cube]) # Extract the lapse rates that correspond to the spot sites. spot_lapse_rate = SpotExtraction( neighbour_selection_method=self.neighbour_selection_method)( neighbour_cube, gridded_lapse_rate_cube) # Extract vertical displacements between the model orography and sites. method_constraint = iris.Constraint( neighbour_selection_method_name=self.neighbour_selection_method) data_constraint = iris.Constraint( grid_attributes_key="vertical_displacement") vertical_displacement = neighbour_cube.extract(method_constraint & data_constraint) # Apply lapse rate adjustment to the temperature at each site. new_temperatures = ( spot_data_cube.data + (spot_lapse_rate.data * vertical_displacement.data)).astype( np.float32) new_spot_cube = spot_data_cube.copy(data=new_temperatures) return new_spot_cube
def test_non_default(self): """Test that the __repr__ returns the expected string with non-default options.""" plugin = SpotExtraction(neighbour_selection_method='nearest_land', grid_metadata_identifier='bom') result = str(plugin) msg = ('<SpotExtraction: neighbour_selection_method: nearest_land, ' 'grid_metadata_identifier: bom>') self.assertEqual(result, msg)
def test_building_cube(self): """Test that a cube is built as expected.""" plugin = SpotExtraction() spot_values = np.array([0, 0, 12, 12]) result = plugin.build_diagnostic_cube( self.neighbour_cube, self.diagnostic_cube_xy, spot_values, unique_site_id=self.unique_site_id, unique_site_id_key=self.unique_site_id_key, ) self.assertArrayEqual(result.coord("latitude").points, self.latitudes) self.assertArrayEqual( result.coord("longitude").points, self.longitudes) self.assertArrayEqual(result.coord("altitude").points, self.altitudes) self.assertArrayEqual(result.coord("wmo_id").points, self.wmo_ids) self.assertArrayEqual( result.coord(self.unique_site_id_key).points, self.unique_site_id) self.assertArrayEqual(result.data, spot_values)
def test_multiple_nonscalar_coords(self): """Test with an input cube containing multiple nonscalar auxiliary coordinates. The returned non-scalar coordinates are 1D representations of the 2D non-scalar input coordinates at spot sites.""" plugin = SpotExtraction() additional_2d_crd = self.time_aux_coord.copy() additional_2d_crd.rename("kittens") self.diagnostic_cube_2d_time.add_aux_coord(additional_2d_crd, data_dims=(0, 1)) additional_expected = self.expected_spot_time_coord.copy() additional_expected.rename("kittens") expected_nonscalar = [ additional_expected, self.expected_spot_time_coord ] x_indices, y_indices = self.coordinate_cube.data _, nonscalar = plugin.get_aux_coords(self.diagnostic_cube_2d_time, x_indices, y_indices) self.assertArrayEqual(nonscalar, expected_nonscalar)
def test_scalar_and_nonscalar_coords(self): """Test with an input cube containing scalar and nonscalar auxiliary coordinates. The returned non-scalar coordinate is a 1D representation of the 2D non-scalar input coordinate at spot sites.""" plugin = SpotExtraction() expected_scalar = [ coord for coord in self.diagnostic_cube_2d_time.aux_coords if coord.name() in [ "time_in_local_timezone", "forecast_reference_time", "forecast_period" ] ] expected_nonscalar = [self.expected_spot_time_coord] x_indices, y_indices = self.coordinate_cube.data scalar, nonscalar = plugin.get_aux_coords(self.diagnostic_cube_2d_time, x_indices, y_indices) self.assertArrayEqual(scalar, expected_scalar) self.assertArrayEqual(nonscalar, expected_nonscalar)
def process(self, spot_data_cube: Cube, neighbour_cube: Cube, gridded_lapse_rate_cube: Cube) -> Cube: """ Extract lapse rates from the appropriate grid points and apply them to the spot extracted temperatures. The calculation is:: lapse_rate_adjusted_temperatures = temperatures + lapse_rate * vertical_displacement Args: spot_data_cube: A spot data cube of temperatures for the spot data sites, extracted from the gridded temperature field. These temperatures will have been extracted using the same neighbour_cube and neighbour_selection_method that are being used here. neighbour_cube: The neighbour_cube that contains the grid coordinates at which lapse rates should be extracted and the vertical displacement between those grid points on the model orography and the spot data sites actual altitudes. This cube is only updated when a new site is added. gridded_lapse_rate_cube: A cube of temperature lapse rates on the same grid as that from which the spot data temperatures were extracted. Returns: A copy of the input spot_data_cube with the data modified by the lapse rates to give a better representation of the site's temperatures. Raises: ValueError: If the lapse rate cube was provided but the diagnostic being processed is not air temperature. ValueError: If the lapse rate cube provided does not have the name "air_temperature_lapse_rate" ValueError: If the lapse rate cube does not contain a single valued height coordinate. Warns: warning: If a lapse rate cube was provided, but the height of the temperature does not match that of the data used. """ if is_probability(spot_data_cube): msg = ( "Input cube has a probability coordinate which cannot be lapse " "rate adjusted. Input data should be in percentile or " "deterministic space only.") raise ValueError(msg) # Check that we are dealing with temperature data. if spot_data_cube.name() not in [ "air_temperature", "feels_like_temperature" ]: msg = ( "The diagnostic being processed is not air temperature " "or feels like temperature and therefore cannot be adjusted.") raise ValueError(msg) if not gridded_lapse_rate_cube.name() == "air_temperature_lapse_rate": msg = ("A cube has been provided as a lapse rate cube but does " "not have the expected name air_temperature_lapse_rate: " "{}".format(gridded_lapse_rate_cube.name())) raise ValueError(msg) try: lapse_rate_height_coord = gridded_lapse_rate_cube.coord("height") except (CoordinateNotFoundError): msg = ("Lapse rate cube does not contain a single valued height " "coordinate. This is required to ensure it is applied to " "equivalent temperature data.") raise CoordinateNotFoundError(msg) # Check the height of the temperature data matches that used to # calculate the lapse rates. If so, adjust temperatures using the lapse # rate values. if not spot_data_cube.coord("height") == lapse_rate_height_coord: raise ValueError( "A lapse rate cube was provided, but the height of the " "temperature data does not match that of the data used " "to calculate the lapse rates. As such the temperatures " "were not adjusted with the lapse rates.") # Check the cubes are compatible. check_grid_match( [neighbour_cube, spot_data_cube, gridded_lapse_rate_cube]) # Extract the lapse rates that correspond to the spot sites. spot_lapse_rate = SpotExtraction( neighbour_selection_method=self.neighbour_selection_method)( neighbour_cube, gridded_lapse_rate_cube) # Extract vertical displacements between the model orography and sites. method_constraint = iris.Constraint( neighbour_selection_method_name=self.neighbour_selection_method) data_constraint = iris.Constraint( grid_attributes_key="vertical_displacement") vertical_displacement = neighbour_cube.extract(method_constraint & data_constraint) # Apply lapse rate adjustment to the temperature at each site. new_spot_lapse_rate = iris.util.broadcast_to_shape( spot_lapse_rate.data, spot_data_cube.shape, [-1]) new_temperatures = ( spot_data_cube.data + (new_spot_lapse_rate * vertical_displacement.data)).astype( np.float32) new_spot_cube = spot_data_cube.copy(data=new_temperatures) return new_spot_cube
def process( neighbour_cube: cli.inputcube, cube: cli.inputcube, lapse_rate: cli.inputcube = None, *, apply_lapse_rate_correction=False, land_constraint=False, similar_altitude=False, extract_percentiles: cli.comma_separated_list = None, ignore_ecc_bounds=False, new_title: str = None, suppress_warnings=False, ): """Module to run spot data extraction. Extract diagnostic data from gridded fields for spot data sites. It is possible to apply a temperature lapse rate adjustment to temperature data that helps to account for differences between the spot site's real altitude and that of the grid point from which the temperature data is extracted. Args: neighbour_cube (iris.cube.Cube): Cube of spot-data neighbours and the spot site information. cube (iris.cube.Cube): Cube containing the diagnostic data to be extracted. lapse_rate (iris.cube.Cube): Optional cube containing temperature lapse rates. If this cube is provided and a screen temperature cube is being processed, the lapse rates will be used to adjust the temperature to better represent each spot's site-altitude. apply_lapse_rate_correction (bool): Use to apply a lapse-rate correction to screen temperature data so that the data are a better match the altitude of the spot site for which they have been extracted. land_constraint (bool): Use to select the nearest-with-land-constraint neighbour-selection method from the neighbour_cube. This means that the grid points should be land points except for sites where none were found within the search radius when the neighbour cube was created. May be used with similar_altitude. similar_altitude (bool): Use to select the nearest-with-height-constraint neighbour-selection method from the neighbour_cube. These are grid points that were found to be the closest in altitude to the spot site within the search radius defined when the neighbour cube was created. May be used with land_constraint. extract_percentiles (list or int): If set to a percentile value or a list of percentile values, data corresponding to those percentiles will be returned. For example "25, 50, 75" will result in the 25th, 50th and 75th percentiles being returned from a cube of probabilities, percentiles or realizations. Deterministic input data will raise a warning message. Note that for percentiles inputs, the desired percentile(s) must exist in the input cube. ignore_ecc_bounds (bool): Demotes exceptions where calculated percentiles are outside the ECC bounds range to warnings. new_title (str): New title for the spot-extracted data. If None, this attribute is removed from the output cube since it has no prescribed standard and may therefore contain grid information that is no longer correct after spot-extraction. suppress_warnings (bool): Suppress warning output. This option should only be used if it is known that warnings will be generated but they are not required. Returns: iris.cube.Cube: Cube of spot data. Raises: ValueError: If the percentile diagnostic cube does not contain the requested percentile value. ValueError: If the lapse rate cube was provided but the diagnostic being processed is not air temperature. ValueError: If the lapse rate cube provided does not have the name "air_temperature_lapse_rate" ValueError: If the lapse rate cube does not contain a single valued height coordinate. Warns: warning: If diagnostic cube is not a known probabilistic type. warning: If a lapse rate cube was provided, but the height of the temperature does not match that of the data used. warning: If a lapse rate cube was not provided, but the option to apply the lapse rate correction was enabled. """ import warnings import iris import numpy as np from iris.exceptions import CoordinateNotFoundError from improver.ensemble_copula_coupling.ensemble_copula_coupling import ( ConvertProbabilitiesToPercentiles, ) from improver.metadata.probabilistic import find_percentile_coordinate from improver.percentile import PercentileConverter from improver.spotdata.apply_lapse_rate import SpotLapseRateAdjust from improver.spotdata.neighbour_finding import NeighbourSelection from improver.spotdata.spot_extraction import SpotExtraction from improver.utilities.cube_extraction import extract_subcube neighbour_selection_method = NeighbourSelection( land_constraint=land_constraint, minimum_dz=similar_altitude).neighbour_finding_method_name() result = SpotExtraction( neighbour_selection_method=neighbour_selection_method)( neighbour_cube, cube, new_title=new_title) # If a probability or percentile diagnostic cube is provided, extract # the given percentile if available. This is done after the spot-extraction # to minimise processing time; usually there are far fewer spot sites than # grid points. if extract_percentiles: extract_percentiles = [np.float32(x) for x in extract_percentiles] try: perc_coordinate = find_percentile_coordinate(result) except CoordinateNotFoundError: if "probability_of_" in result.name(): result = ConvertProbabilitiesToPercentiles( ecc_bounds_warning=ignore_ecc_bounds)( result, percentiles=extract_percentiles) result = iris.util.squeeze(result) elif result.coords("realization", dim_coords=True): fast_percentile_method = not np.ma.isMaskedArray(result.data) result = PercentileConverter( "realization", percentiles=extract_percentiles, fast_percentile_method=fast_percentile_method, )(result) else: msg = ("Diagnostic cube is not a known probabilistic type. " "The {} percentile could not be extracted. Extracting " "data from the cube including any leading " "dimensions.".format(extract_percentiles)) if not suppress_warnings: warnings.warn(msg) else: constraint = [ "{}={}".format(perc_coordinate.name(), extract_percentiles) ] perc_result = extract_subcube(result, constraint) if perc_result is not None: result = perc_result else: msg = ("The percentile diagnostic cube does not contain the " "requested percentile value. Requested {}, available " "{}".format(extract_percentiles, perc_coordinate.points)) raise ValueError(msg) # Check whether a lapse rate cube has been provided and we are dealing with # temperature data and the lapse-rate option is enabled. if apply_lapse_rate_correction and lapse_rate: if not result.name() == "air_temperature": msg = ("A lapse rate cube was provided, but the diagnostic being " "processed is not air temperature and cannot be adjusted.") raise ValueError(msg) if not lapse_rate.name() == "air_temperature_lapse_rate": msg = ("A cube has been provided as a lapse rate cube but does " "not have the expected name air_temperature_lapse_rate: " "{}".format(lapse_rate.name())) raise ValueError(msg) try: lapse_rate_height_coord = lapse_rate.coord("height") except (ValueError, CoordinateNotFoundError): msg = ("Lapse rate cube does not contain a single valued height " "coordinate. This is required to ensure it is applied to " "equivalent temperature data.") raise ValueError(msg) # Check the height of the temperature data matches that used to # calculate the lapse rates. If so, adjust temperatures using the lapse # rate values. if cube.coord("height") == lapse_rate_height_coord: plugin = SpotLapseRateAdjust( neighbour_selection_method=neighbour_selection_method) result = plugin(result, neighbour_cube, lapse_rate) elif not suppress_warnings: warnings.warn( "A lapse rate cube was provided, but the height of the " "temperature data does not match that of the data used " "to calculate the lapse rates. As such the temperatures " "were not adjusted with the lapse rates.") elif apply_lapse_rate_correction and not lapse_rate: if not suppress_warnings: warnings.warn( "A lapse rate cube was not provided, but the option to " "apply the lapse rate correction was enabled. No lapse rate " "correction could be applied.") # Remove the internal model_grid_hash attribute if present. result.attributes.pop("model_grid_hash", None) return result
def test_yx_ordered_cube(self): """Test extraction of diagnostic data that is natively ordered yx.""" plugin = SpotExtraction() expected = [0, 0, 12, 12] result = plugin.process(self.coordinate_cube, self.diagnostic_cube_yx) self.assertArrayEqual(result.data, expected)
def test_nearest_land(self): """Test extraction of nearest land neighbour x and y indices.""" plugin = SpotExtraction(neighbour_selection_method="nearest_land") expected = self.neighbours[1, 0:2, :].astype(int) result = plugin.extract_coordinates(self.neighbour_cube) self.assertArrayEqual(result.data, expected)
def test_basic(self): """Test that the __repr__ returns the expected string with defaults.""" plugin = SpotExtraction() result = str(plugin) msg = "<SpotExtraction: neighbour_selection_method: nearest>" self.assertEqual(result, msg)
def main(argv=None): """Load in arguments and start spotdata extraction process.""" parser = ArgParser( description="Extract diagnostic data from gridded fields for spot data" " sites. It is possible to apply a temperature lapse rate adjustment" " to temperature data that helps to account for differences between" " the spot sites real altitude and that of the grid point from which" " the temperature data is extracted.") # Input and output files required. parser.add_argument("neighbour_filepath", metavar="NEIGHBOUR_FILEPATH", help="Path to a NetCDF file of spot-data neighbours. " "This file also contains the spot site information.") parser.add_argument("diagnostic_filepath", metavar="DIAGNOSTIC_FILEPATH", help="Path to a NetCDF file containing the diagnostic " "data to be extracted.") parser.add_argument("temperature_lapse_rate_filepath", metavar="LAPSE_RATE_FILEPATH", nargs='?', help="(Optional) Filepath to a NetCDF file containing" " temperature lapse rates. If this cube is provided," " and a screen temperature cube is being processed," " the lapse rates will be used to adjust the" " temperatures to better represent each spot's" " site-altitude.") parser.add_argument("output_filepath", metavar="OUTPUT_FILEPATH", help="The output path for the resulting NetCDF") parser.add_argument( "--apply_lapse_rate_correction", default=False, action="store_true", help="If the option is set and a lapse rate cube has been " "provided, extracted screen temperatures will be adjusted to " "better match the altitude of the spot site for which they have " "been extracted.") method_group = parser.add_argument_group( title="Neighbour finding method", description="If none of these options are set, the nearest grid point " "to a spot site will be used without any other constraints.") method_group.add_argument( "--land_constraint", default=False, action='store_true', help="If set the neighbour cube will be interrogated for grid point" " neighbours that were identified using a land constraint. This means" " that the grid points should be land points except for sites where" " none were found within the search radius when the neighbour cube was" " created. May be used with minimum_dz.") method_group.add_argument( "--minimum_dz", default=False, action='store_true', help="If set the neighbour cube will be interrogated for grid point" " neighbours that were identified using a minimum height difference" " constraint. These are grid points that were found to be the closest" " in altitude to the spot site within the search radius defined when" " the neighbour cube was created. May be used with land_constraint.") percentile_group = parser.add_argument_group( title="Extract percentiles", description="Extract particular percentiles from probabilistic, " "percentile, or realization inputs. If deterministic input is " "provided a warning is raised and all leading dimensions are included " "in the returned spot-data cube.") percentile_group.add_argument( "--extract_percentiles", default=None, nargs='+', type=int, help="If set to a percentile value or a list of percentile values, " "data corresponding to those percentiles will be returned. For " "example setting '--extract_percentiles 25 50 75' will result in the " "25th, 50th, and 75th percentiles being returned from a cube of " "probabilities, percentiles, or realizations. Note that for " "percentile inputs, the desired percentile(s) must exist in the input " "cube.") parser.add_argument( "--ecc_bounds_warning", default=False, action="store_true", help="If True, where calculated percentiles are outside the ECC " "bounds range, raise a warning rather than an exception.") meta_group = parser.add_argument_group("Metadata") meta_group.add_argument( "--metadata_json", metavar="METADATA_JSON", default=None, help="If provided, this JSON file can be used to modify the metadata " "of the returned netCDF file. Defaults to None.") output_group = parser.add_argument_group("Suppress Verbose output") # This CLI may be used to prepare data for verification without knowing the # form of the input, be it deterministic, realizations or probabilistic. # A warning is normally raised when attempting to extract a percentile from # deterministic data as this is not possible; the spot-extraction of the # entire cube is returned. When preparing data for verification we know # that we will produce a large number of these warnings when passing in # deterministic data. This option to suppress warnings is provided to # reduce the amount of unneeded logging information that is written out. output_group.add_argument( "--suppress_warnings", default=False, action="store_true", help="Suppress warning output. This option should only be used if " "it is known that warnings will be generated but they are not " "required.") args = parser.parse_args(args=argv) neighbour_cube = load_cube(args.neighbour_filepath) diagnostic_cube = load_cube(args.diagnostic_filepath) neighbour_selection_method = NeighbourSelection( land_constraint=args.land_constraint, minimum_dz=args.minimum_dz).neighbour_finding_method_name() plugin = SpotExtraction( neighbour_selection_method=neighbour_selection_method) result = plugin.process(neighbour_cube, diagnostic_cube) # If a probability or percentile diagnostic cube is provided, extract # the given percentile if available. This is done after the spot-extraction # to minimise processing time; usually there are far fewer spot sites than # grid points. if args.extract_percentiles: try: perc_coordinate = find_percentile_coordinate(result) except CoordinateNotFoundError: if 'probability_of_' in result.name(): result = GeneratePercentilesFromProbabilities( ecc_bounds_warning=args.ecc_bounds_warning).process( result, percentiles=args.extract_percentiles) result = iris.util.squeeze(result) elif result.coords('realization', dim_coords=True): fast_percentile_method = ( False if np.ma.isMaskedArray(result.data) else True) result = PercentileConverter( 'realization', percentiles=args.extract_percentiles, fast_percentile_method=fast_percentile_method).process( result) else: msg = ('Diagnostic cube is not a known probabilistic type. ' 'The {} percentile could not be extracted. Extracting ' 'data from the cube including any leading ' 'dimensions.'.format( args.extract_percentiles)) if not args.suppress_warnings: warnings.warn(msg) else: constraint = ['{}={}'.format(perc_coordinate.name(), args.extract_percentiles)] perc_result = extract_subcube(result, constraint) if perc_result is not None: result = perc_result else: msg = ('The percentile diagnostic cube does not contain the ' 'requested percentile value. Requested {}, available ' '{}'.format(args.extract_percentiles, perc_coordinate.points)) raise ValueError(msg) # Check whether a lapse rate cube has been provided and we are dealing with # temperature data and the lapse-rate option is enabled. if (args.temperature_lapse_rate_filepath and args.apply_lapse_rate_correction): if not result.name() == "air_temperature": msg = ("A lapse rate cube was provided, but the diagnostic being " "processed is not air temperature and cannot be adjusted.") raise ValueError(msg) lapse_rate_cube = load_cube(args.temperature_lapse_rate_filepath) if not lapse_rate_cube.name() == 'air_temperature_lapse_rate': msg = ("A cube has been provided as a lapse rate cube but does " "not have the expected name air_temperature_lapse_rate: " "{}".format(lapse_rate_cube.name())) raise ValueError(msg) try: lapse_rate_height_coord = lapse_rate_cube.coord("height") except (ValueError, CoordinateNotFoundError): msg = ("Lapse rate cube does not contain a single valued height " "coordinate. This is required to ensure it is applied to " "equivalent temperature data.") raise ValueError(msg) # Check the height of the temperature data matches that used to # calculate the lapse rates. If so, adjust temperatures using the lapse # rate values. if diagnostic_cube.coord("height") == lapse_rate_height_coord: plugin = SpotLapseRateAdjust( neighbour_selection_method=neighbour_selection_method) result = plugin.process(result, neighbour_cube, lapse_rate_cube) else: msg = ("A lapse rate cube was provided, but the height of " "the temperature data does not match that of the data used " "to calculate the lapse rates. As such the temperatures " "were not adjusted with the lapse rates.") if not args.suppress_warnings: warnings.warn(msg) elif (args.apply_lapse_rate_correction and not args.temperature_lapse_rate_filepath): msg = ("A lapse rate cube was not provided, but the option to " "apply the lapse rate correction was enabled. No lapse rate " "correction could be applied.") if not args.suppress_warnings: warnings.warn(msg) # Modify final metadata as described by provided JSON file. if args.metadata_json: with open(args.metadata_json, 'r') as input_file: metadata_dict = json.load(input_file) result = amend_metadata(result, **metadata_dict) # Remove the internal model_grid_hash attribute if present. result.attributes.pop('model_grid_hash', None) # Save the spot data cube. save_netcdf(result, args.output_filepath)
def process(neighbour_cube, diagnostic_cube, lapse_rate_cube=None, apply_lapse_rate_correction=False, land_constraint=False, minimum_dz=False, extract_percentiles=None, ecc_bounds_warning=False, metadata_dict=None, suppress_warnings=False): """Module to run spot data extraction. Extract diagnostic data from gridded fields for spot data sites. It is possible to apply a temperature lapse rate adjustment to temperature data that helps to account for differences between the spot site's real altitude and that of the grid point from which the temperature data is extracted. Args: neighbour_cube (iris.cube.Cube): Cube of spot-data neighbours and the spot site information. diagnostic_cube (iris.cube.Cube): Cube containing the diagnostic data to be extracted. lapse_rate_cube (iris.cube.Cube): Cube containing temperature lapse rates. If this cube is provided and a screen temperature cube is being processed, the lapse rates will be used to adjust the temperature to better represent each spot's site-altitude. apply_lapse_rate_correction (bool): If True, and a lapse rate cube has been provided, extracted screen temperature will be adjusted to better match the altitude of the spot site for which they have been extracted. Default is False. land_constraint (bool): If True, the neighbour cube will be interrogated for grid point neighbours that were identified using a land constraint. This means that the grid points should be land points except for sites where none were found within the search radius when the neighbour cube was created. May be used with minimum_dz. Default is False. minimum_dz (bool): If True, the neighbour cube will be interrogated for grid point neighbours that were identified using the minimum height difference constraint. These are grid points that were found to be the closest in altitude to the spot site within the search radius defined when the neighbour cube was created. May be used with land_constraint. Default is False. extract_percentiles (list or int): If set to a percentile value or a list of percentile values, data corresponding to those percentiles will be returned. For example [25, 50, 75] will result in the 25th, 50th and 75th percentiles being returned from a cube of probabilities, percentiles or realizations. Note that for percentiles inputs, the desired percentile(s) must exist in the input cube. Default is None. ecc_bounds_warning (bool): If True, where calculated percentiles are outside the ECC bounds range, raises a warning rather than an exception. Default is False. metadata_dict (dict): If provided, this dictionary can be used to modify the metadata of the returned cube. Default is None. suppress_warnings (bool): Suppress warning output. This option should only be used if it is known that warnings will be generated but they are not required. Default is None. Returns: result (iris.cube.Cube): The processed cube. Raises: ValueError: If the percentile diagnostic cube does not contain the requested percentile value. ValueError: If the lapse rate cube was provided but the diagnostic being processed is not air temperature. ValueError: If the lapse rate cube provided does not have the name "air_temperature_lapse_rate" ValueError: If the lapse rate cube does not contain a single valued height coordinate. Warns: warning: If diagnostic cube is not a known probabilistic type. warning: If a lapse rate cube was provided, but the height of the temperature does not match that of the data used. warning: If a lapse rate cube was not provided, but the option to apply the lapse rate correction was enabled. """ neighbour_selection_method = NeighbourSelection( land_constraint=land_constraint, minimum_dz=minimum_dz).neighbour_finding_method_name() plugin = SpotExtraction( neighbour_selection_method=neighbour_selection_method) result = plugin.process(neighbour_cube, diagnostic_cube) # If a probability or percentile diagnostic cube is provided, extract # the given percentile if available. This is done after the spot-extraction # to minimise processing time; usually there are far fewer spot sites than # grid points. if extract_percentiles is not None: try: perc_coordinate = find_percentile_coordinate(result) except CoordinateNotFoundError: if 'probability_of_' in result.name(): result = GeneratePercentilesFromProbabilities( ecc_bounds_warning=ecc_bounds_warning).process( result, percentiles=extract_percentiles) result = iris.util.squeeze(result) elif result.coords('realization', dim_coords=True): fast_percentile_method = (False if np.ma.isMaskedArray( result.data) else True) result = PercentileConverter( 'realization', percentiles=extract_percentiles, fast_percentile_method=fast_percentile_method).process( result) else: msg = ('Diagnostic cube is not a known probabilistic type. ' 'The {} percentile could not be extracted. Extracting ' 'data from the cube including any leading ' 'dimensions.'.format(extract_percentiles)) if not suppress_warnings: warnings.warn(msg) else: constraint = [ '{}={}'.format(perc_coordinate.name(), extract_percentiles) ] perc_result = extract_subcube(result, constraint) if perc_result is not None: result = perc_result else: msg = ('The percentile diagnostic cube does not contain the ' 'requested percentile value. Requested {}, available ' '{}'.format(extract_percentiles, perc_coordinate.points)) raise ValueError(msg) # Check whether a lapse rate cube has been provided and we are dealing with # temperature data and the lapse-rate option is enabled. if apply_lapse_rate_correction and lapse_rate_cube: if not result.name() == "air_temperature": msg = ("A lapse rate cube was provided, but the diagnostic being " "processed is not air temperature and cannot be adjusted.") raise ValueError(msg) if not lapse_rate_cube.name() == 'air_temperature_lapse_rate': msg = ("A cube has been provided as a lapse rate cube but does " "not have the expected name air_temperature_lapse_rate: " "{}".format(lapse_rate_cube.name())) raise ValueError(msg) try: lapse_rate_height_coord = lapse_rate_cube.coord("height") except (ValueError, CoordinateNotFoundError): msg = ("Lapse rate cube does not contain a single valued height " "coordinate. This is required to ensure it is applied to " "equivalent temperature data.") raise ValueError(msg) # Check the height of the temperature data matches that used to # calculate the lapse rates. If so, adjust temperatures using the lapse # rate values. if diagnostic_cube.coord("height") == lapse_rate_height_coord: plugin = SpotLapseRateAdjust( neighbour_selection_method=neighbour_selection_method) result = plugin.process(result, neighbour_cube, lapse_rate_cube) elif not suppress_warnings: warnings.warn( "A lapse rate cube was provided, but the height of the " "temperature data does not match that of the data used " "to calculate the lapse rates. As such the temperatures " "were not adjusted with the lapse rates.") elif apply_lapse_rate_correction and not lapse_rate_cube: if not suppress_warnings: warnings.warn( "A lapse rate cube was not provided, but the option to " "apply the lapse rate correction was enabled. No lapse rate " "correction could be applied.") # Modify final metadata as described by provided JSON file. if metadata_dict: result = amend_metadata(result, **metadata_dict) # Remove the internal model_grid_hash attribute if present. result.attributes.pop('model_grid_hash', None) return result