def calculate_input_grid_spacing(cube_in: Cube) -> Tuple[float, float]: """ Calculate grid spacing in latitude and logitude. Check if input source grid is on even-spacing and ascending lat/lon system. Args: cube_in: Input source cube. Returns: - Grid spacing in latitude, in degree. - Grid spacing in logitude, in degree. Raises: ValueError: If input grid is not on a latitude/longitude system or input grid coordinates are not ascending. """ # check if in lat/lon system if lat_lon_determine(cube_in) is not None: raise ValueError("Input grid is not on a latitude/longitude system") # calculate grid spacing lon_spacing = calculate_grid_spacing(cube_in, "degree", axis="x", rtol=1.0e-5) lat_spacing = calculate_grid_spacing(cube_in, "degree", axis="y", rtol=1.0e-5) if lon_spacing < 0 or lat_spacing < 0: raise ValueError("Input grid coordinates are not ascending.") return lat_spacing, lon_spacing
def test_lat_lon_not_equal_spacing(self): """Test outputs with lat-lon grid in degrees""" points = self.longitude_points points[0] = -19.998 self.lat_lon_cube.coord("longitude").points = points msg = "Coordinate longitude points are not equally spaced" with self.assertRaisesRegex(ValueError, msg): calculate_grid_spacing(self.lat_lon_cube, "degrees", rtol=self.rtol)
def test_lat_lon_equal_spacing_recurring_decimal_spacing_fails(self): """Test grid spacing with lat-lon grid with with 1/3 degree intervals with tolerance of 1.0e-5""" self.lat_lon_cube.coord( "longitude").points = self.longitude_points_thirds msg = "Coordinate longitude points are not equally spaced" with self.assertRaisesRegex(ValueError, msg): calculate_grid_spacing(self.lat_lon_cube, "degrees", rtol=self.rtol)
def test_lat_lon_equal_spacing(self): """Test grid spacing outputs with lat-lon grid with tolerance""" self.lat_lon_cube.coord("longitude").points = self.longitude_points result = calculate_grid_spacing(self.lat_lon_cube, "degrees", rtol=self.rtol) self.assertAlmostEqual(result, self.expected)
def test_units(self): """Test correct answer is returned for coordinates in km""" for axis in ["x", "y"]: self.cube.coord(axis=axis).convert_units("km") result = calculate_grid_spacing(self.cube, self.unit) self.assertAlmostEqual(result, self.spacing) for axis in ["x", "y"]: self.assertEqual(self.cube.coord(axis=axis).units, "km")
def test_lat_lon_equal_spacing_recurring_decimal_spacing_passes(self): """Test grid spacing outputs with lat-lon grid with 1/3 degree intervals with tolerance of 4.0e-5""" self.lat_lon_cube.coord( "longitude").points = self.longitude_points_thirds result = calculate_grid_spacing(self.lat_lon_cube, "degrees", rtol=self.rtol_thirds) self.assertAlmostEqual(result, self.expected_thirds, places=5)
def test_failure_partial_overlap(self): """Test failure if the cutout is only partially included in the grid""" grid = set_up_variable_cube( np.ones((10, 10), dtype=np.float32), spatial_grid="equalarea" ) cutout = grid.copy() grid_spacing = calculate_grid_spacing(cutout, cutout.coord(axis="x").units) cutout.coord(axis="x").points = cutout.coord(axis="x").points + 2 * grid_spacing self.assertFalse(grid_contains_cutout(grid, cutout))
def test_failure_outside_domain(self): """Test failure if the cutout begins outside the grid domain""" grid = set_up_variable_cube(np.ones((10, 10), dtype=np.float32), spatial_grid="equalarea") cutout = grid.copy() grid_spacing = calculate_grid_spacing(cutout, cutout.coord(axis="x").units) cutout.coord(axis="x").points = (cutout.coord(axis="x").points - 10 * grid_spacing) self.assertFalse(grid_contains_cutout(grid, cutout))
def test_lat_lon_negative_spacing(self): """Test negative-striding axes grid spacing is positive with lat-lon grid in degrees""" for axis in "yx": self.lat_lon_cube.coord( axis=axis).points = self.lat_lon_cube.coord( axis=axis).points[::-1] result = calculate_grid_spacing(self.lat_lon_cube, "degrees", rtol=self.rtol, axis=axis) self.assertAlmostEqual(result, self.expected)
def _generate_displacement_array(self, ucube, vcube): """ Create displacement array of shape (2 x m x n) required by pysteps algorithm Args: ucube (iris.cube.Cube): Cube of x-advection velocities vcube (iris.cube.Cube): Cube of y-advection velocities Returns: displacement (np.ndarray): Array of shape (2, m, n) containing the x- and y-components of the m*n displacement field (format required for pysteps extrapolation algorithm) """ def _calculate_displacement(cube, interval, gridlength): """ Calculate displacement for each time step using velocity cube and time interval Args: cube (iris.cube.Cube): Cube of velocities in the x or y direction interval (int): Lead time interval, in minutes gridlength (float): Size of grid square, in metres Returns: np.ndarray: Array of displacements in grid squares per time step """ cube_ms = cube.copy() cube_ms.convert_units("m s-1") displacement = cube_ms.data * interval * 60.0 / gridlength return np.ma.filled(displacement, np.nan) gridlength = calculate_grid_spacing(self.analysis_cube, "metres") udisp = _calculate_displacement(ucube, self.interval, gridlength) vdisp = _calculate_displacement(vcube, self.interval, gridlength) displacement = np.array([udisp, vdisp]) return displacement
def test_incorrect_units(self): """Test ValueError for incorrect units""" msg = "Unable to convert from" with self.assertRaisesRegex(ValueError, msg): calculate_grid_spacing(self.lat_lon_cube, self.unit)
def test_lat_lon_equal_spacing(self): """Test outputs with lat-lon grid in degrees""" result = calculate_grid_spacing(self.lat_lon_cube, "degrees") self.assertAlmostEqual(result, 10.0)
def test_axis_keyword(self): """Test using the other axis""" self.cube.coord( axis="y").points = 2 * (self.cube.coord(axis="y").points) result = calculate_grid_spacing(self.cube, self.unit, axis="y") self.assertAlmostEqual(result, 2 * self.spacing)
def test_basic(self): """Test correct answer is returned from an equal area grid""" result = calculate_grid_spacing(self.cube, self.unit) self.assertAlmostEqual(result, self.spacing)
def test_negative_y(self): """Test positive answer is returned from a negative-striding y-axis""" result = calculate_grid_spacing(self.cube[..., ::-1, :], self.unit, axis="y") self.assertAlmostEqual(result, self.spacing)
def process(self, cube1: Cube, cube2: Cube, boxsize: int = 30) -> Tuple[Cube, Cube]: """ Extracts data from input cubes, performs dimensionless advection displacement calculation, and creates new cubes with advection velocities in metres per second. Each input cube should have precisely two non-scalar dimension coordinates (spatial x/y), and are expected to be in a projection such that grid spacing is the same (or very close) at all points within the spatial domain. Each input cube must also have a scalar "time" coordinate. Args: cube1: 2D cube that advection will be FROM / advection start point. This may be an earlier observation or an extrapolation forecast for the current time. cube2: 2D cube that advection will be TO / advection end point. This will be the most recent observation. boxsize: The side length of the square box over which to solve the optical flow constraint. This should be greater than the data smoothing radius. Returns: - 2D cube of advection velocities in the x-direction - 2D cube of advection velocities in the y-direction """ # clear existing parameters self.data_smoothing_radius = None self.boxsize = None # check input cubes have appropriate and matching contents and dimensions self._check_input_cubes(cube1, cube2) # get time over which advection displacement has occurred time_diff_seconds = self._get_advection_time(cube1, cube2) # if time difference is greater 15 minutes, increase data smoothing # radius so that larger advection displacements can be resolved grid_length_km = calculate_grid_spacing(cube1, "km") data_smoothing_radius = self._get_smoothing_radius( time_diff_seconds, grid_length_km) # fail if self.boxsize is less than data smoothing radius self.boxsize = boxsize if self.boxsize < data_smoothing_radius: msg = ("Box size {} too small (should not be less than data " "smoothing radius {})") raise ValueError(msg.format(self.boxsize, data_smoothing_radius)) # convert units to mm/hr as these avoid the need to manipulate tiny # decimals cube1 = cube1.copy() cube2 = cube2.copy() try: cube1.convert_units("mm/hr") cube2.convert_units("mm/hr") except ValueError as err: msg = ("Input data are in units that cannot be converted to mm/hr " "which are the required units for use with optical flow.") raise ValueError(msg) from err # extract 2-dimensional data arrays data1 = next( cube1.slices([cube1.coord(axis="y"), cube1.coord(axis="x")])).data data2 = next( cube2.slices([cube2.coord(axis="y"), cube2.coord(axis="x")])).data # fill any mask with 0 values so fill_values are not spread into the # domain when smoothing the fields. if np.ma.is_masked(data1): data1 = data1.filled(0) if np.ma.is_masked(data2): data2 = data2.filled(0) # if input arrays have no non-zero values, set velocities to zero here # and raise a warning if np.allclose(data1, np.zeros(data1.shape)) or np.allclose( data2, np.zeros(data2.shape)): msg = ("No non-zero data in input fields: setting optical flow " "velocities to zero") warnings.warn(msg) ucomp = np.zeros(data1.shape, dtype=np.float32) vcomp = np.zeros(data2.shape, dtype=np.float32) else: # calculate dimensionless displacement between the two input fields ucomp, vcomp = self.process_dimensionless(data1, data2, 1, 0, data_smoothing_radius) # convert displacements to velocities in metres per second for vel in [ucomp, vcomp]: vel *= np.float32(1000.0 * grid_length_km) vel /= time_diff_seconds # create velocity output cubes based on metadata from later input cube ucube = iris.cube.Cube( ucomp, long_name="precipitation_advection_x_velocity", units="m s-1", dim_coords_and_dims=[ (cube2.coord(axis="y"), 0), (cube2.coord(axis="x"), 1), ], aux_coords_and_dims=[(cube2.coord("time"), None)], ) vcube = ucube.copy(vcomp) vcube.rename("precipitation_advection_y_velocity") return ucube, vcube