def test_error_regrid_with_incorrect_landmask(self):
     """Test an error is thrown if a landmask is provided that does not
     match the source grid"""
     landmask = self.target_grid.copy()
     plugin = StandardiseGridAndMetadata(regrid_mode='nearest-with-mask',
                                         landmask=landmask,
                                         landmask_vicinity=90000)
     msg = "Source landmask does not match input grid"
     with self.assertRaisesRegex(ValueError, msg):
         plugin.process(self.cube, target_grid=self.target_grid)
 def setUp(self):
     """Set up input cube"""
     self.cube = set_up_variable_cube(
         282 * np.ones((5, 5), dtype=np.float32),
         spatial_grid='latlon',
         standard_grid_metadata='gl_det',
         time=datetime(2019, 10, 11),
         time_bounds=[datetime(2019, 10, 10, 23),
                      datetime(2019, 10, 11)],
         frt=datetime(2019, 10, 10, 18))
     self.plugin = StandardiseGridAndMetadata()
 def test_incorrect_grid_attributes_removed(self):
     """Test grid attributes not present on the target cube are removed
     after regridding"""
     self.target_grid.attributes.pop("mosg__grid_domain")
     result = StandardiseGridAndMetadata().process(
         self.cube, target_grid=self.target_grid)
     self.assertNotIn("mosg__grid_domain", result.attributes)
 def test_default(self):
     """Test initialisation with default options"""
     plugin = StandardiseGridAndMetadata()
     self.assertEqual(plugin.regrid_mode, 'bilinear')
     self.assertEqual(plugin.extrapolation_mode, 'nanmask')
     self.assertIsNone(plugin.landmask_source_grid)
     self.assertIsNone(plugin.landmask_vicinity)
     self.assertEqual(plugin.landmask_name, 'land_binary_mask')
 def test_access_regrid_with_landmask(self):
     """Test the RegridLandAndSea module is correctly called when using
     landmask arguments. Diagnosed by identifiable error."""
     msg = "Distance of 10000m gives zero cell extent"
     with self.assertRaisesRegex(ValueError, msg):
         StandardiseGridAndMetadata(regrid_mode='nearest-with-mask',
                                    landmask=self.landmask,
                                    landmask_vicinity=10000).process(
                                        self.cube,
                                        target_grid=self.target_grid)
 def test_basic_regrid(self):
     """Test default regridding arguments return expected dimensionality
     and updated grid-defining attributes"""
     expected_data = 282 * np.ones((12, 12), dtype=np.float32)
     expected_attributes = {
         "mosg__model_configuration": "gl_det",
         "title": MANDATORY_ATTRIBUTE_DEFAULTS["title"]
     }
     for attr in [
             "mosg__grid_domain", "mosg__grid_type", "mosg__grid_version"
     ]:
         expected_attributes[attr] = self.target_grid.attributes[attr]
     result = StandardiseGridAndMetadata().process(self.cube,
                                                   self.target_grid.copy())
     self.assertArrayAlmostEqual(result.data, expected_data)
     for axis in ['x', 'y']:
         self.assertEqual(result.coord(axis=axis),
                          self.target_grid.coord(axis=axis))
     self.assertDictEqual(result.attributes, expected_attributes)
 def test_attribute_changes_with_regridding(self):
     """Test attributes inherited on regridding"""
     expected_attributes = self.cube.attributes
     expected_attributes["title"] = MANDATORY_ATTRIBUTE_DEFAULTS["title"]
     for attr in [
             "mosg__grid_domain", "mosg__grid_type", "mosg__grid_version"
     ]:
         expected_attributes[attr] = self.target_grid.attributes[attr]
     result = StandardiseGridAndMetadata().process(
         self.cube, target_grid=self.target_grid)
     self.assertDictEqual(result.attributes, expected_attributes)
 def test_new_title(self):
     """Test new title can be set on regridding"""
     new_title = "Global Model Forecast on UK 2km Standard Grid"
     expected_attributes = self.cube.attributes
     expected_attributes["title"] = new_title
     for attr in [
             "mosg__grid_domain", "mosg__grid_type", "mosg__grid_version"
     ]:
         expected_attributes[attr] = self.target_grid.attributes[attr]
     result = StandardiseGridAndMetadata().process(
         self.cube, target_grid=self.target_grid, regridded_title=new_title)
     self.assertDictEqual(result.attributes, expected_attributes)
 def test_run_regrid_with_landmask(self):
     """Test masked regridding (same expected values as basic, since input
     points are all equal)"""
     expected_data = 282 * np.ones((12, 12), dtype=np.float32)
     expected_attributes = {
         "mosg__model_configuration": "gl_det",
         "title": MANDATORY_ATTRIBUTE_DEFAULTS["title"]
     }
     for attr in [
             "mosg__grid_domain", "mosg__grid_type", "mosg__grid_version"
     ]:
         expected_attributes[attr] = self.target_grid.attributes[attr]
     result = StandardiseGridAndMetadata(
         regrid_mode='nearest-with-mask',
         landmask=self.landmask,
         landmask_vicinity=90000).process(
             self.cube, target_grid=self.target_grid.copy())
     self.assertArrayAlmostEqual(result.data, expected_data)
     for axis in ['x', 'y']:
         self.assertEqual(result.coord(axis=axis),
                          self.target_grid.coord(axis=axis))
     self.assertDictEqual(result.attributes, expected_attributes)
 def test_warning_source_not_landmask(self, warning_list=None):
     """Test warning is raised if landmask_source_grid is not a landmask"""
     expected_data = 282 * np.ones((12, 12), dtype=np.float32)
     self.landmask.rename("not_a_landmask")
     result = StandardiseGridAndMetadata(regrid_mode='nearest-with-mask',
                                         landmask=self.landmask,
                                         landmask_vicinity=90000).process(
                                             self.cube,
                                             target_grid=self.target_grid)
     msg = "Expected land_binary_mask in input_landmask cube"
     self.assertTrue(any([msg in str(warning) for warning in warning_list]))
     self.assertTrue(
         any(item.category == UserWarning for item in warning_list))
     self.assertArrayAlmostEqual(result.data, expected_data)
 def test_attribute_changes_after_regridding(self):
     """Test attributes can be manually updated after regridding"""
     attribute_changes = {
         "institution": "Met Office",
         "mosg__grid_version": "remove"
     }
     expected_attributes = {
         "mosg__grid_domain": "uk_extended",
         "mosg__grid_type": "standard",
         "mosg__model_configuration": "gl_det",
         "institution": "Met Office",
         "title": MANDATORY_ATTRIBUTE_DEFAULTS["title"]
     }
     result = StandardiseGridAndMetadata().process(
         self.cube,
         target_grid=self.target_grid,
         attributes_dict=attribute_changes)
     self.assertDictEqual(result.attributes, expected_attributes)
Example #12
0
def process(cube: cli.inputcube,
            target_grid: cli.inputcube = None,
            land_sea_mask: cli.inputcube = None,
            *,
            regrid_mode='bilinear',
            extrapolation_mode='nanmask',
            land_sea_mask_vicinity: float = 25000,
            regridded_title: str = None,
            attributes_config: cli.inputjson = None,
            coords_to_remove: cli.comma_separated_list = None,
            new_name: str = None,
            new_units: str = None,
            fix_float64=False):
    """Standardises a cube by one or more of regridding, updating meta-data etc

    Standardise a source cube. Available options are regridding (bi-linear or
    nearest-neighbour, optionally with land-mask awareness), renaming,
    converting units, updating attributes and / or converting float64 data to
    float32.

    Args:
        cube (iris.cube.Cube):
            Source cube to be standardised
        target_grid (iris.cube.Cube):
            If specified, then regridding of the source against the target
            grid is enabled. If also using land_sea_mask-aware regridding then
            this must be land_binary_mask data.
        land_sea_mask (iris.cube.Cube):
            A cube describing the land_binary_mask on the source-grid if
            coastline-aware regridding is required.
        regrid_mode (str):
            Selects which regridding techniques to use. Default uses
            iris.analysis.Linear(); "nearest" uses Nearest() (for less
            continuous fields, e.g precipitation); "nearest-with-mask"
            ensures that target data are sources from points with the same
            mask value (for coast-line-dependant variables like temperature).
        extrapolation_mode (str):
            Mode to use for extrapolating data into regions beyond the limits
            of the input cube domain. Refer to online documentation for
            iris.analysis.
            Modes are -
            extrapolate - extrapolated points will take their values from the
            nearest source point
            nan - extrapolated points will be set to NaN
            error - a ValueError will be raised notifying an attempt to
            extrapolate
            mask - extrapolated points will always be masked, even if
            the source data is not a MaskedArray
            nanmask - if the source data is a MaskedArray extrapolated points
            will be masked; otherwise they will be set to NaN
        land_sea_mask_vicinity (float):
            Radius of vicinity to search for a coastline, in metres.
        regridded_title (str):
            New "title" attribute to be set if the field is being regridded
            (since "title" may contain grid information). If None, a default
            value is used.
        attributes_config (dict):
            Dictionary containing required changes that will be applied to
            the attributes.
        coords_to_remove (list):
            List of names of scalar coordinates to remove.
        new_name (str):
            Name of output cube.
        new_units (str):
            Units to convert to.
        fix_float64 (bool):
            If True, checks and fixes cube for float64 data. Without this
            option an exception will be raised if float64 data is found but no
            fix applied.

    Returns:
        iris.cube.Cube:
            Processed cube.

    Raises:
        ValueError:
            If source land_sea_mask is supplied but regrid mode is not
            "nearest-with-mask".
        ValueError:
            If regrid_mode is "nearest-with-mask" but no source land_sea_mask
            is provided (from plugin).
    """
    from improver.standardise import StandardiseGridAndMetadata

    if (land_sea_mask and
            "nearest-with-mask" not in regrid_mode):
        msg = ("Land-mask file supplied without appropriate regrid-mode. "
               "Use --regrid-mode nearest-with-mask.")
        raise ValueError(msg)

    plugin = StandardiseGridAndMetadata(
        regrid_mode=regrid_mode, extrapolation_mode=extrapolation_mode,
        landmask=land_sea_mask,
        landmask_vicinity=land_sea_mask_vicinity)
    output_data = plugin.process(
        cube, target_grid, new_name=new_name, new_units=new_units,
        regridded_title=regridded_title, coords_to_remove=coords_to_remove,
        attributes_dict=attributes_config, fix_float64=fix_float64)

    return output_data
class Test_process_no_regrid(IrisTest):
    """Test the process method without regridding options."""
    def setUp(self):
        """Set up input cube"""
        self.cube = set_up_variable_cube(
            282 * np.ones((5, 5), dtype=np.float32),
            spatial_grid='latlon',
            standard_grid_metadata='gl_det',
            time=datetime(2019, 10, 11),
            time_bounds=[datetime(2019, 10, 10, 23),
                         datetime(2019, 10, 11)],
            frt=datetime(2019, 10, 10, 18))
        self.plugin = StandardiseGridAndMetadata()

    def test_null(self):
        """Test process method with default arguments returns an unchanged
        cube"""
        result = self.plugin.process(self.cube.copy())
        self.assertIsInstance(result, iris.cube.Cube)
        self.assertArrayAlmostEqual(result.data, self.cube.data)
        self.assertEqual(result.metadata, self.cube.metadata)

    def test_standardise_time_coords(self):
        """Test incorrect time-type coordinates are cast to the correct
        datatypes and units"""
        for coord in ["time", "forecast_period"]:
            self.cube.coord(coord).points = (
                self.cube.coord(coord).points.astype(np.float64))
            self.cube.coord(coord).bounds = (
                self.cube.coord(coord).bounds.astype(np.float64))
        self.cube.coord("forecast_period").convert_units("hours")
        result = self.plugin.process(self.cube)
        self.assertEqual(result.coord("forecast_period").units, "seconds")
        self.assertEqual(
            result.coord("forecast_period").points.dtype, np.int32)
        self.assertEqual(
            result.coord("forecast_period").bounds.dtype, np.int32)
        self.assertEqual(result.coord("time").points.dtype, np.int64)
        self.assertEqual(result.coord("time").bounds.dtype, np.int64)

    def test_standardise_time_coords_missing_fp(self):
        """Test a missing time-type coordinate does not cause an error when
        standardisation is required"""
        self.cube.coord("time").points = (
            self.cube.coord("time").points.astype(np.float64))
        self.cube.remove_coord("forecast_period")
        result = self.plugin.process(self.cube)
        self.assertEqual(result.coord("time").points.dtype, np.int64)

    def test_collapse_scalar_dimensions(self):
        """Test scalar dimension is collapsed"""
        cube = iris.util.new_axis(self.cube, "time")
        result = self.plugin.process(cube)
        dim_coord_names = [
            coord.name() for coord in result.coords(dim_coords=True)
        ]
        aux_coord_names = [
            coord.name() for coord in result.coords(dim_coords=False)
        ]
        self.assertSequenceEqual(result.shape, (5, 5))
        self.assertNotIn("time", dim_coord_names)
        self.assertIn("time", aux_coord_names)

    def test_realization_not_collapsed(self):
        """Test scalar realization coordinate is preserved"""
        realization = AuxCoord([1], "realization")
        self.cube.add_aux_coord(realization)
        cube = iris.util.new_axis(self.cube, "realization")
        result = self.plugin.process(cube)
        dim_coord_names = [
            coord.name() for coord in result.coords(dim_coords=True)
        ]
        self.assertSequenceEqual(result.shape, (1, 5, 5))
        self.assertIn("realization", dim_coord_names)

    def test_metadata_changes(self):
        """Test changes to cube name, coordinates and attributes without
        regridding"""
        new_name = "regridded_air_temperature"
        attribute_changes = {
            "institution": "Met Office",
            "mosg__grid_version": "remove"
        }
        expected_attributes = {
            "mosg__grid_domain": "global",
            "mosg__grid_type": "standard",
            "mosg__model_configuration": "gl_det",
            "institution": "Met Office"
        }
        expected_data = self.cube.data.copy() - 273.15
        result = self.plugin.process(self.cube,
                                     new_name=new_name,
                                     new_units="degC",
                                     coords_to_remove=["forecast_period"],
                                     attributes_dict=attribute_changes)
        self.assertEqual(result.name(), new_name)
        self.assertEqual(result.units, "degC")
        self.assertArrayAlmostEqual(result.data, expected_data, decimal=5)
        self.assertDictEqual(result.attributes, expected_attributes)
        self.assertNotIn("forecast_period",
                         [coord.name() for coord in result.coords()])

    def test_float_deescalation(self):
        """Test precision de-escalation from float64 to float32"""
        cube = self.cube.copy()
        cube.data = cube.data.astype(np.float64)
        result = self.plugin.process(cube)
        self.assertEqual(result.data.dtype, np.float32)
        self.assertArrayAlmostEqual(self.cube.data, result.data, decimal=4)
 def test_error_missing_landmask(self):
     """Test an error is thrown if no mask is provided for masked
     regridding"""
     msg = "requires an input landmask cube"
     with self.assertRaisesRegex(ValueError, msg):
         StandardiseGridAndMetadata(regrid_mode='nearest-with-mask')