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
Example #3
0
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
Example #4
0
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
Example #5
0
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
Example #6
0
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