Beispiel #1
0
def process(cube: cli.inputcube,
            mask: cli.inputcube,
            weights: cli.inputcube = None,
            *,
            coord_for_masking, radius: float = None,
            radii_by_lead_time=None,
            sum_or_fraction="fraction",
            remask=False,
            collapse_dimension=False):
    """Runs neighbourhooding processing iterating over a coordinate by mask.

    Apply the requested neighbourhood method via the
    ApplyNeighbourhoodProcessingWithMask plugin to a file with one diagnostic
    dataset in combination with a cube containing one or more masks.
    The mask dataset may have an extra dimension compared to the input
    diagnostic. In this case, the user specifies the name of the extra
    coordinate and this coordinate is iterated over so each mask is applied
    to separate slices over the input cube. These intermediate masked datasets
    are then concatenated, resulting in a dataset that has been processed
    using multiple masks and has gained an extra dimension from the masking.
    There is also an option to re-mask the output dataset, so that after
    neighbourhood processing non-zero values are only present for unmasked
    grid points.
    There is an alternative option of collapsing the dimension that we gain
    using this processing using a weighted average.

    Args:
        cube (iris.cube.Cube):
            Cube to be processed.
        mask (iris.cube.Cube):
            Cube to act as a mask.
        weights (iris.cube.Cube, Optional):
            Cube containing the weights which are used for collapsing the
            dimension gained through masking.
        coord_for_masking (str):
            String matching the name of the coordinate that will be used
            for masking.
        radius (float):
            The radius in metres of the neighbourhood to apply.
            Rounded up to convert into integer number of grid points east and
            north, based on the characteristic spacing at the zero indices of
            the cube projection-x and y coordinates.
        radii_by_lead_time (float or list of float):
            A list with the radius in metres at [0] and the lead_time at [1]
            Lead time is a List of lead times or forecast periods, at which
            the radii within 'radii' are defined. The lead times are expected
            in hours.
        sum_or_fraction (str):
            Identifier for whether sum or fraction should be returned from
            neighbourhooding.
            Sum represents the sum of the neighbourhood.
            Fraction represents the sum of the neighbourhood divided by the
            neighbourhood area.
        remask (bool):
            If True, the original un-neighbourhood processed mask
            is applied to mask out the neighbourhood processed cube.
            If False, the original un-neighbourhood processed mask is not
            applied.
            Therefore, the neighbourhood processing may result in
            values being present in areas that were originally masked.
        collapse_dimension (bool):
            Collapse the dimension from the mask, by doing a weighted mean
            using the weights provided.  This is only suitable when the result
            is left unmasked, so there is data to weight between the points
            in the coordinate we are collapsing.

    Returns:
        (tuple): tuple containing:
            **result** (iris.cube.Cube):
                A cube after being fully processed.
            **intermediate_cube** (iris.cube.Cube):
                A cube before it is collapsed, if 'collapse_dimension' is True.

    """
    from improver.nbhood.use_nbhood import (
        ApplyNeighbourhoodProcessingWithAMask,
        CollapseMaskedNeighbourhoodCoordinate,
    )
    from improver.utilities.cli_utilities import radius_or_radii_and_lead

    radius_or_radii, lead_times = radius_or_radii_and_lead(radius,
                                                           radii_by_lead_time)

    result = ApplyNeighbourhoodProcessingWithAMask(
        coord_for_masking, radius_or_radii, lead_times=lead_times,
        sum_or_fraction=sum_or_fraction,
        re_mask=remask).process(cube, mask)
    intermediate_cube = None

    # Collapse with the masking dimension.
    if collapse_dimension:
        intermediate_cube = result.copy()
        result = CollapseMaskedNeighbourhoodCoordinate(
            coord_for_masking, weights).process(result)
    return result, intermediate_cube
def main(argv=None):
    """Load in arguments for applying neighbourhood processing when using a
    mask."""
    parser = ArgParser(
        description='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.')

    parser.add_argument('input_filepath',
                        metavar='INPUT_FILE',
                        help='A path to an input NetCDF file to be processed.')
    parser.add_argument('input_mask_filepath',
                        metavar='INPUT_MASK',
                        help=('A path to an input NetCDF file containing '
                              'either a mask of topographic zones over land '
                              'or a land-sea mask.'))
    parser.add_argument('output_filepath',
                        metavar='OUTPUT_FILE',
                        help='The output path for the processed NetCDF.')

    mask_group = parser.add_argument_group(
        'Collapse weights - required if using a topographic zones mask')
    mask_group.add_argument('--weights_for_collapsing_dim',
                            metavar='WEIGHTS',
                            default=None,
                            help='A path to an weights NetCDF file containing '
                            'the weights which are used for collapsing the '
                            'dimension gained through masking. These weights '
                            'must have been created using a land-sea mask.')

    radius_group = parser.add_argument_group(
        'Neighbourhooding Radius - Set only one of the options')
    group = radius_group.add_mutually_exclusive_group()
    group.add_argument('--radius',
                       metavar='RADIUS',
                       type=float,
                       help='The radius (in m) for neighbourhood processing.')
    group.add_argument('--radii-by-lead-time',
                       metavar=('RADII_BY_LEAD_TIME', 'LEAD_TIME_IN_HOURS'),
                       nargs=2,
                       help='The radii for neighbourhood processing '
                       'and the associated lead times at which the radii are '
                       'valid. The radii are in metres whilst the lead time '
                       'has units of hours. The radii and lead times are '
                       'expected as individual comma-separated lists with '
                       'the list of radii given first followed by a list of '
                       'lead times to indicate at what lead time each radii '
                       'should be used. For example: 10000,12000,14000 1,2,3 '
                       'where a lead time of 1 hour uses a radius of 10000m, '
                       'a lead time of 2 hours uses a radius of 12000m, etc.')
    parser.add_argument('--sum_or_fraction',
                        default="fraction",
                        choices=["sum", "fraction"],
                        help='The neighbourhood output can either be in the '
                        'form of a sum of the neighbourhood, or a '
                        'fraction calculated by dividing the sum of the '
                        'neighbourhood by the neighbourhood area. '
                        '"fraction" is the default option.')
    parser.add_argument('--intermediate_filepath',
                        default=None,
                        help='Intermediate filepath for results following '
                        'topographic masked neighbourhood processing of '
                        'land points and prior to collapsing the '
                        'topographic_zone coordinate. Intermediate files '
                        'will not be produced if no topographic masked '
                        'neighbourhood processing occurs.')

    args = parser.parse_args(args=argv)

    cube = load_cube(args.input_filepath)
    mask = load_cube(args.input_mask_filepath, no_lazy_load=True)
    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 args.weights_for_collapsing_dim:
            raise IOError('A weights cube must be provided if using a mask '
                          'of topographic zones to collapse the resulting '
                          'vertical dimension.')

        weights = load_cube(args.weights_for_collapsing_dim, no_lazy_load=True)
        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'
        landmask = weights[0].copy(data=weights[0].data.mask)
        landmask.rename('land_binary_mask')
        landmask.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 = landmask.copy(
            data=np.logical_not(landmask.data).astype(int))
        sea_only = landmask.copy(data=landmask.data.astype(int))

    else:
        if args.weights_for_collapsing_dim:
            warnings.warn('A weights cube has been provided but will not be '
                          'used as there is no topographic zone coordinate '
                          'to collapse.')
        landmask = mask
        # In this case the land is set to 1 and the sea is set to 0 in the
        # input mask.
        sea_only = landmask.copy(
            data=np.logical_not(landmask.data).astype(int))
        land_only = landmask.copy(data=landmask.data.astype(int))

    if args.radius:
        radius_or_radii = args.radius
        lead_times = None
    elif args.radii_by_lead_time:
        radius_or_radii = args.radii_by_lead_time[0].split(",")
        lead_times = args.radii_by_lead_time[1].split(",")

    if args.intermediate_filepath is not None and masking_coordinate is None:
        msg = ('No topographic_zone coordinate found, so no intermediate file '
               'will be saved.')
        warnings.warn(msg)

    # Section for neighbourhood processing land points.
    if land_only.data.max() > 0.0:
        if masking_coordinate is not None:
            result_land = ApplyNeighbourhoodProcessingWithAMask(
                masking_coordinate,
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=args.sum_or_fraction,
                re_mask=False).process(cube, mask)
        else:
            result_land = NeighbourhoodProcessing(
                'square',
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=args.sum_or_fraction,
                re_mask=True).process(cube, land_only)

        if masking_coordinate is not None:
            if args.intermediate_filepath is not None:
                save_netcdf(result_land, args.intermediate_filepath)
            # 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=args.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)

    save_netcdf(result, args.output_filepath)