def test_time(self): """Test rejects cube without time coord""" cube = self.valid.copy() cube.remove_coord("time") msg = "Input cube has no time coordinate" with self.assertRaisesRegexp(InvalidCubeError, msg): check_input_coords(cube, require_time=True)
def test_additional_nonscalar_dimension(self): """Test rejects cube with multiple realizations""" vel1 = self.valid.copy() vel1.add_aux_coord(DimCoord(1, standard_name="realization")) vel2 = self.valid.copy() vel2.add_aux_coord(DimCoord(2, standard_name="realization")) invalid_3d, = (iris.cube.CubeList([vel1, vel2])).merge() msg = "Cube has 3" with self.assertRaisesRegexp(InvalidCubeError, msg): check_input_coords(invalid_3d)
def process(self, cube, timestep): """ Extrapolates input cube data and updates validity time. The input cube should have precisely two non-scalar dimension coordinates (spatial x/y), and is expected to be in a projection such that grid spacing is the same (or very close) at all points within the spatial domain. The input cube should also have a "time" coordinate. Args: cube (iris.cube.Cube): The 2D cube containing data to be advected timestep (datetime.timedelta): Advection time step Returns: iris.cube.Cube: New cube with updated time and extrapolated data. New data are filled with np.nan and masked where source data were out of bounds (ie where data could not be advected from outside the cube domain). """ # check that the input cube has precisely two non-scalar dimension # coordinates (spatial x/y) and a scalar time coordinate check_input_coords(cube, require_time=True) # check spatial coordinates match those of plugin velocities if cube.coord(axis="x") != self.x_coord or cube.coord(axis="y") != self.y_coord: raise InvalidCubeError( "Input data grid does not match advection " "velocities" ) # derive velocities in "grid squares per second" def grid_spacing(coord): """Calculate grid spacing along a given spatial axis""" new_coord = coord.copy() new_coord.convert_units("m") return np.float32(np.diff((new_coord).points)[0]) grid_vel_x = self.vel_x.data / grid_spacing(cube.coord(axis="x")) grid_vel_y = self.vel_y.data / grid_spacing(cube.coord(axis="y")) # raise a warning if data contains unmasked NaNs nan_count = np.count_nonzero(~np.isfinite(cube.data)) if nan_count > 0: warnings.warn("input data contains unmasked NaNs") # perform advection and create output cube advected_data = self._advect_field( cube.data, grid_vel_x, grid_vel_y, round(timestep.total_seconds()) ) advected_cube = self._create_output_cube(cube, advected_data, timestep) return advected_cube
def __init__(self, vel_x, vel_y, metadata_dict=None): """ Initialises the plugin. Velocities are expected to be on a regular grid (such that grid spacing in metres is the same at all points in the domain). Args: vel_x (iris.cube.Cube): Cube containing a 2D array of velocities along the x coordinate axis vel_y (iris.cube.Cube): Cube containing a 2D array of velocities along the y coordinate axis Keyword Args: metadata_dict (dict): Dictionary containing information for amending the metadata of the output cube. Please see the :func:`improver.utilities.cube_metadata.amend_metadata` for information regarding the allowed contents of the metadata dictionary. """ # check each input velocity cube has precisely two non-scalar # dimension coordinates (spatial x/y) check_input_coords(vel_x) check_input_coords(vel_y) # check input velocity cubes have the same spatial coordinates if (vel_x.coord(axis="x") != vel_y.coord(axis="x") or vel_x.coord(axis="y") != vel_y.coord(axis="y")): raise InvalidCubeError("Velocity cubes on unmatched grids") vel_x.convert_units('m s-1') vel_y.convert_units('m s-1') self.vel_x = vel_x self.vel_y = vel_y self.x_coord = vel_x.coord(axis="x") self.y_coord = vel_x.coord(axis="y") # Initialise metadata dictionary. if metadata_dict is None: metadata_dict = {} self.metadata_dict = metadata_dict
def __init__(self, vel_x: Cube, vel_y: Cube, attributes_dict: Optional[Dict] = None) -> None: """ Initialises the plugin. Velocities are expected to be on a regular grid (such that grid spacing in metres is the same at all points in the domain). Args: vel_x: Cube containing a 2D array of velocities along the x coordinate axis vel_y: Cube containing a 2D array of velocities along the y coordinate axis attributes_dict: Dictionary containing information for amending the attributes of the output cube. """ # check each input velocity cube has precisely two non-scalar # dimension coordinates (spatial x/y) check_input_coords(vel_x) check_input_coords(vel_y) # check input velocity cubes have the same spatial coordinates if vel_x.coord(axis="x") != vel_y.coord(axis="x") or vel_x.coord( axis="y") != vel_y.coord(axis="y"): raise InvalidCubeError("Velocity cubes on unmatched grids") vel_x.convert_units("m s-1") vel_y.convert_units("m s-1") self.vel_x = vel_x self.vel_y = vel_y self.x_coord = vel_x.coord(axis="x") self.y_coord = vel_x.coord(axis="y") # Initialise metadata dictionary. if attributes_dict is None: attributes_dict = {} self.attributes_dict = attributes_dict
def test_additional_scalar_dimension(self): """Test accepts cube with single realization coordinate""" vel = self.valid.copy() vel.add_aux_coord(DimCoord(1, standard_name="realization")) check_input_coords(vel)
def test_missing_spatial_dimension(self): """Test rejects cube missing y axis""" invalid_1d = self.valid[0] invalid_1d.remove_coord("projection_y_coordinate") with self.assertRaises(InvalidCubeError): check_input_coords(invalid_1d)
def process(self, cube, timestep): """ Extrapolates input cube data and updates validity time. The input cube should have precisely two non-scalar dimension coordinates (spatial x/y), and is expected to be in a projection such that grid spacing is the same (or very close) at all points within the spatial domain. The input cube should also have a "time" coordinate. Args: cube (iris.cube.Cube): The 2D cube containing data to be advected timestep (datetime.timedelta): Advection time step Returns: iris.cube.Cube: New cube with updated time and extrapolated data. New data are filled with np.nan and masked where source data were out of bounds (ie where data could not be advected from outside the cube domain). """ # check that the input cube has precisely two non-scalar dimension # coordinates (spatial x/y) and a scalar time coordinate check_input_coords(cube, require_time=True) # check spatial coordinates match those of plugin velocities if (cube.coord(axis="x") != self.x_coord or cube.coord(axis="y") != self.y_coord): raise InvalidCubeError("Input data grid does not match advection " "velocities") # derive velocities in "grid squares per second" def grid_spacing(coord): """Calculate grid spacing along a given spatial axis""" new_coord = coord.copy() new_coord.convert_units('m') return np.float32(np.diff((new_coord).points)[0]) grid_vel_x = self.vel_x.data / grid_spacing(cube.coord(axis="x")) grid_vel_y = self.vel_y.data / grid_spacing(cube.coord(axis="y")) # raise a warning if data contains unmasked NaNs nan_count = np.count_nonzero(~np.isfinite(cube.data)) if nan_count > 0: warnings.warn("input data contains unmasked NaNs") # perform advection and create output cube advected_data = self._advect_field(cube.data, grid_vel_x, grid_vel_y, timestep.total_seconds()) advected_cube = cube.copy(data=advected_data) # increment output cube time and add a "forecast_period" coordinate original_datetime, = \ (cube.coord("time").units).num2date(cube.coord("time").points) new_datetime = original_datetime + timestep new_time = (cube.coord("time").units).date2num(new_datetime) advected_cube.coord("time").points = new_time advected_cube.coord("time").convert_units( "seconds since 1970-01-01 00:00:00") advected_cube.coord("time").points = (np.around( advected_cube.coord("time").points).astype(np.int64)) try: advected_cube.coord("forecast_reference_time").convert_units( "seconds since 1970-01-01 00:00:00") except CoordinateNotFoundError: frt_coord = cube.coord("time").copy() frt_coord.rename("forecast_reference_time") advected_cube.add_aux_coord(frt_coord) advected_cube.coord("forecast_reference_time").convert_units( "seconds since 1970-01-01 00:00:00") frt_points = np.around( advected_cube.coord("forecast_reference_time").points).astype( np.int64) advected_cube.coord("forecast_reference_time").points = frt_points forecast_period_seconds = np.int32(timestep.total_seconds()) forecast_period_coord = AuxCoord(forecast_period_seconds, standard_name="forecast_period", units="s") try: advected_cube.remove_coord("forecast_period") except CoordinateNotFoundError: pass advected_cube.add_aux_coord(forecast_period_coord) # Modify the source attribute to describe the advected field as a # Nowcast if "institution" in advected_cube.attributes.keys(): advected_cube.attributes["source"] = ("{} Nowcast".format( advected_cube.attributes["institution"])) else: advected_cube.attributes["source"] = "Nowcast" amend_attributes(advected_cube, self.attributes_dict) set_history_attribute(advected_cube, "Nowcast") return advected_cube
def test_time(self): """Test rejects cube without time coord""" msg = "Input cube has no time coordinate" with self.assertRaisesRegexp(InvalidCubeError, msg): check_input_coords(self.valid, require_time=True)