def main(argv=None): """Load in arguments and get going.""" parser = ArgParser( description='Apply the requested neighbourhood method via ' 'the NeighbourhoodProcessing plugin to a file ' 'whose data can be loaded as a single iris.cube.Cube.') parser.add_argument( 'neighbourhood_output', metavar='NEIGHBOURHOOD_OUTPUT', help='The form of the results generated using neighbourhood ' 'processing. If "probabilities" is selected, the mean ' 'probability within a neighbourhood is calculated. If ' '"percentiles" is selected, then the percentiles are calculated ' 'within a neighbourhood. Calculating percentiles from a ' 'neighbourhood is only supported for a circular neighbourhood. ' 'Options: "probabilities", "percentiles".') parser.add_argument('neighbourhood_shape', metavar='NEIGHBOURHOOD_SHAPE', choices=["circular", "square"], help='The shape of the neighbourhood to apply in ' 'neighbourhood processing. Only a "circular" ' 'neighbourhood shape is applicable for ' 'calculating "percentiles" output. ' 'Options: "circular", "square".') group = parser.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('--degrees_as_complex', action='store_true', default=False, help='Set this flag to process angles,' ' eg wind directions, as complex numbers. Not ' 'compatible with circular kernel, percentiles or ' 'recursive filter.') parser.add_argument('--weighted_mode', action='store_true', default=False, help='For neighbourhood processing using a circular ' 'kernel, setting the weighted_mode indicates the ' 'weighting decreases with radius. ' 'If weighted_mode is not set, a constant ' 'weighting is assumed. weighted_mode is only ' 'applicable for calculating "probability" ' 'neighbourhood output.') 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('--re_mask', action='store_true', help='If re_mask is set (i.e. True), the original ' 'un-neighbourhood processed mask is applied to ' 'mask out the neighbourhood processed dataset. ' 'If not set, re_mask defaults to False and 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. ') parser.add_argument('--percentiles', metavar='PERCENTILES', default=DEFAULT_PERCENTILES, nargs='+', type=float, help='Calculate values at the specified percentiles ' 'from the neighbourhood surrounding each grid ' 'point.') parser.add_argument('input_filepath', metavar='INPUT_FILE', help='A path to an input NetCDF file to be processed.') parser.add_argument('output_filepath', metavar='OUTPUT_FILE', help='The output path for the processed NetCDF.') parser.add_argument('--input_mask_filepath', metavar='INPUT_MASK_FILE', help='A path to an input mask NetCDF file to be ' 'used to mask the input file. ' 'This is currently only supported for square ' 'neighbourhoods. The data should contain 1 for ' 'usable points and 0 for discarded points, e.g. ' 'a land-mask.') parser.add_argument('--halo_radius', metavar='HALO_RADIUS', default=None, type=float, help='radius in metres of excess halo to clip.' ' Used where a larger' ' grid was defined than the standard grid' ' and we want to clip the grid back to the' ' standard grid e.g. for global data' ' regridded to UK area. Default=None') parser.add_argument('--apply-recursive-filter', action='store_true', default=False, help='Option to apply the recursive filter to a ' 'square neighbourhooded output dataset, ' 'converting it into a Gaussian-like kernel or ' 'smoothing over short distances. ' 'The filter uses an alpha ' 'parameter (0 < alpha < 1) to control what ' 'proportion of the probability is passed onto ' 'the next grid-square in the x and y directions. ' 'The alpha parameter can be set on a grid-square ' 'by grid-square basis for the x and y directions ' 'separately (using two arrays of alpha ' 'parameters of the same dimensionality as the ' 'domain). Alternatively a single alpha value can ' 'be set for each of the x and y directions. These' ' methods can be mixed, e.g. an array for the x ' 'direction and a float for the y direction and ' 'vice versa. The recursive filter cannot be ' 'applied to a circular kernel') parser.add_argument('--input_filepath_alphas_x_cube', metavar='ALPHAS_X_FILE', help='A path to a NetCDF file describing the alpha ' 'factors to be used for smoothing in the x ' 'direction when applying the recursive filter') parser.add_argument('--input_filepath_alphas_y_cube', metavar='ALPHAS_Y_FILE', help='A path to a NetCDF file describing the alpha ' 'factors to be used for smoothing in the y ' 'direction when applying the recursive filter') parser.add_argument('--alpha_x', metavar='ALPHA_X', default=None, type=float, help='A single alpha factor (0 < alpha_x < 1) to be ' 'applied to every grid square in the x ' 'direction when applying the recursive filter') parser.add_argument('--alpha_y', metavar='ALPHA_Y', default=None, type=float, help='A single alpha factor (0 < alpha_y < 1) to be ' 'applied to every grid square in the y ' 'direction when applying the recursive filter.') parser.add_argument('--iterations', metavar='ITERATIONS', default=1, type=int, help='Number of times to apply the filter, default=1 ' '(typically < 5)') args = parser.parse_args(args=argv) if (args.neighbourhood_output == "percentiles" and args.neighbourhood_shape == "square"): parser.wrong_args_error('square', 'neighbourhood_shape') if (args.neighbourhood_output == "percentiles" and args.weighted_mode): parser.wrong_args_error('weighted_mode', 'neighbourhood_shape=percentiles') if (args.neighbourhood_output == "probabilities" and args.percentiles != DEFAULT_PERCENTILES): parser.wrong_args_error('percentiles', 'neighbourhood_shape=probabilities') if (args.input_mask_filepath and args.neighbourhood_shape == "circular"): parser.wrong_args_error('neighbourhood_shape=circular', 'input_mask_filepath') if args.degrees_as_complex: if args.neighbourhood_output == "percentiles": parser.error('Cannot generate percentiles from complex numbers') if args.neighbourhood_shape == "circular": parser.error('Cannot process complex numbers with circular ' 'neighbourhoods') if args.apply_recursive_filter: parser.error('Cannot process complex numbers with recursive ' 'filter') cube = load_cube(args.input_filepath) if args.degrees_as_complex: # convert cube data into complex numbers cube.data = WindDirection.deg_to_complex(cube.data) 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.input_mask_filepath: mask_cube = load_cube(args.input_mask_filepath) else: mask_cube = None if args.neighbourhood_output == "probabilities": result = (NeighbourhoodProcessing(args.neighbourhood_shape, radius_or_radii, lead_times=lead_times, weighted_mode=args.weighted_mode, sum_or_fraction=args.sum_or_fraction, re_mask=args.re_mask).process( cube, mask_cube=mask_cube)) elif args.neighbourhood_output == "percentiles": result = (GeneratePercentilesFromANeighbourhood( args.neighbourhood_shape, radius_or_radii, lead_times=lead_times, percentiles=args.percentiles).process(cube)) # If the '--apply-recursive-filter' option has been specified in the # input command, pass the neighbourhooded 'result' cube obtained above # through the recursive-filter plugin before saving the output. # The recursive filter is only applicable to square neighbourhoods. if args.neighbourhood_shape == 'square' and args.apply_recursive_filter: alphas_x_cube = None alphas_y_cube = None if args.input_filepath_alphas_x_cube is not None: alphas_x_cube = load_cube(args.input_filepath_alphas_x_cube) if args.input_filepath_alphas_y_cube is not None: alphas_y_cube = load_cube(args.input_filepath_alphas_y_cube) result = RecursiveFilter(alpha_x=args.alpha_x, alpha_y=args.alpha_y, iterations=args.iterations, re_mask=args.re_mask).process( result, alphas_x=alphas_x_cube, alphas_y=alphas_y_cube, mask_cube=mask_cube) elif args.neighbourhood_shape == 'circular' and \ args.apply_recursive_filter: raise ValueError('Recursive filter option is not applicable to ' 'circular neighbourhoods. ') if args.degrees_as_complex: # convert neighbourhooded cube back to degrees result.data = WindDirection.complex_to_deg(result.data) if args.halo_radius is not None: result = remove_cube_halo(result, args.halo_radius) save_netcdf(result, args.output_filepath)
def main(argv=None): """Load in arguments and ensure they are set correctly. Then load in the data to blend and calculate default weights using the method chosen before carrying out the blending.""" parser = ArgParser( description='Calculate the default weights to apply in weighted ' 'blending plugins using the ChooseDefaultWeightsLinear or ' 'ChooseDefaultWeightsNonLinear plugins. Then apply these ' 'weights to the dataset using the BasicWeightedAverage plugin.' ' Required for ChooseDefaultWeightsLinear: y0val and ynval.' ' Required for ChooseDefaultWeightsNonLinear: cval.' ' Required for ChooseWeightsLinear with dict: wts_dict.') parser.add_argument('--wts_calc_method', metavar='WEIGHTS_CALCULATION_METHOD', choices=['linear', 'nonlinear', 'dict'], default='linear', help='Method to use to calculate ' 'weights used in blending. "linear" (default): ' 'calculate linearly varying blending weights. ' '"nonlinear": calculate blending weights that decrease' ' exponentially with increasing blending coordinate. ' '"dict": calculate weights using a dictionary passed ' 'in as a command line argument.') parser.add_argument('coordinate', type=str, metavar='COORDINATE_TO_AVERAGE_OVER', help='The coordinate over which the blending ' 'will be applied.') parser.add_argument('--coordinate_unit', metavar='UNIT_STRING', default='hours since 1970-01-01 00:00:00', help='Units for blending coordinate. Default= ' 'hours since 1970-01-01 00:00:00') parser.add_argument('--calendar', metavar='CALENDAR', help='Calendar for time coordinate. Default=gregorian') parser.add_argument('--cycletime', metavar='CYCLETIME', type=str, help='The forecast reference time to be used after ' 'blending has been applied, in the format ' 'YYYYMMDDTHHMMZ. If not provided, the blended file ' 'will take the latest available forecast reference ' 'time from the input cubes supplied.') parser.add_argument('--model_id_attr', metavar='MODEL_ID_ATTR', type=str, default="mosg__model_configuration", help='The name of the netCDF file attribute to be ' 'used to identify the source model for ' 'multi-model blends. Default assumes Met Office ' 'model metadata. Must be present on all input ' 'files if blending over models.') parser.add_argument('--spatial_weights_from_mask', action='store_true', default=False, help='If set this option will result in the generation' ' of spatially varying weights based on the' ' masks of the data we are blending. The' ' one dimensional weights are first calculated ' ' using the chosen weights calculation method,' ' but the weights will then be adjusted spatially' ' based on where there is masked data in the data' ' we are blending. The spatial weights are' ' calculated using the' ' SpatiallyVaryingWeightsFromMask plugin.') parser.add_argument('weighting_mode', metavar='WEIGHTED_BLEND_MODE', choices=['weighted_mean', 'weighted_maximum'], help='The method used in the weighted blend. ' '"weighted_mean": calculate a normal weighted' ' mean across the coordinate. ' '"weighted_maximum": multiplies the values in the' ' coordinate by the weights, and then takes the' ' maximum.') parser.add_argument('input_filepaths', metavar='INPUT_FILES', nargs="+", help='Paths to input files to be blended.') parser.add_argument('output_filepath', metavar='OUTPUT_FILE', help='The output path for the processed NetCDF.') spatial = parser.add_argument_group( 'Spatial weights from mask options', 'Options for calculating the spatial weights using the ' 'SpatiallyVaryingWeightsFromMask plugin.') spatial.add_argument('--fuzzy_length', metavar='FUZZY_LENGTH', type=float, default=20000, help='When calculating spatially varying weights we' ' can smooth the weights so that areas close to' ' areas that are masked have lower weights than' ' those further away. This fuzzy length controls' ' the scale over which the weights are smoothed.' ' The fuzzy length is in terms of m, the' ' default is 20km. This distance is then' ' converted into a number of grid squares,' ' which does not have to be an integer. Assumes' ' the grid spacing is the same in the x and y' ' directions, and raises an error if this is not' ' true. See SpatiallyVaryingWeightsFromMask for' ' more detail.') linear = parser.add_argument_group( 'linear weights options', 'Options for the linear weights ' 'calculation in ' 'ChooseDefaultWeightsLinear') linear.add_argument('--y0val', metavar='LINEAR_STARTING_POINT', type=float, help='The relative value of the weighting start point ' '(lowest value of blend coord) for choosing default ' 'linear weights. This must be a positive float or 0.') linear.add_argument('--ynval', metavar='LINEAR_END_POINT', type=float, help='The relative value of the weighting ' 'end point (highest value of blend coord) for choosing' ' default linear weights. This must be a positive ' 'float or 0. Note that if blending over forecast ' 'reference time, ynval >= y0val would normally be ' 'expected (to give greater weight to the more recent ' 'forecast).') nonlinear = parser.add_argument_group( 'nonlinear weights options', 'Options for the non-linear ' 'weights calculation in ' 'ChooseDefaultWeightsNonLinear') nonlinear.add_argument('--cval', metavar='NON_LINEAR_FACTOR', type=float, help='Factor used to determine how skewed the ' 'non linear weights will be. ' 'A value of 1 implies equal weighting. If not ' 'set, a default value of cval=0.85 is set.') wts_dict = parser.add_argument_group( 'dict weights options', 'Options for linear weights to be ' 'calculated based on parameters ' 'read from a json file dict') wts_dict.add_argument('--wts_dict', metavar='WEIGHTS_DICTIONARY', help='Path to json file containing dictionary from ' 'which to calculate blending weights. Dictionary ' 'format is as specified in the improver.blending.' 'weights.ChooseWeightsLinear plugin.') wts_dict.add_argument('--weighting_coord', metavar='WEIGHTING_COORD', default='forecast_period', help='Name of ' 'coordinate over which linear weights should be ' 'scaled. This coordinate must be avilable in the ' 'weights dictionary.') args = parser.parse_args(args=argv) # if the linear weights method is called with non-linear args or vice # versa, exit with error if (args.wts_calc_method == "linear") and args.cval: parser.wrong_args_error('cval', 'linear') if ((args.wts_calc_method == "nonlinear") and np.any([args.y0val, args.ynval])): parser.wrong_args_error('y0val, ynval', 'non-linear') if (args.wts_calc_method == "dict") and not args.wts_dict: parser.error('Dictionary is required if --wts_calc_method="dict"') # set blending coordinate units if "time" in args.coordinate: coord_unit = Unit(args.coordinate_unit, args.calendar) elif args.coordinate_unit != 'hours since 1970-01-01 00:00:00.': coord_unit = args.coordinate_unit else: coord_unit = 'no_unit' # For blending across models, only blending across "model_id" is directly # supported. This is because the blending coordinate must be sortable, in # order to ensure that the data cube and the weights cube have coordinates # in the same order for blending. Whilst the model_configuration is # sortable itself, as it is associated with model_id, which is the # dimension coordinate, sorting the model_configuration coordinate can # result in the model_id coordinate becoming non-monotonic. As dimension # coordinates must be monotonic, this leads to the model_id coordinate # being demoted to an auxiliary coordinate. Therefore, for simplicity # model_id is used as the blending coordinate, instead of # model_configuration. # TODO: Support model_configuration as a blending coordinate directly. if args.coordinate == "model_configuration": blend_coord = "model_id" dict_coord = "model_configuration" else: blend_coord = args.coordinate dict_coord = args.coordinate # load cubes to be blended cubelist = load_cubelist(args.input_filepaths) # determine whether or not to equalise forecast periods for model # blending weights calculation weighting_coord = (args.weighting_coord if args.weighting_coord else "forecast_period") # prepare cubes for weighted blending merger = MergeCubesForWeightedBlending(blend_coord, weighting_coord=weighting_coord, model_id_attr=args.model_id_attr) cube = merger.process(cubelist, cycletime=args.cycletime) # if the coord for blending does not exist or has only one value, # update metadata only coord_names = [coord.name() for coord in cube.coords()] if (blend_coord not in coord_names) or (len( cube.coord(blend_coord).points) == 1): result = cube.copy() conform_metadata(result, cube, blend_coord, cycletime=args.cycletime) # raise a warning if this happened because the blend coordinate # doesn't exist if blend_coord not in coord_names: warnings.warn('Blend coordinate {} is not present on input ' 'data'.format(blend_coord)) # otherwise, calculate weights and blend across specified dimension else: weights = calculate_blending_weights( cube, blend_coord, args.wts_calc_method, wts_dict=args.wts_dict, weighting_coord=args.weighting_coord, coord_unit=coord_unit, y0val=args.y0val, ynval=args.ynval, cval=args.cval, dict_coord=dict_coord) if args.spatial_weights_from_mask: check_if_grid_is_equal_area(cube) grid_cells_x, _ = convert_distance_into_number_of_grid_cells( cube, args.fuzzy_length, int_grid_cells=False) SpatialWeightsPlugin = SpatiallyVaryingWeightsFromMask( grid_cells_x) weights = SpatialWeightsPlugin.process(cube, weights, blend_coord) # blend across specified dimension BlendingPlugin = WeightedBlendAcrossWholeDimension( blend_coord, args.weighting_mode, cycletime=args.cycletime) result = BlendingPlugin.process(cube, weights=weights) save_netcdf(result, args.output_filepath)
def main(argv=None): """Load in arguments and get going.""" parser = ArgParser( description="Calculate the threshold truth value of input data " "relative to the provided threshold value. By default data are " "tested to be above the thresholds, though the --below_threshold " "flag enables testing below thresholds. A fuzzy factor or fuzzy " "bounds may be provided to capture data that is close to the " "threshold.") parser.add_argument("input_filepath", metavar="INPUT_FILE", help="A path to an input NetCDF file to be processed") parser.add_argument("output_filepath", metavar="OUTPUT_FILE", help="The output path for the processed NetCDF") parser.add_argument("threshold_values", metavar="THRESHOLD_VALUES", nargs="*", type=float, help="Threshold value or values about which to " "calculate the truth values; e.g. 270 300. " "Must be omitted if --threshold_config is used.") parser.add_argument("--threshold_config", metavar="THRESHOLD_CONFIG", type=str, help="Threshold configuration JSON file containing " "thresholds and (optionally) fuzzy bounds. Best used " "in combination with --threshold_units. " "It should contain a dictionary of strings that can " "be interpreted as floats with the structure: " " \"THRESHOLD_VALUE\": [LOWER_BOUND, UPPER_BOUND] " "e.g: {\"280.0\": [278.0, 282.0], " "\"290.0\": [288.0, 292.0]}, or with structure " " \"THRESHOLD_VALUE\": \"None\" (no fuzzy bounds). " "Repeated thresholds with different bounds are not " "handled well. Only the last duplicate will be used.") parser.add_argument("--threshold_units", metavar="THRESHOLD_UNITS", default=None, type=str, help="Units of the threshold values. If not provided " "the units are assumed to be the same as those of the " "input dataset. Specifying the units here will allow " "a suitable conversion to match the input units if " "possible.") parser.add_argument("--comparison_operator", metavar="COMPARISON_OPERATOR", default='>', choices=['>', '>=', '<', '<=', 'gt', 'ge', 'lt', 'le'], help="Indicates the comparison_operator to use with " "the threshold. e.g. 'ge' or '>=' to evaluate data " ">= threshold or '<' to evaluate data < threshold. " "When using fuzzy thresholds, there is no difference " "between < and <= or > and >=." "Default is >. Valid choices: > >= < <= gt ge lt le.") parser.add_argument("--fuzzy_factor", metavar="FUZZY_FACTOR", default=None, type=float, help="A decimal fraction defining the factor about " "the threshold value(s) which should be treated as " "fuzzy. Data which fail a test against the hard " "threshold value may return a fractional truth value " "if they fall within this fuzzy factor region. Fuzzy " "factor must be in the range 0-1, with higher values " "indicating a narrower fuzzy factor region / sharper " "threshold. NB A fuzzy factor cannot be used with a " "zero threshold or a threshold_config file.") parser.add_argument("--collapse-coord", type=str, metavar="COLLAPSE-COORD", default="None", help="An optional ability to set which coordinate " "we want to collapse over. The default is set " "to None.") parser.add_argument("--vicinity", type=float, default=None, help="If set," " distance in metres used to define the vicinity " "within which to search for an occurrence.") args = parser.parse_args(args=argv) # Deal with mutual-exclusions that ArgumentParser can't handle: if args.threshold_values and args.threshold_config: raise parser.error("--threshold_config option is not compatible " "with THRESHOLD_VALUES list.") if args.fuzzy_factor and args.threshold_config: raise parser.error("--threshold_config option is not compatible " "with --fuzzy_factor option.") # Load Cube cube = load_cube(args.input_filepath) threshold_dict = None if args.threshold_config: try: # Read in threshold configuration from JSON file. with open(args.threshold_config, 'r') as input_file: threshold_dict = json.load(input_file) except ValueError as err: # Extend error message with hint for common JSON error. raise type(err)("{} in JSON file {}. \nHINT: Try " "adding a zero after the decimal point.".format( err, args.threshold_config)) except Exception as err: # Extend any errors with message about WHERE this occurred. raise type(err)("{} in JSON file {}".format( err, args.threshold_config)) # Process Cube result = process(cube, args.threshold_values, threshold_dict, args.threshold_units, args.comparison_operator, args.fuzzy_factor, args.collapse_coord, args.vicinity) # Save Cube save_netcdf(result, args.output_filepath)
def main(argv=None): """Load in arguments and ensure they are set correctly. Then load in the data to blend and calculate default weights using the method chosen before carrying out the blending.""" parser = ArgParser( description='Calculate the default weights to apply in weighted ' 'blending plugins using the ChooseDefaultWeightsLinear or ' 'ChooseDefaultWeightsNonLinear plugins. Then apply these ' 'weights to the dataset using the BasicWeightedAverage plugin.' ' Required for ChooseDefaultWeightsLinear: y0val and ynval.' ' Required for ChooseDefaultWeightsNonLinear: cval.' ' Required for ChooseWeightsLinear with dict: wts_dict.') parser.add_argument('--wts_calc_method', metavar='WEIGHTS_CALCULATION_METHOD', choices=['linear', 'nonlinear', 'dict'], default='linear', help='Method to use to calculate ' 'weights used in blending. "linear" (default): ' 'calculate linearly varying blending weights. ' '"nonlinear": calculate blending weights that decrease' ' exponentially with increasing blending coordinate. ' '"dict": calculate weights using a dictionary passed ' 'in as a command line argument.') parser.add_argument('coordinate', type=str, metavar='COORDINATE_TO_AVERAGE_OVER', help='The coordinate over which the blending ' 'will be applied.') parser.add_argument('--cycletime', metavar='CYCLETIME', type=str, help='The forecast reference time to be used after ' 'blending has been applied, in the format ' 'YYYYMMDDTHHMMZ. If not provided, the blended file ' 'will take the latest available forecast reference ' 'time from the input cubes supplied.') parser.add_argument('--model_id_attr', metavar='MODEL_ID_ATTR', type=str, default=None, help='The name of the netCDF file attribute to be ' 'used to identify the source model for ' 'multi-model blends. Default is None. ' 'Must be present on all input ' 'files if blending over models.') parser.add_argument('--spatial_weights_from_mask', action='store_true', default=False, help='If set this option will result in the generation' ' of spatially varying weights based on the' ' masks of the data we are blending. The' ' one dimensional weights are first calculated ' ' using the chosen weights calculation method,' ' but the weights will then be adjusted spatially' ' based on where there is masked data in the data' ' we are blending. The spatial weights are' ' calculated using the' ' SpatiallyVaryingWeightsFromMask plugin.') parser.add_argument('input_filepaths', metavar='INPUT_FILES', nargs="+", help='Paths to input files to be blended.') parser.add_argument('output_filepath', metavar='OUTPUT_FILE', help='The output path for the processed NetCDF.') spatial = parser.add_argument_group( 'Spatial weights from mask options', 'Options for calculating the spatial weights using the ' 'SpatiallyVaryingWeightsFromMask plugin.') spatial.add_argument('--fuzzy_length', metavar='FUZZY_LENGTH', type=float, default=20000, help='When calculating spatially varying weights we' ' can smooth the weights so that areas close to' ' areas that are masked have lower weights than' ' those further away. This fuzzy length controls' ' the scale over which the weights are smoothed.' ' The fuzzy length is in terms of m, the' ' default is 20km. This distance is then' ' converted into a number of grid squares,' ' which does not have to be an integer. Assumes' ' the grid spacing is the same in the x and y' ' directions, and raises an error if this is not' ' true. See SpatiallyVaryingWeightsFromMask for' ' more detail.') linear = parser.add_argument_group( 'linear weights options', 'Options for the linear weights ' 'calculation in ' 'ChooseDefaultWeightsLinear') linear.add_argument('--y0val', metavar='LINEAR_STARTING_POINT', type=float, help='The relative value of the weighting start point ' '(lowest value of blend coord) for choosing default ' 'linear weights. This must be a positive float or 0.') linear.add_argument('--ynval', metavar='LINEAR_END_POINT', type=float, help='The relative value of the weighting ' 'end point (highest value of blend coord) for choosing' ' default linear weights. This must be a positive ' 'float or 0. Note that if blending over forecast ' 'reference time, ynval >= y0val would normally be ' 'expected (to give greater weight to the more recent ' 'forecast).') nonlinear = parser.add_argument_group( 'nonlinear weights options', 'Options for the non-linear ' 'weights calculation in ' 'ChooseDefaultWeightsNonLinear') nonlinear.add_argument('--cval', metavar='NON_LINEAR_FACTOR', type=float, help='Factor used to determine how skewed the ' 'non linear weights will be. A value of 1 ' 'implies equal weighting.') wts_dict = parser.add_argument_group( 'dict weights options', 'Options for linear weights to be ' 'calculated based on parameters ' 'read from a json file dict') wts_dict.add_argument('--wts_dict', metavar='WEIGHTS_DICTIONARY', help='Path to json file containing dictionary from ' 'which to calculate blending weights. Dictionary ' 'format is as specified in the improver.blending.' 'weights.ChooseWeightsLinear plugin.') wts_dict.add_argument('--weighting_coord', metavar='WEIGHTING_COORD', default='forecast_period', help='Name of ' 'coordinate over which linear weights should be ' 'scaled. This coordinate must be available in the ' 'weights dictionary.') args = parser.parse_args(args=argv) # reject incorrect argument combinations if (args.wts_calc_method == "linear") and args.cval: parser.wrong_args_error('cval', 'linear') if ((args.wts_calc_method == "nonlinear") and np.any([args.y0val, args.ynval])): parser.wrong_args_error('y0val, ynval', 'non-linear') if (args.wts_calc_method == "dict") and not args.wts_dict: parser.error('Dictionary is required if --wts_calc_method="dict"') weights_dict = load_json_or_none(args.wts_dict) # Load cubes to be blended. cubelist = load_cubelist(args.input_filepaths) result = process(cubelist, args.wts_calc_method, args.coordinate, args.cycletime, args.weighting_coord, weights_dict, args.y0val, args.ynval, args.cval, args.model_id_attr, args.spatial_weights_from_mask, args.fuzzy_length) save_netcdf(result, args.output_filepath)
def main(argv=None): """Load in arguments and get going.""" parser = ArgParser( description="Calculate the threshold truth value of input data " "relative to the provided threshold value. By default data are " "tested to be above the thresholds, though the --below_threshold " "flag enables testing below thresholds. A fuzzy factor or fuzzy " "bounds may be provided to capture data that is close to the " "threshold.") parser.add_argument("input_filepath", metavar="INPUT_FILE", help="A path to an input NetCDF file to be processed") parser.add_argument("output_filepath", metavar="OUTPUT_FILE", help="The output path for the processed NetCDF") parser.add_argument("threshold_values", metavar="THRESHOLD_VALUES", nargs="*", type=float, help="Threshold value or values about which to " "calculate the truth values; e.g. 270 300. " "Must be omitted if --threshold_config is used.") parser.add_argument("--threshold_config", metavar="THRESHOLD_CONFIG", type=str, help="Threshold configuration JSON file containing " "thresholds and (optionally) fuzzy bounds. Best used " "in combination with --threshold_units. " "It should contain a dictionary of strings that can " "be interpreted as floats with the structure: " " \"THRESHOLD_VALUE\": [LOWER_BOUND, UPPER_BOUND] " "e.g: {\"280.0\": [278.0, 282.0], " "\"290.0\": [288.0, 292.0]}, or with structure " " \"THRESHOLD_VALUE\": \"None\" (no fuzzy bounds). " "Repeated thresholds with different bounds are not " "handled well. Only the last duplicate will be used.") parser.add_argument("--threshold_units", metavar="THRESHOLD_UNITS", default=None, type=str, help="Units of the threshold values. If not provided " "the units are assumed to be the same as those of the " "input dataset. Specifying the units here will allow " "a suitable conversion to match the input units if " "possible.") parser.add_argument("--below_threshold", default=False, action='store_true', help="By default truth values of 1 are returned for " "data ABOVE the threshold value(s). Using this flag " "changes this behaviour to return 1 for data below " "the threshold values.") parser.add_argument("--fuzzy_factor", metavar="FUZZY_FACTOR", default=None, type=float, help="A decimal fraction defining the factor about " "the threshold value(s) which should be treated as " "fuzzy. Data which fail a test against the hard " "threshold value may return a fractional truth value " "if they fall within this fuzzy factor region. Fuzzy " "factor must be in the range 0-1, with higher values " "indicating a narrower fuzzy factor region / sharper " "threshold. NB A fuzzy factor cannot be used with a " "zero threshold or a threshold_config file.") parser.add_argument("--collapse-coord", type=str, metavar="COLLAPSE-COORD", default="None", help="An optional ability to set which coordinate " "we want to collapse over. The default is set " "to None.") parser.add_argument("--vicinity", type=float, default=None, help="If set," " distance in metres used to define the vicinity " "within which to search for an occurrence.") args = parser.parse_args(args=argv) # Deal with mutual-exclusions that ArgumentParser can't handle: if args.threshold_values and args.threshold_config: raise parser.error("--threshold_config option is not compatible " "with THRESHOLD_VALUES list.") if args.fuzzy_factor and args.threshold_config: raise parser.error("--threshold_config option is not compatible " "with --fuzzy_factor option.") cube = load_cube(args.input_filepath) if args.threshold_config: try: # Read in threshold configuration from JSON file. with open(args.threshold_config, 'r') as input_file: thresholds_from_file = json.load(input_file) thresholds = [] fuzzy_bounds = [] is_fuzzy = True for key in thresholds_from_file.keys(): thresholds.append(float(key)) if is_fuzzy: # If the first threshold has no bounds, fuzzy_bounds is # set to None and subsequent bounds checks are skipped if thresholds_from_file[key] == "None": is_fuzzy = False fuzzy_bounds = None else: fuzzy_bounds.append(tuple(thresholds_from_file[key])) except ValueError as err: # Extend error message with hint for common JSON error. raise type(err)(err + " in JSON file {}. \nHINT: Try " "adding a zero after the decimal point.".format( args.threshold_config)) except Exception as err: # Extend any errors with message about WHERE this occurred. raise type(err)(err + " in JSON file {}".format(args.threshold_config)) else: thresholds = args.threshold_values fuzzy_bounds = None result_no_collapse_coord = BasicThreshold( thresholds, fuzzy_factor=args.fuzzy_factor, fuzzy_bounds=fuzzy_bounds, threshold_units=args.threshold_units, below_thresh_ok=args.below_threshold).process(cube) if args.vicinity is not None: # smooth thresholded occurrences over local vicinity result_no_collapse_coord = OccurrenceWithinVicinity( args.vicinity).process(result_no_collapse_coord) new_cube_name = in_vicinity_name_format( result_no_collapse_coord.name()) result_no_collapse_coord.rename(new_cube_name) if args.collapse_coord == "None": save_netcdf(result_no_collapse_coord, args.output_filepath) else: # Raise warning if result_no_collapse_coord is masked array if np.ma.isMaskedArray(result_no_collapse_coord.data): warnings.warn("Collapse-coord option not fully tested with " "masked data.") # This is where we fix values for y0val, slope and weighting_mode. # In this case they are fixed to the values required for realization # collapse. This can be changed if other functionality needs to be # implemented. weights = ChooseDefaultWeightsLinear(y0val=1.0, slope=0.0).process( result_no_collapse_coord, args.collapse_coord) BlendingPlugin = WeightedBlendAcrossWholeDimension( args.collapse_coord, weighting_mode='weighted_mean') result_collapse_coord = BlendingPlugin.process( result_no_collapse_coord, weights) save_netcdf(result_collapse_coord, args.output_filepath)