def process( orography: cli.inputcube, land_sea_mask: cli.inputcube = None, *, bands_config: cli.inputjson = None, ): """Runs topographic bands mask generation. Reads orography and land_sea_mask fields of a cube. Creates a series of masks, where each mask excludes data below or equal to the lower threshold and excludes data above the upper threshold. Args: orography (iris.cube.Cube): The orography on a standard grid. land_sea_mask (iris.cube.Cube): The land mask on standard grid, with land points set to one and sea points set to zero. If provided sea points will be set to zero in every band. If no land mask is provided, sea points will be included in the appropriate topographic band. bands_config (dict): Definition of orography bands required. The expected format of the dictionary is e.g {'bounds':[[0, 50], [50, 200]], 'units': 'm'} The default dictionary has the following form: {'bounds': [[-500., 50.], [50., 100.], [100., 150.],[150., 200.], [200., 250.], [250., 300.], [300., 400.], [400., 500.], [500., 650.],[650., 800.], [800., 950.], [950., 6000.]], 'units': 'm'} Returns: iris.cube.Cube: list of orographic band mask cube. """ from improver.generate_ancillaries.generate_ancillary import ( GenerateOrographyBandAncils, THRESHOLDS_DICT, ) if bands_config is None: bands_config = THRESHOLDS_DICT if land_sea_mask: land_sea_mask = next( land_sea_mask.slices( [land_sea_mask.coord(axis="y"), land_sea_mask.coord(axis="x")])) orography = next( orography.slices( [orography.coord(axis="y"), orography.coord(axis="x")])) result = GenerateOrographyBandAncils()(orography, bands_config, landmask=land_sea_mask) result = result.concatenate_cube() return result
def test_nonzero_landband_cube(self): """test that a correct cube is produced when neither landband bound is zero.""" result = GenOrogMasks().gen_orography_masks( self.orography, self.landmask, self.nonzero_land_threshold) self.assertEqual( result.coord('topographic_zone').points, np.mean(self.nonzero_land_threshold))
def test_high_landband_cube(self): """test that a correct cube is produced when the land band is higher than any land in the test cube.""" result = GenOrogMasks().gen_orography_masks(self.orography, self.landmask, self.high_land_threshold) self.assertEqual( result.coord("topographic_zone").points, np.mean(self.high_land_threshold))
def test_unit_conversion_for_landband_data(self): """test correct mask is produced for land bands > 0m""" land_threshold = [0, 0.05] threshold_units = "km" result = GenOrogMasks().gen_orography_masks( self.orography, self.landmask, land_threshold, units=threshold_units ) self.assertArrayAlmostEqual(result.data, self.exp_landmask) self.assertEqual(result.coord("topographic_zone").units, Unit("m"))
def test_valleyband_cube(self): """test correct cube data is produced for land bands < 0m""" result = GenOrogMasks().gen_orography_masks(self.orography, self.landmask, self.valley_key, self.valley_threshold)[0] self.assertEqual(result.attributes['Topographical Type'], 'Land') self.assertEqual( result.coord('topographic_bound_lower').points, self.valley_threshold[0]) self.assertEqual( result.coord('topographic_bound_upper').points, self.valley_threshold[1])
def test_maxband_cube(self): """test correct cube data is produced for land bands > max""" result = GenOrogMasks().gen_orography_masks(self.orography, self.landmask, self.max_key, self.max_threshold)[0] self.assertEqual(result.attributes['Topographical Type'], 'Max_Land_Threshold') self.assertEqual( result.coord('topographic_bound_lower').points, self.max_threshold[0]) msg = 'Expected to find exactly 1 coordinate, but found none.' with self.assertRaisesRegexp(CoordinateNotFoundError, msg): result.coord('topographic_bound_upper')
def process(orography, landmask=None, thresholds_dict=None): """Runs topographic bands mask generation. Reads orography and landmask fields of a cube. Creates a series of masks, where each mask excludes data below or equal to the lower threshold and excludes data above the upper threshold. Args: orography (iris.cube.Cube): The orography a standard grid. landmask (iris.cube.Cube): The land mask on standard grid. If provided data points are set to zero in every band. Default is None. thresholds_dict (dict): Definition of orography bands required. The expected format of the dictionary is e.g {'bounds':[[0, 50], [50, 200]], 'units': 'm'} The default dictionary has the following form: {'bounds': [[-500., 50.], [50., 100.], [100., 150.],[150., 200.], [200., 250.], [250., 300.], [300., 400.], [400., 500.], [500., 650.],[650., 800.], [800., 950.], [950., 6000.]], 'units': 'm'} Returns: iris.cube.Cube: list of orographic band mask cube. """ if landmask: landmask = next( landmask.slices( [landmask.coord(axis='y'), landmask.coord(axis='x')])) orography = next( orography.slices( [orography.coord(axis='y'), orography.coord(axis='x')])) if thresholds_dict is None: thresholds_dict = THRESHOLDS_DICT result = GenerateOrographyBandAncils().process(orography, thresholds_dict, landmask=landmask) result = result.concatenate_cube() return result
def test_landband_cube(self): """test correct cube data is produced for land bands > 0m""" result = GenOrogMasks().gen_orography_masks(self.orography, self.landmask, self.land_threshold) self.assertEqual( result.attributes["topographic_zones_include_seapoints"], "False") self.assertEqual( result.coord("topographic_zone").points, np.mean(self.land_threshold)) self.assertEqual( result.coord("topographic_zone").bounds[0][0], self.land_threshold[0]) self.assertEqual( result.coord("topographic_zone").bounds[0][1], self.land_threshold[1])
def test_landband_cube(self): """test correct cube data is produced for land bands > 0m""" result = GenOrogMasks().gen_orography_masks(self.orography, self.landmask, self.land_key, self.land_threshold) self.assertEqual(result.attributes['Topographical Type'], 'Land') self.assertEqual( result.coord('topographic_zone').points, np.mean(self.land_threshold)) self.assertEqual( result.coord('topographic_zone').bounds[0][0], self.land_threshold[0]) self.assertEqual( result.coord('topographic_zone').bounds[0][1], self.land_threshold[1])
def process(self, orography, thresholds_dict, landmask=None): """Calculate the weights depending upon where the orography point is within the topographic zones. Args: orography (iris.cube.Cube): Orography on standard grid. thresholds_dict (dict): Definition of orography bands required. The expected format of the dictionary is e.g. `{'bounds': [[0, 50], [50, 200]], 'units': 'm'}` landmask (iris.cube.Cube): Land mask on standard grid, with land points set to one and sea points set to zero. If provided sea points are masked out in the output array. Returns: iris.cube.Cube: Cube containing the weights depending upon where the orography point is within the topographic zones. """ # Check that orography is a 2d cube. if len(orography.shape) != 2: msg = ("The input orography cube should be two-dimensional." "The input orography cube has {} dimensions".format( len(orography.shape))) raise InvalidCubeError(msg) # Find bands and midpoints from bounds. bands = np.array(thresholds_dict["bounds"], dtype=np.float32) threshold_units = thresholds_dict["units"] # Create topographic_zone_cube first, so that a cube is created for # each band. This will allow the data for neighbouring bands to be # put into the cube. mask_data = np.zeros(orography.shape, dtype=np.float32) topographic_zone_cubes = iris.cube.CubeList([]) for band in bands: sea_points_included = not landmask topographic_zone_cube = _make_mask_cube( mask_data, orography.coords(), band, threshold_units, sea_points_included=sea_points_included, ) topographic_zone_cubes.append(topographic_zone_cube) topographic_zone_weights = topographic_zone_cubes.concatenate_cube() topographic_zone_weights.data = topographic_zone_weights.data.astype( np.float32) # Ensure topographic_zone coordinate units is equal to orography units. topographic_zone_weights.coord("topographic_zone").convert_units( orography.units) # Read bands from cube, now that they can be guaranteed to be in the # same units as the orography. The bands are converted to a list, so # that they can be iterated through. bands = list(topographic_zone_weights.coord("topographic_zone").bounds) midpoints = topographic_zone_weights.coord("topographic_zone").points # Raise a warning, if orography extremes are outside the extremes of # the bands. if np.max(orography.data) > np.max(bands): msg = ("The maximum orography is greater than the uppermost band. " "This will potentially cause the topographic zone weights " "to not sum to 1 for a given grid point.") warnings.warn(msg) if np.min(orography.data) < np.min(bands): msg = ("The minimum orography is lower than the lowest band. " "This will potentially cause the topographic zone weights " "to not sum to 1 for a given grid point.") warnings.warn(msg) # Insert the appropriate weights into the topographic zone cube. This # includes the weights from the band that a point is in, as well as # the contribution from an adjacent band. for band_number, band in enumerate(bands): # Determine the points that are within the specified band. mask_y, mask_x = np.where((orography.data > band[0]) & (orography.data <= band[1])) orography_band = np.full(orography.shape, np.nan, dtype=np.float32) orography_band[mask_y, mask_x] = orography.data[mask_y, mask_x] # Calculate the weights. This involves calculating the # weights for all the orography but only inserting weights # that are within the band into the topographic_zone_weights cube. weights = self.calculate_weights(orography_band, band) topographic_zone_weights.data[band_number, mask_y, mask_x] = weights[mask_y, mask_x] # Calculate the contribution to the weights from the adjacent # lower band. topographic_zone_weights.data = self.add_weight_to_lower_adjacent_band( topographic_zone_weights.data, orography_band, midpoints[band_number], band_number, ) # Calculate the contribution to the weights from the adjacent # upper band. topographic_zone_weights.data = self.add_weight_to_upper_adjacent_band( topographic_zone_weights.data, orography_band, midpoints[band_number], band_number, len(bands) - 1, ) # Metadata updates topographic_zone_weights.rename("topographic_zone_weights") topographic_zone_weights.units = Unit("1") # Mask output weights using a land-sea mask. topographic_zone_masked_weights = iris.cube.CubeList([]) for topographic_zone_slice in topographic_zone_weights.slices_over( "topographic_zone"): if landmask: topographic_zone_slice.data = GenerateOrographyBandAncils( ).sea_mask(landmask.data, topographic_zone_slice.data) topographic_zone_masked_weights.append(topographic_zone_slice) topographic_zone_weights = topographic_zone_masked_weights.merge_cube() return topographic_zone_weights