def test_process_metadata(orography):
    """Test that the cube returned by process has the expected metadata and
    coordinates. These tests cover the functionality of create_coefficient_cube."""

    expected_x_coord = 0.5 * (orography.coord(axis="x").points[1:] +
                              orography.coord(axis="x").points[:-1])
    expected_y_coord = 0.5 * (orography.coord(axis="y").points[1:] +
                              orography.coord(axis="y").points[:-1])

    plugin = OrographicSmoothingCoefficients()
    orography.attributes["history"] = "I have a history"
    result = plugin.process(orography)

    assert isinstance(result, CubeList)
    assert isinstance(result[0], Cube)
    assert result[0].attributes[
        "title"] == "Recursive filter smoothing coefficients"
    assert result[0].attributes.get("history", None) is None
    assert result[0].shape == (orography.shape[0], orography.shape[1] - 1)
    assert result[1].shape == (orography.shape[0] - 1, orography.shape[1])
    # Check x-y coordinates of x coefficients cube
    assert_array_almost_equal(result[0].coord(axis="x").points,
                              expected_x_coord)
    assert result[0].coord(axis="y") == orography.coord(axis="y")
    # Check x-y coordinates of y coefficients cube
    assert result[1].coord(axis="x") == orography.coord(axis="x")
    assert_array_almost_equal(result[1].coord(axis="y").points,
                              expected_y_coord)
    # Check there are no time coordinates on the returned cube
    with pytest.raises(CoordinateNotFoundError):
        result[0].coord(axis="t")
def test_unnormalised_smoothing_coefficients(gradient, power):
    """Test the unnormalised_smoothing_coefficients function using various
    powers."""

    plugin = OrographicSmoothingCoefficients(power=power)
    expected = np.abs(gradient.data.copy())**power
    result = plugin.unnormalised_smoothing_coefficients(gradient)
    assert_array_almost_equal(result, expected)
def test_process_with_mask(orography, mask):
    """Test generation of smoothing coefficients from orography returns the
    expected values when a mask is used to zero some elements. Here we are using
    a mask that covers a 2x2 area of the 3x3 orography cube in the bottom right
    hand corner.

    The masking is performed after scaling, so masking values does not lead to
    different scaling of the smoothing coefficients."""
    def compare_outputs(plugin, expected_x, expected_y):
        result_x, result_y = plugin.process(orography, mask[1:4, 1:4])
        assert_array_almost_equal(result_x.data, expected_x)
        assert_array_almost_equal(result_y.data, expected_y)

    # Using use_mask_boundary=True which zeroes the edges of the masked region.
    # For x this means the coefficients between the left and central columns
    # in the bottom two rows are zeroed.
    # For y this means the coefficients between the top and middle rows
    # in the right hand two columns are zeroed.
    expected_x = [[0.5, 0.5], [0.0, 0.3], [0.0, 0.1]]
    expected_y = [[0.4, 0.0, 0.0], [0.4, 0.2, 0.0]]

    plugin = OrographicSmoothingCoefficients(use_mask_boundary=True)
    compare_outputs(plugin, expected_x, expected_y)

    # Using use_mask_boundary=False gives the same result as above modified to
    # zero the smoothing coefficients that are entirely covered by the mask as
    # well. This is the bottom right corner.
    expected_x = [[0.5, 0.5], [0.0, 0.0], [0.0, 0.0]]
    expected_y = [[0.4, 0.0, 0.0], [0.4, 0.0, 0.0]]

    plugin = OrographicSmoothingCoefficients()
    compare_outputs(plugin, expected_x, expected_y)

    # Using use_mask_boundary=False and invert_mask=True. In this case only
    # the values in the bottom right corner that are entirely covered by the
    # mask will be returned.
    expected_x = [[0.0, 0.0], [0.0, 0.3], [0.0, 0.1]]
    expected_y = [[0.0, 0.0, 0.0], [0.0, 0.2, 0.0]]

    plugin = OrographicSmoothingCoefficients(invert_mask=True)
    compare_outputs(plugin, expected_x, expected_y)

    # This test repeats the one above but with the coordinates of the input
    # cubes reversed. The result should also have reversed coordinates.
    expected_x = [[0.0, 0.0, 0.0], [0.0, 0.3, 0.1]]
    expected_y = [[0.0, 0.0], [0.0, 0.2], [0.0, 0.0]]

    order = ["projection_x_coordinate", "projection_y_coordinate"]
    enforce_coordinate_ordering(orography, order)
    enforce_coordinate_ordering(mask, order)

    plugin = OrographicSmoothingCoefficients(invert_mask=True)
    compare_outputs(plugin, expected_x, expected_y)
def test_zero_masked_whole_area_inverted(smoothing_coefficients, mask):
    """Test the zero_masked function to set smoothing coefficients to zero
    across the entire unmasked area (where the mask is 0)."""

    expected_x = np.zeros((6, 5), dtype=np.float32)
    expected_x[2:4, 2] = 0.5
    expected_y = np.zeros((5, 6), dtype=np.float32)
    expected_y[2, 2:4] = 0.475

    plugin = OrographicSmoothingCoefficients(invert_mask=True)
    plugin.zero_masked(*smoothing_coefficients, mask)
    assert_array_equal(smoothing_coefficients[0].data, expected_x)
    assert_array_equal(smoothing_coefficients[1].data, expected_y)
def test_zero_masked_whole_area(smoothing_coefficients, mask):
    """Test the zero_masked function to set smoothing coefficients to zero
    across the entire masked area (where the mask is 1)."""

    expected_x = smoothing_coefficients[0].data.copy()
    expected_x[2:4, 1:4] = 0
    expected_y = smoothing_coefficients[1].data.copy()
    expected_y[1:4, 2:4] = 0

    plugin = OrographicSmoothingCoefficients()
    plugin.zero_masked(*smoothing_coefficients, mask)
    assert_array_equal(smoothing_coefficients[0].data, expected_x)
    assert_array_equal(smoothing_coefficients[1].data, expected_y)
def test_zero_masked_use_mask_boundary(smoothing_coefficients, mask):
    """Test the zero_masked function to set smoothing coefficients to zero
    around the boundary of a mask."""

    expected_x = smoothing_coefficients[0].data.copy()
    expected_x[2:4, 1] = 0
    expected_x[2:4, 3] = 0
    expected_y = smoothing_coefficients[1].data.copy()
    expected_y[1, 2:4] = 0
    expected_y[3, 2:4] = 0

    plugin = OrographicSmoothingCoefficients(use_mask_boundary=True)
    plugin.zero_masked(*smoothing_coefficients, mask)
    assert_array_equal(smoothing_coefficients[0].data, expected_x)
    assert_array_equal(smoothing_coefficients[1].data, expected_y)
def test_init():
    """Test default attribute initialisation"""
    result = OrographicSmoothingCoefficients()
    assert result.min_gradient_smoothing_coefficient == 0.5
    assert result.max_gradient_smoothing_coefficient == 0.0
    assert result.power == 1.0
    assert result.use_mask_boundary is False
예제 #8
0
    def _update_coefficients_from_mask(coeffs_x: Cube, coeffs_y: Cube,
                                       mask: Cube) -> Tuple[Cube, Cube]:
        """
        Zero all smoothing coefficients for data points that are masked

        Args:
            coeffs_x
            coeffs_y
            mask

        Returns:
            Updated smoothing coefficients
        """
        plugin = OrographicSmoothingCoefficients(use_mask_boundary=False,
                                                 invert_mask=False)
        plugin.zero_masked(coeffs_x, coeffs_y, mask)
        return coeffs_x, coeffs_y
예제 #9
0
    def _update_coefficients_from_mask(coeffs_x, coeffs_y, mask):
        """
        Zero all smoothing coefficients for data points that are masked

        Args:
            coeffs_x (iris.cube.Cube)
            coeffs_y (iris.cube.Cube)
            mask (iris.cube.Cube)

        Returns:
            tuple of iris.cube.Cube:
                Updated smoothing coefficients
        """
        plugin = OrographicSmoothingCoefficients(use_mask_boundary=False,
                                                 invert_mask=False)
        plugin.zero_masked(coeffs_x, coeffs_y, mask)
        return coeffs_x, coeffs_y
def test_init_value_error(min_value, max_value):
    """Test a ValueError is raised if the chosen smoothing coefficient limits
    are outside the range 0 to 0.5 inclusive."""

    msg = "min_gradient_smoothing_coefficient and max_gradient_smoothing_coefficient"
    with pytest.raises(ValueError, match=msg):
        OrographicSmoothingCoefficients(
            min_gradient_smoothing_coefficient=min_value,
            max_gradient_smoothing_coefficient=max_value,
        )
def test_process_no_mask(orography, min_value, max_value, expected_x,
                         expected_y):
    """Test generation of smoothing coefficients from orography returns the
    expected values.

    The orography is such that the steepest gradient is found in the y-direction,
    whilst the minimum gradient is found in the x-direction. The maximum gradient
    in x is 4/5ths of the maximum gradient in y.

    In the first test the smoothing coefficient is largest where the orography
    gradient is at its minimum, giving the largest smoothing coefficients in the
    x-dimension. In the second test smoothing coefficient is largest where the
    orography gradient is at its maximum, giving the largest smoothing
    coefficients in the y-dimension."""

    plugin = OrographicSmoothingCoefficients(
        max_gradient_smoothing_coefficient=max_value,
        min_gradient_smoothing_coefficient=min_value,
    )
    result_x, result_y = plugin.process(orography)
    assert_array_almost_equal(result_x.data[:, 0], expected_x)
    assert_array_almost_equal(result_y.data[0, :], expected_y)
def test_process_exceptions(orography, mask):
    """Test exceptions raised by the process method."""

    plugin = OrographicSmoothingCoefficients()

    # Test the orography is a cube
    msg = "expects cube"
    with pytest.raises(ValueError, match=msg):
        plugin.process(None, None)

    # Test the orography cube has 2 dimensions
    msg = "Expected orography on 2D grid"
    with pytest.raises(ValueError, match=msg):
        plugin.process(orography[0], None)

    # Test the mask cube shares the orography grid
    msg = "If a mask is provided it must have the same grid"
    mask.coord(axis="x").points = mask.coord(axis="x").points + 1.0
    with pytest.raises(ValueError, match=msg):
        plugin.process(orography, mask)
def test_scale_smoothing_coefficients(smoothing_coefficients):
    """Test the scale_smoothing_coefficients function"""

    # Test using the default max and min coefficient values
    plugin = OrographicSmoothingCoefficients()
    result = plugin.scale_smoothing_coefficients(smoothing_coefficients)
    expected_x = np.linspace(0.5, 0, 5, dtype=np.float32)
    expected_y = np.linspace(0.475, 0.05, 5, dtype=np.float32)
    assert_array_almost_equal(result[0].data[0, :], expected_x)
    assert_array_almost_equal(result[1].data[:, 0], expected_y)

    # Test using custom max and min coefficient values
    plugin = OrographicSmoothingCoefficients(
        min_gradient_smoothing_coefficient=0.4,
        max_gradient_smoothing_coefficient=0.1)
    result = plugin.scale_smoothing_coefficients(smoothing_coefficients)
    expected_x = np.linspace(0.4, 0.1, 5, dtype=np.float32)
    expected_y = np.linspace(0.385, 0.13, 5, dtype=np.float32)
    assert_array_almost_equal(result[0].data[0, :], expected_x)
    assert_array_almost_equal(result[1].data[:, 0], expected_y)
def process(
    orography: cli.inputcube,
    mask: cli.inputcube = None,
    *,
    min_gradient_smoothing_coefficient: float = 0.5,
    max_gradient_smoothing_coefficient: float = 0.0,
    power: float = 1.0,
    use_mask_boundary: bool = False,
    invert_mask: bool = False,
):
    """Generate smoothing coefficients for recursive filtering based on
    orography gradients.

    A smoothing coefficient determines how much "value" of a cell
    undergoing filtering is comprised of the current value at that cell and
    how much comes from the adjacent cell preceding it in the direction in
    which filtering is being applied. A larger smoothing_coefficient results in
    a more significant proportion of a cell's new value coming from its
    neighbouring cell.

    The smoothing coefficients are calculated from the orography gradient using
    a simple equation with the user defined value for the power:

    smoothing_coefficient = coefficient * gradient**power

    The resulting values are scaled between min_gradient_smoothing_coefficient
    and max_gradient_smoothing_coefficient to give the desired range of
    smoothing_coefficients. These limiting values must be greater than or equal to
    zero and less than or equal to 0.5.

    Note that the smoothing coefficients are returned on a grid that is one cell
    smaller in the given dimension than the input orography, i.e. the smoothing
    coefficients in the x-direction are returned on a grid that is one cell
    smaller in x than the input. This is because the coefficients are used in
    both forward and backward passes of the recursive filter, so they need to be
    symmetric between cells in the original grid to help ensure conservation.

    Args:
        orography (iris.cube.Cube):
            A 2D field of orography on the grid for which
            smoothing_coefficients are to be generated.
        mask (iris.cube.Cube):
            A mask that defines where the smoothing coefficients should
            be zeroed. Further options below determine how this mask is used.
        min_gradient_smoothing_coefficient (float):
            The value of recursive filter smoothing_coefficient to be used
            where the orography gradient is a minimum. Generally this number
            will be larger than the max_gradient_smoothing_coefficient as
            quantities are likely to be smoothed more across flat terrain.
        max_gradient_smoothing_coefficient (float):
            The value of recursive filter smoothing_coefficient to be used
            where the orography gradient is a maximum. Generally this number
            will be smaller than the min_gradient_smoothing_coefficient as
            quantities are likely to be smoothed less across complex terrain.
        power (float):
            The power for the smoothing_coefficient equation.
        use_mask_boundary (bool):
            A mask can be provided to define a region in which smoothing
            coefficients are set to zero, i.e. no smoothing. If this
            option is set to True then rather than the whole masked region
            being set to zero, only the smoothing coefficient cells that mark
            the transition from masked to unmasked will be set to zero. The
            primary purpose for this is to prevent smoothing across land-sea
            boundaries.
        invert_mask (bool):
            By default, if a mask is provided and use_mask_boundary is False,
            all the smoothing coefficients corresponding to a mask value of 1
            will be zeroed. Setting invert_mask to True reverses this behaviour
            such that mask values of 0 set the points to be zeroed in the
            smoothing coefficients. If use_mask_boundary is True this option
            has no effect.
    Returns:
        iris.cube.CubeList:
            Processed CubeList containing smoothing_coefficients_x and
            smoothing_coefficients_y cubes.
    """
    from improver.generate_ancillaries.generate_orographic_smoothing_coefficients import (
        OrographicSmoothingCoefficients, )

    plugin = OrographicSmoothingCoefficients(
        min_gradient_smoothing_coefficient,
        max_gradient_smoothing_coefficient,
        power,
        use_mask_boundary,
        invert_mask,
    )

    return plugin(orography, mask)