def test_renormalise_when_missing_data(self):
     """
     Test we get the expected result when the weights need renormalising
     to account for missing data that comes from neighbourhood processing
     where there are no points to process in a given band for a given point.
     The expected behaviour is that the weights are renormalised and the
     result for that point takes 100% of the band with a valid value in.
     """
     data = np.array(
         [
             [[1, 1, 0], [1, 1, 0], [0, 0, 0]],
             [[0, 0, 1], [0, 1, 1], [1, 1, 0]],
             [[0, 0, 0], [0, 0, 0], [0, 0, np.nan]],
         ],
         dtype=np.float32,
     )
     self.cube.data = data
     expected_data = np.array(
         [[1.0, 1.0, 1.0], [1.0, 1.0, 0.75], [1.0, 0.75, 0]],
         dtype=np.float32)
     plugin = ApplyNeighbourhoodProcessingWithAMask(
         "topographic_zone",
         "square",
         2000,
         collapse_weights=self.weights_cube)
     result = plugin.collapse_mask_coord(self.cube)
     assert_allclose(result.data, expected_data)
     self.assertNotIn("topographic_zone", result.coords())
 def test_basic(self):
     """Test we get the expected result with a simple collapse"""
     expected_data = np.array(
         [[1.0, 1.0, 1.0], [1.0, 1.0, 0.75], [1.0, 0.75, 0.5]],
         dtype=np.float32)
     plugin = ApplyNeighbourhoodProcessingWithAMask(
         "topographic_zone", 2000, collapse_weights=self.weights_cube)
     result = plugin.collapse_mask_coord(self.cube)
     assert_allclose(result.data, expected_data)
     self.assertNotIn("topographic_zone", result.coords())
 def test_identical_slices(self):
     """Test that identical successive slices of the cube produce
        identical results."""
     expected = np.array(
         [
             [
                 [1.00, 1.00, 1.00, np.nan, np.nan],
                 [1.00, 1.00, 1.00, np.nan, np.nan],
                 [1.00, 1.00, 1.00, np.nan, np.nan],
                 [1.00, 1.00, 1.00, np.nan, np.nan],
                 [np.nan, np.nan, np.nan, np.nan, np.nan],
             ],
             [
                 [np.nan, 1.00, 1.00, 1.00, 1.00],
                 [np.nan, 0.50, 0.75, 0.75, 1.00],
                 [np.nan, 0.50, 0.75, 0.75, 1.00],
                 [np.nan, 0.00, 0.50, 0.50, 1.00],
                 [np.nan, np.nan, np.nan, np.nan, np.nan],
             ],
             [
                 [np.nan, np.nan, np.nan, np.nan, np.nan],
                 [np.nan, np.nan, np.nan, np.nan, np.nan],
                 [np.nan, np.nan, 1.00, 1.00, 1.00],
                 [np.nan, np.nan, 1.00, 1.00, 1.00],
                 [np.nan, np.nan, 1.00, 1.00, 1.00],
             ],
         ]
     )
     cube = set_up_cube(
         zero_point_indices=((0, 0, 2, 2), (1, 0, 2, 2)),
         num_grid_points=5,
         num_realization_points=2,
     )
     # The neighbourhood code adds bounds to the coordinates if they are
     # not present so add them now to make it easier to compare input and
     # output from the plugin.
     cube.coord("projection_x_coordinate").guess_bounds()
     cube.coord("projection_y_coordinate").guess_bounds()
     cube = iris.util.squeeze(cube)
     coord_for_masking = "topographic_zone"
     radii = 2000
     num_zones = len(self.mask_cube.coord(coord_for_masking).points)
     expected_shape = tuple(
         [cube.data.shape[0], num_zones] + list(cube.data.shape[1:])
     )
     result = ApplyNeighbourhoodProcessingWithAMask(coord_for_masking, radii)(
         cube, self.mask_cube
     )
     self.assertEqual(result.data.shape, expected_shape)
     for realization_slice in result.slices_over("realization"):
         self.assertArrayAlmostEqual(realization_slice.data, expected)
Пример #4
0
 def test_basic(self):
     """Test that the expected result is returned, when the
     topographic_zone coordinate is iterated over."""
     expected = np.array(
         [[[1.00, 1.00, 1.00, np.nan, np.nan],
           [1.00, 1.00, 1.00, np.nan, np.nan],
           [1.00, 1.00, 1.00, np.nan, np.nan],
           [1.00, 1.00, 1.00, np.nan, np.nan],
           [np.nan, np.nan, np.nan, np.nan, np.nan]],
          [[np.nan, 1.00, 1.00, 1.00, 1.00],
           [np.nan, 0.50, 0.75, 0.75, 1.00],
           [np.nan, 0.50, 0.75, 0.75, 1.00],
           [np.nan, 0.00, 0.50, 0.50, 1.00],
           [np.nan, np.nan, np.nan, np.nan, np.nan]],
          [[np.nan, np.nan, np.nan, np.nan, np.nan],
           [np.nan, np.nan, np.nan, np.nan, np.nan],
           [np.nan, np.nan, 1.00, 1.00, 1.00],
           [np.nan, np.nan, 1.00, 1.00, 1.00],
           [np.nan, np.nan, 1.00, 1.00, 1.00]]])
     coord_for_masking = "topographic_zone"
     radii = 2000
     num_zones = len(self.mask_cube.coord(coord_for_masking).points)
     expected_shape = tuple(
         [num_zones] + list(self.cube.data.shape))
     result = ApplyNeighbourhoodProcessingWithAMask(
         coord_for_masking, radii).process(self.cube, self.mask_cube)
     self.assertEqual(result.data.shape, expected_shape)
     self.assertArrayAlmostEqual(result.data, expected)
 def test_masked_weights_data(self):
     """Test points where weights are masked.
     Covers the case where sea points may be masked out so they aren't
     neighbourhood processed."""
     self.weights_cube.data[:, 0, 0] = np.nan
     self.weights_cube.data = np.ma.masked_invalid(self.weights_cube.data)
     expected_data = np.array(
         [[np.nan, 1.0, 1.0], [1.0, 1.0, 0.75], [1.0, 0.75, 0.5]],
         dtype=np.float32)
     expected_mask = np.array([[True, False, False], [False, False, False],
                               [False, False, False]])
     plugin = ApplyNeighbourhoodProcessingWithAMask(
         "topographic_zone", 2000, collapse_weights=self.weights_cube)
     result = plugin.collapse_mask_coord(self.cube)
     assert_allclose(result.data.data, expected_data)
     assert_allclose(result.data.mask, expected_mask)
     self.assertNotIn("topographic_zone", result.coords())
 def test_raises_error(self):
     """Test raises an error if re_mask=True when using collapse_weights"""
     message = "re_mask should be set to False when using collapse_weights"
     with self.assertRaisesRegex(ValueError, message):
         ApplyNeighbourhoodProcessingWithAMask(
             "topographic_zone",
             2000,
             collapse_weights=iris.cube.Cube([0]),
             re_mask=True,
         )
Пример #7
0
 def test_basic(self):
     """Test that the __repr__ method returns the expected string."""
     coord_for_masking = "topographic_zone"
     radii = 2000
     result = str(ApplyNeighbourhoodProcessingWithAMask(
         coord_for_masking, radii))
     msg = ("<ApplyNeighbourhoodProcessingWithAMask: coord_for_masking: "
            "topographic_zone, neighbourhood_method: square, radii: 2000, "
            "lead_times: None, weighted_mode: True, "
            "sum_or_fraction: fraction, re_mask: False>")
     self.assertEqual(result, msg)
Пример #8
0
 def test_basic_no_collapse(self):
     """Test process for a cube with 1 threshold and no collapse.
     This test shows the result of neighbourhood processing the same input
     data three times with the three different masks for the different
     topographic zones."""
     plugin = ApplyNeighbourhoodProcessingWithAMask("topographic_zone", 2000)
     result = plugin(self.cube, self.mask_cube)
     assert_allclose(result.data, self.expected_uncollapsed_result, equal_nan=True)
     expected_coords = self.cube.coords()
     expected_coords.insert(1, self.mask_cube.coord("topographic_zone"))
     self.assertEqual(result.coords(), expected_coords)
     self.assertEqual(result.metadata, self.cube.metadata)
 def test_masked_weights_and_missing(self):
     """Test masked weights, and one point has nan in from
     neighbourhood processing.
     Covers the case where sea points may be masked out so they aren't
     neighbourhood processed. The nan point comes from neighbourhood
     processing where it has found no points to process for a given point
     in a given band."""
     self.weights_cube.data[:, 0, 0] = np.nan
     self.weights_cube.data = np.ma.masked_invalid(self.weights_cube.data)
     self.cube.data[2, 2, 2] = np.nan
     expected_data = np.array(
         [[np.nan, 1.0, 1.0], [1.0, 1.0, 0.75], [1.0, 0.75, 0.0]],
         dtype=np.float32)
     expected_mask = np.array([[True, False, False], [False, False, False],
                               [False, False, False]])
     plugin = ApplyNeighbourhoodProcessingWithAMask(
         "topographic_zone", 2000, collapse_weights=self.weights_cube)
     result = plugin.collapse_mask_coord(self.cube)
     assert_allclose(result.data.data, expected_data)
     assert_allclose(result.data.mask, expected_mask)
     self.assertNotIn("topographic_zone", result.coords())
 def test_collapse_multithreshold(self):
     """Test process for a cube with 2 thresholds and collapsing the topographic_zones.
     Same data as test_basic_collapse with an extra point in the leading
     threshold dimension"""
     plugin = ApplyNeighbourhoodProcessingWithAMask(
         "topographic_zone", 2000, collapse_weights=self.weights_cube)
     result = plugin(self.multi_threshold_cube, self.mask_cube)
     expected_result = np.ma.MaskedArray(
         [self.expected_collapsed_result, self.expected_collapsed_result])
     assert_allclose(result.data.data, expected_result.data, equal_nan=True)
     assert_array_equal(result.data.mask, expected_result.mask)
     self.assertEqual(result.coords(), self.multi_threshold_cube.coords())
     self.assertEqual(result.metadata, self.multi_threshold_cube.metadata)
Пример #11
0
 def test_no_collapse_multithreshold(self):
     """Test process for a cube with 2 thresholds and no collapse.
     Same data as test_basic_no_collapse with an extra point in the leading
     threshold dimension"""
     plugin = ApplyNeighbourhoodProcessingWithAMask("topographic_zone", 2000)
     result = plugin(self.multi_threshold_cube, self.mask_cube)
     expected_result = np.concatenate(
         [self.expected_uncollapsed_result, self.expected_uncollapsed_result]
     )
     assert_allclose(result.data, expected_result, equal_nan=True)
     expected_coords = self.multi_threshold_cube.coords()
     expected_coords.insert(1, self.mask_cube.coord("topographic_zone"))
     self.assertEqual(result.coords(), expected_coords)
     self.assertEqual(result.metadata, self.cube.metadata)
    def test_basic_collapse(self):
        """Test process for a cube with 1 threshold and collapsing the topographic_zones.
        This test shows the result of neighbourhood processing the same input
        data three times with the three different masks for the different
        topographic zones, then doing a weighted collapse of the topopgraphic
        band taking into account any missing data."""
        plugin = ApplyNeighbourhoodProcessingWithAMask(
            "topographic_zone", 2000, collapse_weights=self.weights_cube)
        result = plugin(self.cube, self.mask_cube)

        assert_allclose(result.data.data,
                        self.expected_collapsed_result.data,
                        equal_nan=True)
        assert_array_equal(result.data.mask,
                           self.expected_collapsed_result.mask)
        self.assertEqual(result.coords(), self.cube.coords())
        self.assertEqual(result.metadata, self.cube.metadata)
 def test_basic_no_collapse_circular(self):
     """Test process for a cube with 1 threshold and no collapse.
     This test shows the result of neighbourhood processing the same input
     data three times with the three different masks for the different
     topographic zones."""
     plugin = ApplyNeighbourhoodProcessingWithAMask("topographic_zone",
                                                    "circular", 2000)
     expected_data = np.array([
         [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, np.nan]],
         [[np.nan, 1.0, 0.75], [0.0, 0.0, 0.33333334], [0.0, 0.0, 0.0]],
         [[np.nan, np.nan, np.nan], [np.nan, np.nan, 0.0],
          [np.nan, 0.0, 0.0]],
     ])
     result = plugin(self.cube, self.mask_cube)
     assert_allclose(result.data, expected_data, equal_nan=True)
     expected_coords = self.cube.coords()
     expected_coords.insert(0, self.mask_cube.coord("topographic_zone"))
     self.assertEqual(result.coords(), expected_coords)
     self.assertEqual(result.metadata, self.cube.metadata)
 def test_basic(self):
     """Test that the expected result is returned, when the
     topographic_zone coordinate is iterated over."""
     expected = np.array([[[[1.00, 1.00, 1.00, 0.00, 0.00],
                            [1.00, 1.00, 1.00, 0.00, 0.00],
                            [1.00, 1.00, 1.00, 0.00, 0.00],
                            [1.00, 1.00, 1.00, 0.00, 0.00],
                            [0.00, 0.00, 0.00, 0.00, 0.00]]],
                          [[[0.00, 1.00, 1.00, 1.00, 1.00],
                            [0.00, 0.50, 0.75, 0.75, 1.00],
                            [0.00, 0.50, 0.75, 0.75, 1.00],
                            [0.00, 0.00, 0.50, 0.50, 1.00],
                            [0.00, 0.00, 0.00, 0.00, 0.00]]],
                          [[[0.00, 0.00, 0.00, 0.00, 0.00],
                            [0.00, 0.00, 0.00, 0.00, 0.00],
                            [0.00, 0.00, 1.00, 1.00, 1.00],
                            [0.00, 0.00, 1.00, 1.00, 1.00],
                            [0.00, 0.00, 1.00, 1.00, 1.00]]]])
     coord_for_masking = "topographic_zone"
     radii = 2000
     result = ApplyNeighbourhoodProcessingWithAMask(
         coord_for_masking, radii).process(self.cube, self.mask_cube)
     self.assertArrayAlmostEqual(result.data, expected)
Пример #15
0
 def test_preserve_dimensions_input(self):
     """Test that the dimensions on the output cube are the same as the
        input cube, apart from the additional topographic zone coordinate.
     """
     self.cube.remove_coord("realization")
     cube = add_dimensions_to_cube(
         self.cube, OrderedDict([("threshold", 3), ("realization", 4)]))
     coord_for_masking = "topographic_zone"
     radii = 2000
     result = ApplyNeighbourhoodProcessingWithAMask(
         coord_for_masking, radii).process(cube, self.mask_cube)
     expected_dims = list(cube.dim_coords)
     expected_dims.insert(2, self.mask_cube.coord("topographic_zone"))
     self.assertEqual(result.dim_coords, tuple(expected_dims))
     self.assertEqual(result.coord_dims("realization"), (0,))
     self.assertEqual(result.coord_dims("threshold"), (1,))
     self.assertEqual(result.coord_dims("topographic_zone"), (2,))
     self.assertEqual(result.coord_dims("projection_y_coordinate"), (3,))
     self.assertEqual(result.coord_dims("projection_x_coordinate"), (4,))
Пример #16
0
    def test_preserve_dimensions_with_single_point(self):
        """Test that the dimensions on the output cube are the same as the
           input cube, apart from the collapsed dimension.
           Check that a dimension coordinate with a single point is preserved
           and not demoted to a scalar coordinate."""
        self.cube.remove_coord("realization")
        cube = add_dimensions_to_cube(self.cube,
                                      {"threshold": 4, "realization": 1})
        coord_for_masking = "topographic_zone"
        radii = 2000
        result = ApplyNeighbourhoodProcessingWithAMask(
            coord_for_masking, radii).process(cube, self.mask_cube)
        expected_dims = list(cube.dim_coords)
        expected_dims.insert(2, self.mask_cube.coord("topographic_zone"))

        self.assertEqual(result.dim_coords, tuple(expected_dims))
        self.assertEqual(result.coord_dims("realization"), (0,))
        self.assertEqual(result.coord_dims("threshold"), (1,))
        self.assertEqual(result.coord_dims("topographic_zone"), (2,))
        self.assertEqual(result.coord_dims("projection_y_coordinate"), (3,))
        self.assertEqual(result.coord_dims("projection_x_coordinate"), (4,))
Пример #17
0
def process(cube: cli.inputcube,
            mask: cli.inputcube,
            weights: cli.inputcube = None,
            *,
            coord_for_masking,
            radii: cli.comma_separated_list,
            lead_times: cli.comma_separated_list = None,
            area_sum=False,
            remask=False,
            collapse_dimension=False):
    """Runs neighbourhooding processing iterating over a coordinate by mask.

    Apply the requested neighbourhood method via the
    ApplyNeighbourhoodProcessingWithMask plugin to a file with one diagnostic
    dataset in combination with a cube containing one or more masks.
    The mask dataset may have an extra dimension compared to the input
    diagnostic. In this case, the user specifies the name of the extra
    coordinate and this coordinate is iterated over so each mask is applied
    to separate slices over the input cube. These intermediate masked datasets
    are then concatenated, resulting in a dataset that has been processed
    using multiple masks and has gained an extra dimension from the masking.
    There is also an option to re-mask the output dataset, so that after
    neighbourhood processing non-zero values are only present for unmasked
    grid points.
    There is an alternative option of collapsing the dimension that we gain
    using this processing using a weighted average.

    Args:
        cube (iris.cube.Cube):
            Cube to be processed.
        mask (iris.cube.Cube):
            Cube to act as a mask.
        weights (iris.cube.Cube, Optional):
            Cube containing the weights which are used for collapsing the
            dimension gained through masking. (Optional).
        coord_for_masking (str):
            String matching the name of the coordinate that will be used
            for masking.
        radii (list of float):
            The radius or a list of radii in metres of the neighbourhood to
            apply.
            If it is a list, it must be the same length as lead_times, which
            defines at which lead time to use which nbhood radius. The radius
            will be interpolated for intermediate lead times.
        lead_times (list of int):
            The lead times in hours that correspond to the radii to be used.
            If lead_times are set, radii must be a list the same length as
            lead_times. Lead times must be given as integer values.
        area_sum (bool):
            Return sum rather than fraction over the neighbourhood area.
        remask (bool):
            Include this option to apply the original un-neighbourhood
            processed mask to the neighbourhood processed cube.
            Otherwise the original un-neighbourhood processed mask
            is not applied. Therefore, the neighbourhood processing may result
            in values being present in area that were originally masked.
        collapse_dimension (bool):
            Include this option to collapse the dimension from the mask, by
            doing a weighted mean using the weights provided. This is only
            suitable when the result is left unmasked, so there is data to
            weight between the points in the coordinate we are collapsing.

    Returns:
        iris.cube.Cube:
            A cube after being fully processed.
    """
    from improver.nbhood import radius_by_lead_time
    from improver.nbhood.use_nbhood import (
        ApplyNeighbourhoodProcessingWithAMask,
        CollapseMaskedNeighbourhoodCoordinate,
    )

    sum_or_fraction = 'sum' if area_sum else 'fraction'

    radius_or_radii, lead_times = radius_by_lead_time(radii, lead_times)

    result = ApplyNeighbourhoodProcessingWithAMask(
        coord_for_masking,
        radius_or_radii,
        lead_times=lead_times,
        sum_or_fraction=sum_or_fraction,
        re_mask=remask)(cube, mask)

    # Collapse with the masking dimension.
    if collapse_dimension:
        result = CollapseMaskedNeighbourhoodCoordinate(coord_for_masking,
                                                       weights)(result)
    return result
    def test_collapse_preserve_dimensions_input(self):
        """Test that the dimensions on the output cube are the same as the
           input cube, apart from the additional topographic zone coordinate.
        """
        self.cube.remove_coord("realization")
        cube = add_dimensions_to_cube(
            self.cube, OrderedDict([("threshold", 3), ("realization", 4)])
        )
        coord_for_masking = "topographic_zone"
        radii = 2000
        uncollapsed_result = None
        mask_data = np.array(
            [
                [
                    [1, 1, 1, 1, 1],
                    [1, 1, 0, 0, 0],
                    [1, 1, 0, 0, 0],
                    [0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0],
                ],
                [
                    [0, 0, 0, 0, 1],
                    [0, 0, 1, 1, 1],
                    [0, 0, 1, 1, 1],
                    [1, 1, 0, 1, 0],
                    [1, 1, 0, 0, 0],
                ],
                [
                    [0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0],
                    [0, 0, 0, 1, 1],
                    [0, 0, 1, 1, 1],
                    [0, 0, 1, 1, 1],
                ],
            ]
        )
        mask_cube = self.mask_cube.copy(mask_data)

        for collapse_weights in [None, self.weights_cube]:
            coords_order = [
                "realization",
                "threshold",
                "topographic_zone",
                "projection_y_coordinate",
                "projection_x_coordinate",
            ]
            result = ApplyNeighbourhoodProcessingWithAMask(
                coord_for_masking, radii, collapse_weights=collapse_weights,
            )(cube, mask_cube)
            expected_dims = list(cube.dim_coords)
            if collapse_weights is None:
                uncollapsed_result = result
                expected_dims.insert(2, self.mask_cube.coord("topographic_zone"))
            else:
                coords_order.remove("topographic_zone")
                collapsed_result = CollapseMaskedNeighbourhoodCoordinate(
                    coord_for_masking, collapse_weights
                )(uncollapsed_result)
                self.assertArrayAlmostEqual(result.data, collapsed_result.data)
            self.assertEqual(result.dim_coords, tuple(expected_dims))
            for dim, coord in enumerate(coords_order):
                self.assertEqual(result.coord_dims(coord), (dim,))
Пример #19
0
def process(cube: cli.inputcube,
            mask: cli.inputcube,
            weights: cli.inputcube = None,
            *,
            coord_for_masking, radius: float = None,
            radii_by_lead_time=None,
            sum_or_fraction="fraction",
            remask=False,
            collapse_dimension=False):
    """Runs neighbourhooding processing iterating over a coordinate by mask.

    Apply the requested neighbourhood method via the
    ApplyNeighbourhoodProcessingWithMask plugin to a file with one diagnostic
    dataset in combination with a cube containing one or more masks.
    The mask dataset may have an extra dimension compared to the input
    diagnostic. In this case, the user specifies the name of the extra
    coordinate and this coordinate is iterated over so each mask is applied
    to separate slices over the input cube. These intermediate masked datasets
    are then concatenated, resulting in a dataset that has been processed
    using multiple masks and has gained an extra dimension from the masking.
    There is also an option to re-mask the output dataset, so that after
    neighbourhood processing non-zero values are only present for unmasked
    grid points.
    There is an alternative option of collapsing the dimension that we gain
    using this processing using a weighted average.

    Args:
        cube (iris.cube.Cube):
            Cube to be processed.
        mask (iris.cube.Cube):
            Cube to act as a mask.
        weights (iris.cube.Cube, Optional):
            Cube containing the weights which are used for collapsing the
            dimension gained through masking.
        coord_for_masking (str):
            String matching the name of the coordinate that will be used
            for masking.
        radius (float):
            The radius in metres of the neighbourhood to apply.
            Rounded up to convert into integer number of grid points east and
            north, based on the characteristic spacing at the zero indices of
            the cube projection-x and y coordinates.
        radii_by_lead_time (float or list of float):
            A list with the radius in metres at [0] and the lead_time at [1]
            Lead time is a List of lead times or forecast periods, at which
            the radii within 'radii' are defined. The lead times are expected
            in hours.
        sum_or_fraction (str):
            Identifier for whether sum or fraction should be returned from
            neighbourhooding.
            Sum represents the sum of the neighbourhood.
            Fraction represents the sum of the neighbourhood divided by the
            neighbourhood area.
        remask (bool):
            If True, the original un-neighbourhood processed mask
            is applied to mask out the neighbourhood processed cube.
            If False, the original un-neighbourhood processed mask is not
            applied.
            Therefore, the neighbourhood processing may result in
            values being present in areas that were originally masked.
        collapse_dimension (bool):
            Collapse the dimension from the mask, by doing a weighted mean
            using the weights provided.  This is only suitable when the result
            is left unmasked, so there is data to weight between the points
            in the coordinate we are collapsing.

    Returns:
        (tuple): tuple containing:
            **result** (iris.cube.Cube):
                A cube after being fully processed.
            **intermediate_cube** (iris.cube.Cube):
                A cube before it is collapsed, if 'collapse_dimension' is True.

    """
    from improver.nbhood.use_nbhood import (
        ApplyNeighbourhoodProcessingWithAMask,
        CollapseMaskedNeighbourhoodCoordinate,
    )
    from improver.utilities.cli_utilities import radius_or_radii_and_lead

    radius_or_radii, lead_times = radius_or_radii_and_lead(radius,
                                                           radii_by_lead_time)

    result = ApplyNeighbourhoodProcessingWithAMask(
        coord_for_masking, radius_or_radii, lead_times=lead_times,
        sum_or_fraction=sum_or_fraction,
        re_mask=remask).process(cube, mask)
    intermediate_cube = None

    # Collapse with the masking dimension.
    if collapse_dimension:
        intermediate_cube = result.copy()
        result = CollapseMaskedNeighbourhoodCoordinate(
            coord_for_masking, weights).process(result)
    return result, intermediate_cube
def main(argv=None):
    """Load in arguments for applying neighbourhood processing when using a
    mask."""
    parser = ArgParser(
        description='Neighbourhood the input dataset over two distinct regions'
        ' of land and sea. If performed as a single level neighbourhood, a '
        'land-sea mask should be provided. If instead topographic_zone '
        'neighbourhooding is being employed, the mask should be one of '
        'topographic zones. In the latter case a weights array is also needed'
        ' to collapse the topographic_zone coordinate. These weights are '
        'created with the improver generate-topography-bands-weights CLI and '
        'should be made using a land-sea mask, which will then be employed '
        'within this code to draw the distinction between the two surface '
        'types.')

    parser.add_argument('input_filepath',
                        metavar='INPUT_FILE',
                        help='A path to an input NetCDF file to be processed.')
    parser.add_argument('input_mask_filepath',
                        metavar='INPUT_MASK',
                        help=('A path to an input NetCDF file containing '
                              'either a mask of topographic zones over land '
                              'or a land-sea mask.'))
    parser.add_argument('output_filepath',
                        metavar='OUTPUT_FILE',
                        help='The output path for the processed NetCDF.')

    mask_group = parser.add_argument_group(
        'Collapse weights - required if using a topographic zones mask')
    mask_group.add_argument('--weights_for_collapsing_dim',
                            metavar='WEIGHTS',
                            default=None,
                            help='A path to an weights NetCDF file containing '
                            'the weights which are used for collapsing the '
                            'dimension gained through masking. These weights '
                            'must have been created using a land-sea mask.')

    radius_group = parser.add_argument_group(
        'Neighbourhooding Radius - Set only one of the options')
    group = radius_group.add_mutually_exclusive_group()
    group.add_argument('--radius',
                       metavar='RADIUS',
                       type=float,
                       help='The radius (in m) for neighbourhood processing.')
    group.add_argument('--radii-by-lead-time',
                       metavar=('RADII_BY_LEAD_TIME', 'LEAD_TIME_IN_HOURS'),
                       nargs=2,
                       help='The radii for neighbourhood processing '
                       'and the associated lead times at which the radii are '
                       'valid. The radii are in metres whilst the lead time '
                       'has units of hours. The radii and lead times are '
                       'expected as individual comma-separated lists with '
                       'the list of radii given first followed by a list of '
                       'lead times to indicate at what lead time each radii '
                       'should be used. For example: 10000,12000,14000 1,2,3 '
                       'where a lead time of 1 hour uses a radius of 10000m, '
                       'a lead time of 2 hours uses a radius of 12000m, etc.')
    parser.add_argument('--sum_or_fraction',
                        default="fraction",
                        choices=["sum", "fraction"],
                        help='The neighbourhood output can either be in the '
                        'form of a sum of the neighbourhood, or a '
                        'fraction calculated by dividing the sum of the '
                        'neighbourhood by the neighbourhood area. '
                        '"fraction" is the default option.')
    parser.add_argument('--intermediate_filepath',
                        default=None,
                        help='Intermediate filepath for results following '
                        'topographic masked neighbourhood processing of '
                        'land points and prior to collapsing the '
                        'topographic_zone coordinate. Intermediate files '
                        'will not be produced if no topographic masked '
                        'neighbourhood processing occurs.')

    args = parser.parse_args(args=argv)

    cube = load_cube(args.input_filepath)
    mask = load_cube(args.input_mask_filepath, no_lazy_load=True)
    masking_coordinate = None

    if any([
            'topographic_zone' in coord.name()
            for coord in mask.coords(dim_coords=True)
    ]):

        if mask.attributes['topographic_zones_include_seapoints'] == 'True':
            raise ValueError('The topographic zones mask cube must have been '
                             'masked to exclude sea points, but '
                             'topographic_zones_include_seapoints = True')

        if not args.weights_for_collapsing_dim:
            raise IOError('A weights cube must be provided if using a mask '
                          'of topographic zones to collapse the resulting '
                          'vertical dimension.')

        weights = load_cube(args.weights_for_collapsing_dim, no_lazy_load=True)
        if weights.attributes['topographic_zones_include_seapoints'] == 'True':
            raise ValueError('The weights cube must be masked to exclude sea '
                             'points, but topographic_zones_include_seapoints '
                             '= True')

        masking_coordinate = 'topographic_zone'
        landmask = weights[0].copy(data=weights[0].data.mask)
        landmask.rename('land_binary_mask')
        landmask.remove_coord(masking_coordinate)
        # Create land and sea masks in IMPROVER format (inverse of
        # numpy standard) 1 - include this region, 0 - exclude this region.
        land_only = landmask.copy(
            data=np.logical_not(landmask.data).astype(int))
        sea_only = landmask.copy(data=landmask.data.astype(int))

    else:
        if args.weights_for_collapsing_dim:
            warnings.warn('A weights cube has been provided but will not be '
                          'used as there is no topographic zone coordinate '
                          'to collapse.')
        landmask = mask
        # In this case the land is set to 1 and the sea is set to 0 in the
        # input mask.
        sea_only = landmask.copy(
            data=np.logical_not(landmask.data).astype(int))
        land_only = landmask.copy(data=landmask.data.astype(int))

    if args.radius:
        radius_or_radii = args.radius
        lead_times = None
    elif args.radii_by_lead_time:
        radius_or_radii = args.radii_by_lead_time[0].split(",")
        lead_times = args.radii_by_lead_time[1].split(",")

    if args.intermediate_filepath is not None and masking_coordinate is None:
        msg = ('No topographic_zone coordinate found, so no intermediate file '
               'will be saved.')
        warnings.warn(msg)

    # Section for neighbourhood processing land points.
    if land_only.data.max() > 0.0:
        if masking_coordinate is not None:
            result_land = ApplyNeighbourhoodProcessingWithAMask(
                masking_coordinate,
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=args.sum_or_fraction,
                re_mask=False).process(cube, mask)
        else:
            result_land = NeighbourhoodProcessing(
                'square',
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=args.sum_or_fraction,
                re_mask=True).process(cube, land_only)

        if masking_coordinate is not None:
            if args.intermediate_filepath is not None:
                save_netcdf(result_land, args.intermediate_filepath)
            # Collapse the masking coordinate.
            result_land = CollapseMaskedNeighbourhoodCoordinate(
                masking_coordinate, weights=weights).process(result_land)

        result = result_land

    # Section for neighbourhood processing sea points.
    if sea_only.data.max() > 0.0:
        result_sea = NeighbourhoodProcessing(
            'square',
            radius_or_radii,
            lead_times=lead_times,
            sum_or_fraction=args.sum_or_fraction,
            re_mask=True).process(cube, sea_only)

        result = result_sea

    # Section for combining land and sea points following land and sea points
    # being neighbourhood processed individually.
    if sea_only.data.max() > 0.0 and land_only.data.max() > 0.0:
        # Recombine cubes to be a single output.
        combined_data = result_land.data.filled(0) + result_sea.data.filled(0)
        result = result_land.copy(data=combined_data)

    save_netcdf(result, args.output_filepath)
Пример #21
0
def main(argv=None):
    """Load in arguments for applying neighbourhood processing when using a
    mask."""
    parser = ArgParser(
        description='Apply the requested neighbourhood method via the '
        'ApplyNeighbourhoodProcessingWithAMask plugin to a file '
        'with one diagnostic dataset in combination with a file '
        'containing one or more masks. The mask dataset may have '
        'an extra dimension compared to the input diagnostic. '
        'In this case, the user specifies the name of '
        'the extra coordinate and this coordinate is iterated '
        'over so each mask is applied to separate slices over the'
        ' input data. These intermediate masked datasets are then'
        ' concatenated, resulting in a dataset that has been '
        ' processed using multiple masks and has gained an extra '
        'dimension from the masking.  There is also an option to '
        're-mask the output dataset, so that after '
        'neighbourhood processing, non-zero values are only '
        'present for unmasked grid points. '
        'There is an alternative option of collapsing the '
        'dimension that we gain using this processing using a '
        'weighted average.')
    parser.add_argument('coord_for_masking',
                        metavar='COORD_FOR_MASKING',
                        help='Coordinate to iterate over when applying a mask '
                        'to the neighbourhood processing. ')
    parser.add_argument('input_filepath',
                        metavar='INPUT_FILE',
                        help='A path to an input NetCDF file to be processed.')
    parser.add_argument('input_mask_filepath',
                        metavar='INPUT_MASK_FILE',
                        help='A path to an input mask NetCDF file to be '
                        'used to mask the input file.')
    parser.add_argument('output_filepath',
                        metavar='OUTPUT_FILE',
                        help='The output path for the processed NetCDF.')
    group = parser.add_mutually_exclusive_group()
    group.add_argument('--radius',
                       metavar='RADIUS',
                       type=float,
                       help='The radius (in m) for neighbourhood processing.')
    group.add_argument('--radii-by-lead-time',
                       metavar=('RADII_BY_LEAD_TIME', 'LEAD_TIME_IN_HOURS'),
                       nargs=2,
                       help='The radii for neighbourhood processing '
                       'and the associated lead times at which the radii are '
                       'valid. The radii are in metres whilst the lead time '
                       'has units of hours. The radii and lead times are '
                       'expected as individual comma-separated lists with '
                       'the list of radii given first followed by a list of '
                       'lead times to indicate at what lead time each radii '
                       'should be used. For example: 10000,12000,14000 1,2,3 '
                       'where a lead time of 1 hour uses a radius of 10000m, '
                       'a lead time of 2 hours uses a radius of 12000m, etc.')
    parser.add_argument('--sum_or_fraction',
                        default="fraction",
                        choices=["sum", "fraction"],
                        help='The neighbourhood output can either be in the '
                        'form of a sum of the neighbourhood, or a '
                        'fraction calculated by dividing the sum of the '
                        'neighbourhood by the neighbourhood area. '
                        '"fraction" is the default option.')
    group2 = parser.add_mutually_exclusive_group()
    group2.add_argument('--re_mask',
                        action='store_true',
                        help='If re_mask is set (i.e. True), the output data '
                        'following neighbourhood processing is '
                        're-masked. This re-masking removes any values '
                        'that have been generated by neighbourhood '
                        'processing at grid points that were '
                        'originally masked. '
                        'If not set, re_mask defaults to False and no '
                        're-masking is applied to the neighbourhood '
                        'processed output. Therefore, the neighbourhood '
                        'processing may result in values being present '
                        'in areas that were originally masked. This '
                        'allows the the values in adjacent bands to be'
                        'weighted together if the additional dimension'
                        'from the masking process is collapsed.')
    group2.add_argument('--collapse_dimension',
                        action='store_true',
                        help='Collapse the dimension from the mask, by doing '
                        'a weighted mean using the weights provided. '
                        'This is only suitable when the result is is '
                        'left unmasked, so there is data to weight '
                        'between the points in coordinate we are '
                        'collapsing.')
    parser.add_argument('--weights_for_collapsing_dim',
                        metavar='WEIGHTS',
                        default=None,
                        help='A path to an weights NetCDF file containing the '
                        'weights which are used for collapsing the '
                        'dimension gained through masking.')
    parser.add_argument('--intermediate_filepath',
                        default=None,
                        help='If provided the result after neighbourhooding, '
                        'before collapsing the extra dimension is saved '
                        'in the given filepath.')

    args = parser.parse_args(args=argv)

    cube = load_cube(args.input_filepath)
    mask_cube = load_cube(args.input_mask_filepath)

    if args.radius:
        radius_or_radii = args.radius
        lead_times = None
    elif args.radii_by_lead_time:
        radius_or_radii = args.radii_by_lead_time[0].split(",")
        lead_times = args.radii_by_lead_time[1].split(",")

    result = ApplyNeighbourhoodProcessingWithAMask(
        args.coord_for_masking,
        radius_or_radii,
        lead_times=lead_times,
        sum_or_fraction=args.sum_or_fraction,
        re_mask=args.re_mask).process(cube, mask_cube)

    if args.intermediate_filepath is not None:
        save_netcdf(result, args.intermediate_filepath)
    # Collapse with the masking dimension.
    if args.collapse_dimension:
        weights = load_cube(args.weights_for_collapsing_dim)
        result = CollapseMaskedNeighbourhoodCoordinate(
            args.coord_for_masking, weights=weights).process(result)
    save_netcdf(result, args.output_filepath)
Пример #22
0
def process(cube,
            mask,
            radius=None,
            radii_by_lead_time=None,
            weights=None,
            sum_or_fraction="fraction",
            return_intermediate=False):
    """ Module to process land and sea separately before combining them.

    Neighbourhood the input dataset over two distinct regions of land and sea.
    If performed as a single level neighbourhood, a land-sea mask should be
    provided. If instead topographic_zone neighbourhooding is being employed,
    the mask should be one of topographic zones. In the latter case a weights
    array is also needed to collapse the topographic_zone coordinate. These
    weights are created with the improver generate-topography-bands-weights
    CLI and should be made using a land-sea mask, which will then be employed
    within this code to draw the distinction between the two surface types.

    Args:
        cube (iris.cube.Cube):
            A cube to be processed.
        mask (iris.cube.Cube):
            A cube containing either a mask of topographic zones over land or
            a land-sea mask.
        radius (float):
            The radius in metres of the neighbourhood to apply.
            Rounded up to convert into integer number of grid points east and
            north, based on the characteristic spacing at the zero indices of
            the cube projection-x and y coordinates.
            Default is None.
        radii_by_lead_time (list):
            A list with the radius in metres at [0] and the lead_time at [1]
            Lead time is a List of lead times or forecast periods, at which
            the radii within 'radii' are defined. The lead times are expected
            in hours.
            Default is None
        weights (iris.cube.Cube):
            A cube containing the weights which are used for collapsing the
            dimension gained through masking. These weights must have been
            created using a land-sea mask.
            Default is None.
        sum_or_fraction (str):
            The neighbourhood output can either be in the form of a sum of the
            neighbourhood, or a fraction calculated by dividing the sum of the
            neighbourhood by the neighbourhood area.
            Default is 'fraction'
        return_intermediate (bool):
            If True will return a cube with results following topographic
            masked neighbourhood processing of land points and prior to
            collapsing the topographic_zone coordinate. If no topographic
            masked neighbourhooding occurs, there will be no intermediate cube
            and a warning.
            Default is False.

    Returns:
        (tuple): tuple containing:
            **result** (iris.cube.Cube):
                A cube of the processed data.
            **intermediate_cube** (iris.cube.Cube or None):
                A cube of the intermediate data, before collapsing.

    Raises:
        ValueError:
            If the topographic zone mask has the attribute
            topographic_zones_include_seapoints.
        IOError:
            if a weights cube isn't given and a topographic_zone mask is given.
        ValueError:
            If the weights cube has the attribute
            topographic_zones_include_seapoints.

    Warns:
        warning:
            A weights cube has been provided but no topographic zone.

    """
    masking_coordinate = intermediate_cube = None
    if any([
            'topographic_zone' in coord.name()
            for coord in mask.coords(dim_coords=True)
    ]):

        if mask.attributes['topographic_zones_include_seapoints'] == 'True':
            raise ValueError('The topographic zones mask cube must have been '
                             'masked to exclude sea points, but '
                             'topographic_zones_include_seapoints = True')

        if not weights:
            raise TypeError('A weights cube must be provided if using a mask '
                            'of topographic zones to collapse the resulting '
                            'vertical dimension.')

        if weights.attributes['topographic_zones_include_seapoints'] == 'True':
            raise ValueError('The weights cube must be masked to exclude sea '
                             'points, but topographic_zones_include_seapoints '
                             '= True')

        masking_coordinate = 'topographic_zone'
        landmask = weights[0].copy(data=weights[0].data.mask)
        landmask.rename('land_binary_mask')
        landmask.remove_coord(masking_coordinate)
        # Create land and sea masks in IMPROVER format (inverse of
        # numpy standard) 1 - include this region, 0 - exclude this region.
        land_only = landmask.copy(
            data=np.logical_not(landmask.data).astype(int))
        sea_only = landmask.copy(data=landmask.data.astype(int))

    else:
        if weights is None:
            warnings.warn('A weights cube has been provided but will not be '
                          'used as there is no topographic zone coordinate '
                          'to collapse.')
        landmask = mask
        # In this case the land is set to 1 and the sea is set to 0 in the
        # input mask.
        sea_only = landmask.copy(
            data=np.logical_not(landmask.data).astype(int))
        land_only = landmask.copy(data=landmask.data.astype(int))

    radius_or_radii, lead_times = radius_or_radii_and_lead(
        radius, radii_by_lead_time)

    if return_intermediate is not None and masking_coordinate is None:
        warnings.warn('No topographic_zone coordinate found, so no '
                      'intermediate file will be saved.')

    # Section for neighbourhood processing land points.
    if land_only.data.max() > 0.0:
        if masking_coordinate is None:
            result_land = NeighbourhoodProcessing(
                'square',
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=sum_or_fraction,
                re_mask=True).process(cube, land_only)
        else:
            result_land = ApplyNeighbourhoodProcessingWithAMask(
                masking_coordinate,
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=sum_or_fraction,
                re_mask=False).process(cube, mask)

            if return_intermediate:
                intermediate_cube = result_land.copy()
            # Collapse the masking coordinate.
            result_land = CollapseMaskedNeighbourhoodCoordinate(
                masking_coordinate, weights=weights).process(result_land)
        result = result_land

    # Section for neighbourhood processing sea points.
    if sea_only.data.max() > 0.0:
        result_sea = NeighbourhoodProcessing('square',
                                             radius_or_radii,
                                             lead_times=lead_times,
                                             sum_or_fraction=sum_or_fraction,
                                             re_mask=True).process(
                                                 cube, sea_only)
        result = result_sea

    # Section for combining land and sea points following land and sea points
    # being neighbourhood processed individually.
    if sea_only.data.max() > 0.0 and land_only.data.max() > 0.0:
        # Recombine cubes to be a single output.
        combined_data = result_land.data.filled(0) + result_sea.data.filled(0)
        result = result_land.copy(data=combined_data)

    return result, intermediate_cube
Пример #23
0
def process(cube: cli.inputcube,
            mask: cli.inputcube,
            weights: cli.inputcube = None,
            *,
            radii: cli.comma_separated_list,
            lead_times: cli.comma_separated_list = None,
            area_sum=False,
            return_intermediate=False):
    """ Module to process land and sea separately before combining them.

    Neighbourhood the input dataset over two distinct regions of land and sea.
    If performed as a single level neighbourhood, a land-sea mask should be
    provided. If instead topographic_zone neighbourhooding is being employed,
    the mask should be one of topographic zones. In the latter case a weights
    array is also needed to collapse the topographic_zone coordinate. These
    weights are created with the improver generate-topography-bands-weights
    CLI and should be made using a land-sea mask, which will then be employed
    within this code to draw the distinction between the two surface types.

    Args:
        cube (iris.cube.Cube):
            A cube to be processed.
        mask (iris.cube.Cube):
            A cube containing either a mask of topographic zones over land or
            a land-sea mask.
        weights (iris.cube.Cube):
            A cube containing the weights which are used for collapsing the
            dimension gained through masking. These weights must have been
            created using a land-sea mask. (Optional).
        radii (list of float):
            The radius or a list of radii in metres of the neighbourhood to
            apply.
            If it is a list, it must be the same length as lead_times, which
            defines at which lead time to use which nbhood radius. The radius
            will be interpolated for intermediate lead times.
        lead_times (list of int):
            The lead times in hours that correspond to the radii to be used.
            If lead_times are set, radii must be a list the same length as
            lead_times. Lead times must be given as integer values.
        area_sum (bool):
            Return sum rather than fraction over the neighbourhood area.
        return_intermediate (bool):
            Include this option to return a cube with results following
            topographic masked neighbourhood processing of land points and
            prior to collapsing the topographic_zone coordinate. If no
            topographic masked neighbourhooding occurs, there will be no
            intermediate cube and a warning.

    Returns:
        (tuple): tuple containing:
            **result** (iris.cube.Cube):
                A cube of the processed data.
            **intermediate_cube** (iris.cube.Cube):
                A cube of the intermediate data, before collapsing.

    Raises:
        ValueError:
            If the topographic zone mask has the attribute
            topographic_zones_include_seapoints.
        IOError:
            if a weights cube isn't given and a topographic_zone mask is given.
        ValueError:
            If the weights cube has the attribute
            topographic_zones_include_seapoints.
        RuntimeError:
            If lead times are not None and has a different length to radii.
        TypeError:
            A weights cube has been provided but no topographic zone.

    """
    import warnings

    import numpy as np

    from improver.nbhood.nbhood import NeighbourhoodProcessing
    from improver.nbhood.use_nbhood import (
        ApplyNeighbourhoodProcessingWithAMask,
        CollapseMaskedNeighbourhoodCoordinate)

    sum_or_fraction = 'sum' if area_sum else 'fraction'

    masking_coordinate = intermediate_cube = None
    if any([
            'topographic_zone' in coord.name()
            for coord in mask.coords(dim_coords=True)
    ]):

        if mask.attributes['topographic_zones_include_seapoints'] == 'True':
            raise ValueError('The topographic zones mask cube must have been '
                             'masked to exclude sea points, but '
                             'topographic_zones_include_seapoints = True')

        if not weights:
            raise TypeError('A weights cube must be provided if using a mask '
                            'of topographic zones to collapse the resulting '
                            'vertical dimension.')

        if weights.attributes['topographic_zones_include_seapoints'] == 'True':
            raise ValueError('The weights cube must be masked to exclude sea '
                             'points, but topographic_zones_include_seapoints '
                             '= True')

        masking_coordinate = 'topographic_zone'
        land_sea_mask = weights[0].copy(data=weights[0].data.mask)
        land_sea_mask.rename('land_binary_mask')
        land_sea_mask.remove_coord(masking_coordinate)
        # Create land and sea masks in IMPROVER format (inverse of
        # numpy standard) 1 - include this region, 0 - exclude this region.
        land_only = land_sea_mask.copy(
            data=np.logical_not(land_sea_mask.data).astype(int))
        sea_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int))

    else:
        if weights is not None:
            raise TypeError('A weights cube has been provided but will not be '
                            'used')
        land_sea_mask = mask
        # In this case the land is set to 1 and the sea is set to 0 in the
        # input mask.
        sea_only = land_sea_mask.copy(
            data=np.logical_not(land_sea_mask.data).astype(int))
        land_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int))

    if lead_times is None:
        radius_or_radii = float(radii[0])
    else:
        if len(radii) != len(lead_times):
            raise RuntimeError("If leadtimes are supplied, it must be a list"
                               " of equal length to a list of radii.")
        radius_or_radii = [float(x) for x in radii]
        lead_times = [int(x) for x in lead_times]

    if return_intermediate is not None and masking_coordinate is None:
        warnings.warn('No topographic_zone coordinate found, so no '
                      'intermediate file will be saved.')

    # Section for neighbourhood processing land points.
    if land_only.data.max() > 0.0:
        if masking_coordinate is None:
            result_land = NeighbourhoodProcessing(
                'square',
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=sum_or_fraction,
                re_mask=True).process(cube, land_only)
        else:
            result_land = ApplyNeighbourhoodProcessingWithAMask(
                masking_coordinate,
                radius_or_radii,
                lead_times=lead_times,
                sum_or_fraction=sum_or_fraction,
                re_mask=False).process(cube, mask)

            if return_intermediate:
                intermediate_cube = result_land.copy()
            # Collapse the masking coordinate.
            result_land = CollapseMaskedNeighbourhoodCoordinate(
                masking_coordinate, weights=weights).process(result_land)
        result = result_land

    # Section for neighbourhood processing sea points.
    if sea_only.data.max() > 0.0:
        result_sea = NeighbourhoodProcessing('square',
                                             radius_or_radii,
                                             lead_times=lead_times,
                                             sum_or_fraction=sum_or_fraction,
                                             re_mask=True).process(
                                                 cube, sea_only)
        result = result_sea

    # Section for combining land and sea points following land and sea points
    # being neighbourhood processed individually.
    if sea_only.data.max() > 0.0 and land_only.data.max() > 0.0:
        # Recombine cubes to be a single output.
        combined_data = result_land.data.filled(0) + result_sea.data.filled(0)
        result = result_land.copy(data=combined_data)

    return result, intermediate_cube
Пример #24
0
def process(
    cube: cli.inputcube,
    mask: cli.inputcube,
    weights: cli.inputcube = None,
    *,
    neighbourhood_shape="square",
    radii: cli.comma_separated_list,
    lead_times: cli.comma_separated_list = None,
    area_sum=False,
):
    """ Module to process land and sea separately before combining them.

    Neighbourhood the input dataset over two distinct regions of land and sea.
    If performed as a single level neighbourhood, a land-sea mask should be
    provided. If instead topographic_zone neighbourhooding is being employed,
    the mask should be one of topographic zones. In the latter case a weights
    array is also needed to collapse the topographic_zone coordinate. These
    weights are created with the improver generate-topography-bands-weights
    CLI and should be made using a land-sea mask, which will then be employed
    within this code to draw the distinction between the two surface types.

    Args:
        cube (iris.cube.Cube):
            A cube to be processed.
        mask (iris.cube.Cube):
            A cube containing either a mask of topographic zones over land or
            a land-sea mask. If this is a land-sea mask, land points should be
            set to one and sea points set to zero.
        weights (iris.cube.Cube):
            A cube containing the weights which are used for collapsing the
            dimension gained through masking. These weights must have been
            created using a land-sea mask. (Optional).
        neighbourhood_shape (str):
            Name of the neighbourhood method to use.
            Options: "circular", "square".
            Default: "square".
        radii (list of float):
            The radius or a list of radii in metres of the neighbourhood to
            apply.
            If it is a list, it must be the same length as lead_times, which
            defines at which lead time to use which nbhood radius. The radius
            will be interpolated for intermediate lead times.
        lead_times (list of int):
            The lead times in hours that correspond to the radii to be used.
            If lead_times are set, radii must be a list the same length as
            lead_times. Lead times must be given as integer values.
        area_sum (bool):
            Return sum rather than fraction over the neighbourhood area.

    Returns:
        (tuple): tuple containing:
            **result** (iris.cube.Cube):
                A cube of the processed data.

    Raises:
        ValueError:
            If the topographic zone mask has the attribute
            topographic_zones_include_seapoints.
        IOError:
            if a weights cube isn't given and a topographic_zone mask is given.
        ValueError:
            If the weights cube has the attribute
            topographic_zones_include_seapoints.
        RuntimeError:
            If lead times are not None and has a different length to radii.
        TypeError:
            A weights cube has been provided but no topographic zone.

    """
    import numpy as np

    from improver.nbhood.nbhood import NeighbourhoodProcessing
    from improver.nbhood.use_nbhood import ApplyNeighbourhoodProcessingWithAMask

    masking_coordinate = None
    if any(
        "topographic_zone" in coord.name() for coord in mask.coords(dim_coords=True)
    ):

        if mask.attributes["topographic_zones_include_seapoints"] == "True":
            raise ValueError(
                "The topographic zones mask cube must have been "
                "masked to exclude sea points, but "
                "topographic_zones_include_seapoints = True"
            )

        if not weights:
            raise TypeError(
                "A weights cube must be provided if using a mask "
                "of topographic zones to collapse the resulting "
                "vertical dimension."
            )

        if weights.attributes["topographic_zones_include_seapoints"] == "True":
            raise ValueError(
                "The weights cube must be masked to exclude sea "
                "points, but topographic_zones_include_seapoints "
                "= True"
            )

        masking_coordinate = "topographic_zone"
        land_sea_mask = weights[0].copy(data=weights[0].data.mask)
        land_sea_mask.rename("land_binary_mask")
        land_sea_mask.remove_coord(masking_coordinate)
        # Create land and sea masks in IMPROVER format (inverse of
        # numpy standard) 1 - include this region, 0 - exclude this region.
        land_only = land_sea_mask.copy(
            data=np.logical_not(land_sea_mask.data).astype(int)
        )
        sea_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int))

    else:
        if weights is not None:
            raise TypeError("A weights cube has been provided but will not be " "used")
        land_sea_mask = mask
        # In this case the land is set to 1 and the sea is set to 0 in the
        # input mask.
        sea_only = land_sea_mask.copy(
            data=np.logical_not(land_sea_mask.data).astype(int)
        )
        land_only = land_sea_mask.copy(data=land_sea_mask.data.astype(int))

    if lead_times is None:
        radius_or_radii = float(radii[0])
    else:
        if len(radii) != len(lead_times):
            raise RuntimeError(
                "If leadtimes are supplied, it must be a list"
                " of equal length to a list of radii."
            )
        radius_or_radii = [float(x) for x in radii]
        lead_times = [int(x) for x in lead_times]

    # Section for neighbourhood processing land points.
    if land_only.data.max() > 0.0:
        if masking_coordinate is None:
            result_land = NeighbourhoodProcessing(
                neighbourhood_shape,
                radius_or_radii,
                lead_times=lead_times,
                sum_only=area_sum,
                re_mask=True,
            )(cube, land_only)
        else:
            result_land = ApplyNeighbourhoodProcessingWithAMask(
                masking_coordinate,
                neighbourhood_shape,
                radius_or_radii,
                lead_times=lead_times,
                collapse_weights=weights,
                sum_only=area_sum,
            )(cube, mask)
        result = result_land

    # Section for neighbourhood processing sea points.
    if sea_only.data.max() > 0.0:
        result_sea = NeighbourhoodProcessing(
            neighbourhood_shape,
            radius_or_radii,
            lead_times=lead_times,
            sum_only=area_sum,
            re_mask=True,
        )(cube, sea_only)
        result = result_sea

    # Section for combining land and sea points following land and sea points
    # being neighbourhood processed individually.
    if sea_only.data.max() > 0.0 and land_only.data.max() > 0.0:
        # Recombine cubes to be a single output.
        combined_data = result_land.data.filled(0) + result_sea.data.filled(0)
        result = result_land.copy(data=combined_data)

    return result
Пример #25
0
def process(
    cube: cli.inputcube,
    mask: cli.inputcube,
    weights: cli.inputcube = None,
    *,
    coord_for_masking,
    neighbourhood_shape="square",
    radii: cli.comma_separated_list,
    lead_times: cli.comma_separated_list = None,
    area_sum=False,
):
    """Runs neighbourhooding processing iterating over a coordinate by mask.

    Apply the requested neighbourhood method via the
    ApplyNeighbourhoodProcessingWithMask plugin to a file with one diagnostic
    dataset in combination with a cube containing one or more masks.
    The mask dataset may have an extra dimension compared to the input
    diagnostic. In this case, the user specifies the name of the extra
    coordinate and this coordinate is iterated over so each mask is applied
    to separate slices over the input cube. These intermediate masked datasets
    are then concatenated, resulting in a dataset that has been processed
    using multiple masks and has gained an extra dimension from the masking.
    If weights are given the masking dimension that we gain will be collapsed
    using a weighted average.

    Args:
        cube (iris.cube.Cube):
            Cube to be processed.
        mask (iris.cube.Cube):
            Cube to act as a mask.
        weights (iris.cube.Cube, Optional):
            Cube containing the weights which are used for collapsing the
            dimension gained through masking. (Optional).
        coord_for_masking (str):
            String matching the name of the coordinate that will be used
            for masking.
        neighbourhood_shape (str):
            Name of the neighbourhood method to use.
            Options: "circular", "square".
            Default: "square".
        radii (list of float):
            The radius or a list of radii in metres of the neighbourhood to
            apply.
            If it is a list, it must be the same length as lead_times, which
            defines at which lead time to use which nbhood radius. The radius
            will be interpolated for intermediate lead times.
        lead_times (list of int):
            The lead times in hours that correspond to the radii to be used.
            If lead_times are set, radii must be a list the same length as
            lead_times. Lead times must be given as integer values.
        area_sum (bool):
            Return sum rather than fraction over the neighbourhood area.


    Returns:
        iris.cube.Cube:
            A cube after being fully processed.
    """
    from improver.nbhood import radius_by_lead_time
    from improver.nbhood.use_nbhood import ApplyNeighbourhoodProcessingWithAMask

    radius_or_radii, lead_times = radius_by_lead_time(radii, lead_times)

    result = ApplyNeighbourhoodProcessingWithAMask(
        coord_for_masking,
        neighbourhood_shape,
        radius_or_radii,
        lead_times=lead_times,
        collapse_weights=weights,
        sum_only=area_sum,
    )(cube, mask)

    return result