def process(cube: cli.inputcube, raw_cube: cli.inputcube = None, *, realizations_count: int = None, random_seed: int = None, ignore_ecc_bounds=False): """Converts an incoming cube into one containing realizations. Args: cube (iris.cube.Cube): A cube to be processed. raw_cube (iris.cube.Cube): Cube of raw (not post processed) weather data. If this argument is given ensemble realizations will be created from percentiles by reshuffling them in correspondence to the rank order of the raw ensemble. Otherwise, the percentiles are rebadged as realizations. realizations_count (int): The number of ensemble realizations in the output. random_seed (int): Option to specify a value for the random seed for testing purposes, otherwise the default random seed behaviours is utilised. The random seed is used in the generation of the random numbers used for splitting tied values within the raw ensemble, so that the values from the input percentiles can be ordered to match the raw ensemble. ignore_ecc_bounds (bool): If True, where percentiles exceed the ECC bounds range, raises a warning rather than an exception. Returns: iris.cube.Cube: The processed cube. """ from improver.cli import (percentiles_to_realizations, probabilities_to_realizations) if cube.coords('percentile'): output_cube = percentiles_to_realizations.process( cube, raw_cube=raw_cube, realizations_count=realizations_count, random_seed=random_seed, ignore_ecc_bounds=ignore_ecc_bounds) elif cube.coords(var_name='threshold'): output_cube = probabilities_to_realizations.process( cube, raw_cube=raw_cube, realizations_count=realizations_count, random_seed=random_seed, ignore_ecc_bounds=ignore_ecc_bounds) elif cube.coords(var_name='realization'): output_cube = cube else: raise ValueError("Unable to convert to realizations:\n" + str(cube)) return output_cube
def process(cube: cli.inputcube, *, realizations_count: int = None, ignore_ecc_bounds=False): """Converts an incoming cube into one containing realizations. Args: cube (iris.cube.Cube): A cube to be processed. realizations_count (int): The number of ensemble realizations in the output. ignore_ecc_bounds (bool): If True, where percentiles exceed the ECC bounds range, raises a warning rather than an exception. Returns: iris.cube.Cube: The processed cube. """ from improver.cli import (percentiles_to_realizations, probabilities_to_realizations) if cube.coords('percentile'): output_cube = percentiles_to_realizations.process( cube, realizations_count=realizations_count, ignore_ecc_bounds=ignore_ecc_bounds) elif cube.coords(var_name='threshold'): output_cube = probabilities_to_realizations.process( cube, realizations_count=realizations_count, ignore_ecc_bounds=ignore_ecc_bounds) elif cube.coords(var_name='realization'): output_cube = cube else: raise ValueError("Unable to convert to realizations:\n" + str(cube)) return output_cube
def process( cube: cli.inputcube, mask: cli.inputcube, weights: cli.inputcube = None, *, neighbourhood_shape="square", radii: cli.comma_separated_list, lead_times: cli.comma_separated_list = None, area_sum=False, ): """ Module to process land and sea separately before combining them. Neighbourhood the input dataset over two distinct regions of land and sea. If performed as a single level neighbourhood, a land-sea mask should be provided. If instead topographic_zone neighbourhooding is being employed, the mask should be one of topographic zones. In the latter case a weights array is also needed to collapse the topographic_zone coordinate. These weights are created with the improver generate-topography-bands-weights CLI and should be made using a land-sea mask, which will then be employed within this code to draw the distinction between the two surface types. Args: cube (iris.cube.Cube): A cube to be processed. mask (iris.cube.Cube): A cube containing either a mask of topographic zones over land or a land-sea mask. If this is a land-sea mask, land points should be set to one and sea points set to zero. weights (iris.cube.Cube): A cube containing the weights which are used for collapsing the dimension gained through masking. These weights must have been created using a land-sea mask. (Optional). neighbourhood_shape (str): Name of the neighbourhood method to use. Options: "circular", "square". Default: "square". radii (list of float): The radius or a list of radii in metres of the neighbourhood to apply. If it is a list, it must be the same length as lead_times, which defines at which lead time to use which nbhood radius. The radius will be interpolated for intermediate lead times. lead_times (list of int): The lead times in hours that correspond to the radii to be used. If lead_times are set, radii must be a list the same length as lead_times. Lead times must be given as integer values. area_sum (bool): Return sum rather than fraction over the neighbourhood area. Returns: (tuple): tuple containing: **result** (iris.cube.Cube): A cube of the processed data. Raises: ValueError: If the topographic zone mask has the attribute topographic_zones_include_seapoints. IOError: if a weights cube isn't given and a topographic_zone mask is given. ValueError: If the weights cube has the attribute topographic_zones_include_seapoints. RuntimeError: If lead times are not None and has a different length to radii. TypeError: A weights cube has been provided but no topographic zone. """ import numpy as np from improver.nbhood.nbhood import NeighbourhoodProcessing from improver.nbhood.use_nbhood import ApplyNeighbourhoodProcessingWithAMask masking_coordinate = None if any( "topographic_zone" in coord.name() for coord in mask.coords(dim_coords=True) ): if mask.attributes["topographic_zones_include_seapoints"] == "True": raise ValueError( "The topographic zones mask cube must have been " "masked to exclude sea points, but " "topographic_zones_include_seapoints = True" ) if not weights: raise TypeError( "A weights cube must be provided if using a mask " "of topographic zones to collapse the resulting " "vertical dimension." ) if weights.attributes["topographic_zones_include_seapoints"] == "True": raise ValueError( "The weights cube must be masked to exclude sea " "points, but topographic_zones_include_seapoints " "= True" ) masking_coordinate = "topographic_zone" land_sea_mask = weights[0].copy(data=weights[0].data.mask) land_sea_mask.rename("land_binary_mask") land_sea_mask.remove_coord(masking_coordinate) # Create land and sea masks in IMPROVER format (inverse of # numpy standard) 1 - include this region, 0 - exclude this region. land_only = land_sea_mask.copy( data=np.logical_not(land_sea_mask.data).astype(int) ) sea_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int)) else: if weights is not None: raise TypeError("A weights cube has been provided but will not be " "used") land_sea_mask = mask # In this case the land is set to 1 and the sea is set to 0 in the # input mask. sea_only = land_sea_mask.copy( data=np.logical_not(land_sea_mask.data).astype(int) ) land_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int)) if lead_times is None: radius_or_radii = float(radii[0]) else: if len(radii) != len(lead_times): raise RuntimeError( "If leadtimes are supplied, it must be a list" " of equal length to a list of radii." ) radius_or_radii = [float(x) for x in radii] lead_times = [int(x) for x in lead_times] # Section for neighbourhood processing land points. if land_only.data.max() > 0.0: if masking_coordinate is None: result_land = NeighbourhoodProcessing( neighbourhood_shape, radius_or_radii, lead_times=lead_times, sum_only=area_sum, re_mask=True, )(cube, land_only) else: result_land = ApplyNeighbourhoodProcessingWithAMask( masking_coordinate, neighbourhood_shape, radius_or_radii, lead_times=lead_times, collapse_weights=weights, sum_only=area_sum, )(cube, mask) result = result_land # Section for neighbourhood processing sea points. if sea_only.data.max() > 0.0: result_sea = NeighbourhoodProcessing( neighbourhood_shape, radius_or_radii, lead_times=lead_times, sum_only=area_sum, re_mask=True, )(cube, sea_only) result = result_sea # Section for combining land and sea points following land and sea points # being neighbourhood processed individually. if sea_only.data.max() > 0.0 and land_only.data.max() > 0.0: # Recombine cubes to be a single output. combined_data = result_land.data.filled(0) + result_sea.data.filled(0) result = result_land.copy(data=combined_data) return result
def process( wind_speed: cli.inputcube, sigma: cli.inputcube, target_orography: cli.inputcube, standard_orography: cli.inputcube, silhouette_roughness: cli.inputcube, vegetative_roughness: cli.inputcube = None, *, model_resolution: float, output_height_level: float = None, output_height_level_units="m", ): """Wind downscaling. Run wind downscaling to apply roughness correction and height correction to wind fields as described in Howard and Clark (2007). All inputs must be on the same standard grid. Args: wind_speed (iris.cube.Cube): Cube of wind speed on standard grid. Any units can be supplied. sigma (iris.cube.Cube): Cube of standard deviation of model orography height. Units of field: m. target_orography (iris.cube.Cube): Cube of orography to downscale fields to. Units of field: m. standard_orography (iris.cube.Cube): Cube of orography on standard grid. (interpolated model orography). Units of field: m. silhouette_roughness (iris.cube.Cube): Cube of model silhouette roughness. Units of field: dimensionless. vegetative_roughness (iris.cube.Cube): Cube of vegetative roughness length. Units of field: m. model_resolution (float): Original resolution of model orography (before interpolation to standard grid) Units of field: m. output_height_level (float): If only a single height level is desired as output from wind-downscaling, this option can be used to select the height level. If no units are provided with 'output_height_level_units', metres are assumed. output_height_level_units (str): If a single height level is selected as output using 'output_height_level', this additional argument may be used to specify the units of the value entered to select the level. e.g hPa. Returns: iris.cube.Cube: The processed Cube. Rises: ValueError: If the requested height value is not found. """ import warnings import iris from iris.exceptions import CoordinateNotFoundError from improver.utilities.cube_extraction import apply_extraction from improver.wind_calculations import wind_downscaling if output_height_level_units and output_height_level is None: warnings.warn( "output_height_level_units has been set but no " "associated height level has been provided. These units " "will have no effect." ) try: wind_speed_iterator = wind_speed.slices_over("realization") except CoordinateNotFoundError: wind_speed_iterator = [wind_speed] wind_speed_list = iris.cube.CubeList() for wind_speed_slice in wind_speed_iterator: result = wind_downscaling.RoughnessCorrection( silhouette_roughness, sigma, target_orography, standard_orography, model_resolution, z0_cube=vegetative_roughness, height_levels_cube=None, )(wind_speed_slice) wind_speed_list.append(result) wind_speed = wind_speed_list.merge_cube() non_dim_coords = [x.name() for x in wind_speed.coords(dim_coords=False)] if "realization" in non_dim_coords: wind_speed = iris.util.new_axis(wind_speed, "realization") if output_height_level is not None: constraints = {"height": output_height_level} units = {"height": output_height_level_units} single_level = apply_extraction( wind_speed, iris.Constraint(**constraints), units ) if not single_level: raise ValueError( "Requested height level not found, no cube " "returned. Available height levels are:\n" "{0:}\nin units of {1:}".format( wind_speed.coord("height").points, wind_speed.coord("height").units ) ) wind_speed = single_level return wind_speed
def process( cube: cli.inputcube, raw_cube: cli.inputcube = None, *, realizations_count: int = None, random_seed: int = None, ignore_ecc_bounds=False, ): """Converts an incoming cube into one containing realizations. Args: cube (iris.cube.Cube): A cube to be processed. raw_cube (iris.cube.Cube): Cube of raw (not post processed) weather data. If this argument is given ensemble realizations will be created from percentiles by reshuffling them in correspondence to the rank order of the raw ensemble. Otherwise, the percentiles are rebadged as realizations. realizations_count (int): The number of ensemble realizations in the output. random_seed (int): Option to specify a value for the random seed when reordering percentiles. This value is for testing purposes only, to ensure reproduceable outputs. It should not be used in real time operations as it may introduce a bias into the reordered forecasts. ignore_ecc_bounds (bool): If True where percentiles (calculated as an intermediate output before realization) exceed the ECC bounds range, raises a warning rather than an exception. Returns: iris.cube.Cube: The processed cube. """ from improver.ensemble_copula_coupling.ensemble_copula_coupling import ( ConvertProbabilitiesToPercentiles, EnsembleReordering, RebadgePercentilesAsRealizations, ResamplePercentiles, ) from improver.metadata.probabilistic import is_probability if cube.coords("realization"): return cube if not cube.coords("percentile") and not is_probability(cube): raise ValueError("Unable to convert to realizations:\n" + str(cube)) if realizations_count is None: try: realizations_count = len(raw_cube.coord("realization").points) except AttributeError: # raised if raw_cube is None, hence has no attribute "coord" msg = "Either realizations_count or raw_cube must be provided" raise ValueError(msg) if cube.coords("percentile"): percentiles = ResamplePercentiles( ecc_bounds_warning=ignore_ecc_bounds)( cube, no_of_percentiles=realizations_count) else: percentiles = ConvertProbabilitiesToPercentiles( ecc_bounds_warning=ignore_ecc_bounds)( cube, no_of_percentiles=realizations_count) if raw_cube: result = EnsembleReordering()(percentiles, raw_cube, random_seed=random_seed) else: result = RebadgePercentilesAsRealizations()(percentiles) return result
def process(cube: cli.inputcube, mask: cli.inputcube, weights: cli.inputcube = None, *, radii: cli.comma_separated_list, lead_times: cli.comma_separated_list = None, area_sum=False, return_intermediate=False): """ Module to process land and sea separately before combining them. Neighbourhood the input dataset over two distinct regions of land and sea. If performed as a single level neighbourhood, a land-sea mask should be provided. If instead topographic_zone neighbourhooding is being employed, the mask should be one of topographic zones. In the latter case a weights array is also needed to collapse the topographic_zone coordinate. These weights are created with the improver generate-topography-bands-weights CLI and should be made using a land-sea mask, which will then be employed within this code to draw the distinction between the two surface types. Args: cube (iris.cube.Cube): A cube to be processed. mask (iris.cube.Cube): A cube containing either a mask of topographic zones over land or a land-sea mask. weights (iris.cube.Cube): A cube containing the weights which are used for collapsing the dimension gained through masking. These weights must have been created using a land-sea mask. (Optional). radii (list of float): The radius or a list of radii in metres of the neighbourhood to apply. If it is a list, it must be the same length as lead_times, which defines at which lead time to use which nbhood radius. The radius will be interpolated for intermediate lead times. lead_times (list of int): The lead times in hours that correspond to the radii to be used. If lead_times are set, radii must be a list the same length as lead_times. Lead times must be given as integer values. area_sum (bool): Return sum rather than fraction over the neighbourhood area. return_intermediate (bool): Include this option to return a cube with results following topographic masked neighbourhood processing of land points and prior to collapsing the topographic_zone coordinate. If no topographic masked neighbourhooding occurs, there will be no intermediate cube and a warning. Returns: (tuple): tuple containing: **result** (iris.cube.Cube): A cube of the processed data. **intermediate_cube** (iris.cube.Cube): A cube of the intermediate data, before collapsing. Raises: ValueError: If the topographic zone mask has the attribute topographic_zones_include_seapoints. IOError: if a weights cube isn't given and a topographic_zone mask is given. ValueError: If the weights cube has the attribute topographic_zones_include_seapoints. RuntimeError: If lead times are not None and has a different length to radii. TypeError: A weights cube has been provided but no topographic zone. """ import warnings import numpy as np from improver.nbhood.nbhood import NeighbourhoodProcessing from improver.nbhood.use_nbhood import ( ApplyNeighbourhoodProcessingWithAMask, CollapseMaskedNeighbourhoodCoordinate) sum_or_fraction = 'sum' if area_sum else 'fraction' masking_coordinate = intermediate_cube = None if any([ 'topographic_zone' in coord.name() for coord in mask.coords(dim_coords=True) ]): if mask.attributes['topographic_zones_include_seapoints'] == 'True': raise ValueError('The topographic zones mask cube must have been ' 'masked to exclude sea points, but ' 'topographic_zones_include_seapoints = True') if not weights: raise TypeError('A weights cube must be provided if using a mask ' 'of topographic zones to collapse the resulting ' 'vertical dimension.') if weights.attributes['topographic_zones_include_seapoints'] == 'True': raise ValueError('The weights cube must be masked to exclude sea ' 'points, but topographic_zones_include_seapoints ' '= True') masking_coordinate = 'topographic_zone' land_sea_mask = weights[0].copy(data=weights[0].data.mask) land_sea_mask.rename('land_binary_mask') land_sea_mask.remove_coord(masking_coordinate) # Create land and sea masks in IMPROVER format (inverse of # numpy standard) 1 - include this region, 0 - exclude this region. land_only = land_sea_mask.copy( data=np.logical_not(land_sea_mask.data).astype(int)) sea_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int)) else: if weights is not None: raise TypeError('A weights cube has been provided but will not be ' 'used') land_sea_mask = mask # In this case the land is set to 1 and the sea is set to 0 in the # input mask. sea_only = land_sea_mask.copy( data=np.logical_not(land_sea_mask.data).astype(int)) land_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int)) if lead_times is None: radius_or_radii = float(radii[0]) else: if len(radii) != len(lead_times): raise RuntimeError("If leadtimes are supplied, it must be a list" " of equal length to a list of radii.") radius_or_radii = [float(x) for x in radii] lead_times = [int(x) for x in lead_times] if return_intermediate is not None and masking_coordinate is None: warnings.warn('No topographic_zone coordinate found, so no ' 'intermediate file will be saved.') # Section for neighbourhood processing land points. if land_only.data.max() > 0.0: if masking_coordinate is None: result_land = NeighbourhoodProcessing( 'square', radius_or_radii, lead_times=lead_times, sum_or_fraction=sum_or_fraction, re_mask=True).process(cube, land_only) else: result_land = ApplyNeighbourhoodProcessingWithAMask( masking_coordinate, radius_or_radii, lead_times=lead_times, sum_or_fraction=sum_or_fraction, re_mask=False).process(cube, mask) if return_intermediate: intermediate_cube = result_land.copy() # Collapse the masking coordinate. result_land = CollapseMaskedNeighbourhoodCoordinate( masking_coordinate, weights=weights).process(result_land) result = result_land # Section for neighbourhood processing sea points. if sea_only.data.max() > 0.0: result_sea = NeighbourhoodProcessing('square', radius_or_radii, lead_times=lead_times, sum_or_fraction=sum_or_fraction, re_mask=True).process( cube, sea_only) result = result_sea # Section for combining land and sea points following land and sea points # being neighbourhood processed individually. if sea_only.data.max() > 0.0 and land_only.data.max() > 0.0: # Recombine cubes to be a single output. combined_data = result_land.data.filled(0) + result_sea.data.filled(0) result = result_land.copy(data=combined_data) return result, intermediate_cube