Beispiel #1
0
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
Beispiel #2
0
 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)
Beispiel #3
0
 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)
Beispiel #4
0
 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)
Beispiel #5
0
 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")
Beispiel #6
0
 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))
Beispiel #8
0
 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))
Beispiel #9
0
 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
Beispiel #11
0
 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)
Beispiel #12
0
 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)
Beispiel #13
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)
Beispiel #14
0
 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)
Beispiel #15
0
 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)
Beispiel #16
0
    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