class Test_process(IrisTest): """Test dimensioned cube data is correctly advected""" def setUp(self): """Set up plugin instance and a cube to advect""" vel_x = set_up_xy_velocity_cube("advection_velocity_x") vel_y = vel_x.copy(data=2. * np.ones(shape=(4, 3))) vel_y.rename("advection_velocity_y") self.plugin = AdvectField(vel_x, vel_y) data = np.array([[2., 3., 4.], [1., 2., 3.], [0., 1., 2.], [0., 0., 1.]]) self.cube = iris.cube.Cube(data, standard_name='rainfall_rate', units='mm h-1', dim_coords_and_dims=[ (self.plugin.y_coord, 0), (self.plugin.x_coord, 1) ]) # input time: [datetime.datetime(2018, 2, 20, 4, 0)] self.time_coord = DimCoord(1519099200, standard_name="time", units='seconds since 1970-01-01 00:00:00') self.cube.add_aux_coord(self.time_coord) self.timestep = datetime.timedelta(seconds=600) def test_basic(self): """Test plugin returns a cube""" result = self.plugin.process(self.cube, self.timestep) self.assertIsInstance(result, iris.cube.Cube) def test_advected_values(self): """Test output cube data is as expected""" expected_data = np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 2., 3.], [np.nan, 1., 2.]]) result = self.plugin.process(self.cube, self.timestep) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) def test_masked_input(self): """Test masked data is correctly advected and remasked""" mask = np.array([[True, True, True], [False, False, False], [False, False, False], [False, False, False]]) masked_data = np.ma.MaskedArray(self.cube.data, mask=mask) masked_cube = self.cube.copy(masked_data) expected_data = np.full((4, 3), np.nan, dtype=np.float32) expected_data[3, 1:] = np.array([1., 2.]) expected_mask = ~np.isfinite(expected_data) result = self.plugin.process(masked_cube, self.timestep) self.assertIsInstance(result.data, np.ma.MaskedArray) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) self.assertArrayEqual(result.data.mask, expected_mask) def test_mask_creation(self): """Test a mask is added if the fill value is NaN""" expected_output = np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 2., 3.], [np.nan, 1., 2.]]) result = self.plugin.process(self.cube, self.timestep) self.assertIsInstance(result.data, np.ma.MaskedArray) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_output[~result.data.mask]) @ManageWarnings(record=True) def test_unmasked_nans(self, warning_list=None): """Test an array with unmasked nans raises a warning""" data_with_nan = self.cube.data data_with_nan[2, 1] = np.nan cube = self.cube.copy(data_with_nan) expected_data = np.full((4, 3), np.nan, dtype=np.float32) expected_data[2, 1:] = np.array([2., 3.]) result = self.plugin.process(cube, self.timestep) self.assertTrue(len(warning_list) == 1) self.assertTrue(warning_list[0].category == UserWarning) self.assertIn("contains unmasked NaNs", str(warning_list[0])) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) def test_time_step(self): """Test outputs are OK for a time step with non-second components""" self.plugin.vel_x.data = self.plugin.vel_x.data / 6. self.plugin.vel_y.data = self.plugin.vel_y.data / 6. expected_data = np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 2., 3.], [np.nan, 1., 2.]]) result = self.plugin.process(self.cube, datetime.timedelta(hours=1)) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) def test_raises_grid_mismatch_error(self): """Test error is raised if cube grid does not match velocity grids""" x_coord = DimCoord(np.arange(5), 'projection_x_coordinate', units='km') y_coord = DimCoord(np.arange(4), 'projection_y_coordinate', units='km') cube = iris.cube.Cube(np.zeros(shape=(4, 5)), standard_name='rainfall_rate', units='mm h-1', dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)]) cube.add_aux_coord(self.time_coord) msg = "Input data grid does not match advection velocities" with self.assertRaisesRegex(InvalidCubeError, msg): self.plugin.process(cube, self.timestep) def test_validity_time(self): """Test output cube time is correctly updated""" result = self.plugin.process(self.cube, self.timestep) output_cube_time, = \ (result.coord("time").units).num2date(result.coord("time").points) self.assertEqual(output_cube_time.year, 2018) self.assertEqual(output_cube_time.month, 2) self.assertEqual(output_cube_time.day, 20) self.assertEqual(output_cube_time.hour, 4) self.assertEqual(output_cube_time.minute, 10) def test_lead_time(self): """Test output cube has a forecast_period coordinate with the correct value and units""" result = self.plugin.process(self.cube, self.timestep) result.coord("forecast_period").convert_units("s") lead_time = result.coord("forecast_period").points self.assertEqual(len(lead_time), 1) self.assertEqual(lead_time[0], self.timestep.total_seconds())
class Test_process(IrisTest): """Test dimensioned cube data is correctly advected""" def setUp(self): """Set up plugin instance and a cube to advect""" vel_x = set_up_xy_velocity_cube("advection_velocity_x") vel_y = vel_x.copy(data=2. * np.ones(shape=(4, 3))) vel_y.rename("advection_velocity_y") self.plugin = AdvectField(vel_x, vel_y) self.metadata_dict = { "attributes": { "mosg__grid_version": "1.0.0", "mosg__model_configuration": "nc_det", "source": "Met Office Nowcast", "institution": "Met Office", "title": "Nowcast on UK 2 km Standard Grid" } } self.plugin_with_meta = AdvectField(vel_x, vel_y, metadata_dict=self.metadata_dict) data = np.array( [[2., 3., 4.], [1., 2., 3.], [0., 1., 2.], [0., 0., 1.]], dtype=np.float32) self.cube = iris.cube.Cube(data, standard_name='rainfall_rate', units='mm h-1', dim_coords_and_dims=[ (self.plugin.y_coord, 0), (self.plugin.x_coord, 1) ]) # input time: [datetime.datetime(2018, 2, 20, 4, 0)] self.time_coord = DimCoord(1519099200, standard_name="time", units='seconds since 1970-01-01 00:00:00') self.cube.add_aux_coord(self.time_coord) self.timestep = datetime.timedelta(seconds=600) def test_basic(self): """Test plugin returns a cube""" result = self.plugin.process(self.cube, self.timestep) self.assertIsInstance(result, iris.cube.Cube) def test_metadata(self): """Test plugin returns a cube with the desired metadata.""" result = self.plugin_with_meta.process(self.cube, self.timestep) result.attributes.pop("history") self.assertEqual(result.attributes, self.metadata_dict["attributes"]) def test_check_source_metadata(self): """Test plugin returns a cube with the desired source attribute.""" institution_cube = self.cube.copy() institution_cube.attributes["institution"] = "Met Office" expected_source = "Met Office Nowcast" result = self.plugin.process(institution_cube, self.timestep) self.assertEqual(result.attributes["source"], expected_source) def test_check_source_metadata_no_institution(self): """Test plugin returns a cube with the desired source attribute without an institution.""" institution_cube = self.cube.copy() expected_source = "Nowcast" result = self.plugin.process(institution_cube, self.timestep) self.assertEqual(result.attributes["source"], expected_source) def test_check_history_metadata(self): """Test plugin returns a cube with the desired metadata.""" expected_pattern = (r'[0-9]{4}-[0-9]{2}-[0-9]{2}T' r'[0-9]{2}:[0-9]{2}:[0-9]{2}Z: Nowcast') result = self.plugin.process(self.cube, self.timestep) self.assertTrue("history" in result.attributes.keys()) self.assertRegex(result.attributes['history'], expected_pattern) def test_advected_values(self): """Test output cube data is as expected""" expected_data = np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 2., 3.], [np.nan, 1., 2.]]) result = self.plugin.process(self.cube, self.timestep) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) def test_masked_input(self): """Test masked data is correctly advected and remasked""" mask = np.array([[True, True, True], [False, False, False], [False, False, False], [False, False, False]]) masked_data = np.ma.MaskedArray(self.cube.data, mask=mask) masked_cube = self.cube.copy(masked_data) expected_data = np.full((4, 3), np.nan, dtype=np.float32) expected_data[3, 1:] = np.array([1., 2.]) expected_mask = ~np.isfinite(expected_data) result = self.plugin.process(masked_cube, self.timestep) self.assertIsInstance(result.data, np.ma.MaskedArray) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) self.assertArrayEqual(result.data.mask, expected_mask) def test_mask_creation(self): """Test a mask is added if the fill value is NaN""" expected_output = np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 2., 3.], [np.nan, 1., 2.]]) result = self.plugin.process(self.cube, self.timestep) self.assertIsInstance(result.data, np.ma.MaskedArray) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_output[~result.data.mask]) @ManageWarnings(record=True) def test_unmasked_nans(self, warning_list=None): """Test an array with unmasked nans raises a warning""" data_with_nan = self.cube.data data_with_nan[2, 1] = np.nan cube = self.cube.copy(data_with_nan) expected_data = np.full((4, 3), np.nan, dtype=np.float32) expected_data[2, 1:] = np.array([2., 3.]) result = self.plugin.process(cube, self.timestep) warning_msg = "contains unmasked NaNs" self.assertTrue( any(item.category == UserWarning for item in warning_list)) self.assertTrue(any(warning_msg in str(item) for item in warning_list)) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) def test_time_step(self): """Test outputs are OK for a time step with non-second components""" self.plugin.vel_x.data = self.plugin.vel_x.data / 6. self.plugin.vel_y.data = self.plugin.vel_y.data / 6. expected_data = np.array([[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 2., 3.], [np.nan, 1., 2.]]) result = self.plugin.process(self.cube, datetime.timedelta(hours=1)) self.assertArrayAlmostEqual(result.data[~result.data.mask], expected_data[~result.data.mask]) def test_raises_grid_mismatch_error(self): """Test error is raised if cube grid does not match velocity grids""" x_coord = DimCoord(np.arange(5), 'projection_x_coordinate', units='km') y_coord = DimCoord(np.arange(4), 'projection_y_coordinate', units='km') cube = iris.cube.Cube(np.zeros(shape=(4, 5)), standard_name='rainfall_rate', units='mm h-1', dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)]) cube.add_aux_coord(self.time_coord) msg = "Input data grid does not match advection velocities" with self.assertRaisesRegex(InvalidCubeError, msg): self.plugin.process(cube, self.timestep) def test_validity_time(self): """Test output cube time is correctly updated""" result = self.plugin.process(self.cube, self.timestep) output_cube_time, = \ (result.coord("time").units).num2date(result.coord("time").points) self.assertEqual(output_cube_time.year, 2018) self.assertEqual(output_cube_time.month, 2) self.assertEqual(output_cube_time.day, 20) self.assertEqual(output_cube_time.hour, 4) self.assertEqual(output_cube_time.minute, 10) def test_lead_time(self): """Test output cube has a forecast_period coordinate with the correct value and units""" result = self.plugin.process(self.cube, self.timestep) result.coord("forecast_period").convert_units("s") lead_time = result.coord("forecast_period").points self.assertEqual(len(lead_time), 1) self.assertEqual(lead_time[0], self.timestep.total_seconds()) def test_units_and_datatypes_for_time_coordinates(self): """Test that the output cube contains the desired units and datatypes for the time, forecast_reference_time and forecast_period coordinate. In this instance, no forecast_reference_time coordinate is present on the input cube. """ result = self.plugin.process(self.cube, self.timestep) self.assertEqual(result.coord("forecast_period").points, 600) # 2017-11-10 04:10:00 self.assertEqual(result.coord("time").points, 1519099800) self.assertEqual( result.coord("forecast_reference_time").points, 1519099200) self.assertEqual(result.coord("forecast_period").units, "seconds") self.assertEqual( result.coord("time").units, "seconds since 1970-01-01 00:00:00") self.assertEqual( result.coord("forecast_reference_time").units, "seconds since 1970-01-01 00:00:00") self.assertEqual(result.coord("forecast_period").dtype, np.int32) self.assertEqual(result.coord("time").dtype, np.int64) self.assertEqual( result.coord("forecast_reference_time").dtype, np.int64) def test_time_unit_conversion(self): """Test that the output cube contains the desired units and datatypes for the time, forecast_reference_time and forecast_period coordinate, where a unit conversion has been required for the time and forecast reference time coordinates.""" self.cube.coord("time").convert_units("hours since 1970-01-01 00:00") frt_coord = (DimCoord(1519099200, standard_name="forecast_reference_time", units='seconds since 1970-01-01 00:00:00')) frt_coord.convert_units("hours since 1970-01-01 00:00") self.cube.add_aux_coord(frt_coord) self.cube.coord("forecast_reference_time").convert_units( "hours since 1970-01-01 00:00") result = self.plugin.process(self.cube, self.timestep) self.assertEqual(result.coord("forecast_period").points, 600) # 2017-11-10 04:10:00 self.assertEqual(result.coord("time").points, 1519099800) self.assertEqual( result.coord("forecast_reference_time").points, 1519099200) self.assertEqual(result.coord("forecast_period").units, "seconds") self.assertEqual( result.coord("time").units, "seconds since 1970-01-01 00:00:00") self.assertEqual( result.coord("forecast_reference_time").units, "seconds since 1970-01-01 00:00:00") self.assertEqual(result.coord("forecast_period").dtype, np.int32) self.assertEqual(result.coord("time").dtype, np.int64) self.assertEqual( result.coord("forecast_reference_time").dtype, np.int64) def test_floating_point_time_input(self): """Test that the output cube contains the desired units and datatypes for the time, forecast_reference_time and forecast_period coordinate, where a time and forecast_reference_time are input as floating point values, so that the impact of the rounding is clearer.""" self.cube.coord("time").points = 1519099199.7 frt_coord = (DimCoord(1519099199.7, standard_name="forecast_reference_time", units='seconds since 1970-01-01 00:00:00')) self.cube.add_aux_coord(frt_coord) result = self.plugin.process(self.cube, self.timestep) self.assertEqual(result.coord("forecast_period").points, 600) # 2017-11-10 04:10:00 self.assertEqual(result.coord("time").points, 1519099800) self.assertEqual( result.coord("forecast_reference_time").points, 1519099200) self.assertEqual(result.coord("forecast_period").units, "seconds") self.assertEqual( result.coord("time").units, "seconds since 1970-01-01 00:00:00") self.assertEqual( result.coord("forecast_reference_time").units, "seconds since 1970-01-01 00:00:00") self.assertEqual(result.coord("forecast_period").dtype, np.int32) self.assertEqual(result.coord("time").dtype, np.int64) self.assertEqual( result.coord("forecast_reference_time").dtype, np.int64)
class Test_process(IrisTest): """Test dimensioned cube data is correctly advected""" def setUp(self): """Set up plugin instance and a cube to advect""" vel_x = set_up_xy_velocity_cube("advection_velocity_x") vel_y = vel_x.copy(data=2.*np.ones(shape=(4, 3))) vel_y.rename("advection_velocity_y") self.plugin = AdvectField(vel_x, vel_y) data = np.array([[2., 3., 4.], [1., 2., 3.], [0., 1., 2.], [0., 0., 1.]]) self.cube = iris.cube.Cube( data, standard_name='rainfall_rate', units='mm h-1', dim_coords_and_dims=[(self.plugin.y_coord, 0), (self.plugin.x_coord, 1)]) # input time: [datetime.datetime(2018, 2, 20, 4, 0)] self.time_coord = DimCoord(1519099200, standard_name="time", units='seconds since 1970-01-01 00:00:00') self.cube.add_aux_coord(self.time_coord) self.timestep = datetime.timedelta(seconds=600) def test_basic(self): """Test plugin returns a cube""" result = self.plugin.process(self.cube, self.timestep) self.assertIsInstance(result, iris.cube.Cube) def test_advected_values(self): """Test output cube data is as expected""" expected_output = np.array([[0., 0., 0.], [0., 0., 0.], [0., 2., 3.], [0., 1., 2.]]) result = self.plugin.process(self.cube, self.timestep) self.assertArrayAlmostEqual(result.data, expected_output) def test_fill_values(self): """Test output cube data is padded as expected where source grid points are out of bounds""" expected_output = np.array([[-1., -1., -1.], [-1., -1., -1.], [-1., 2., 3.], [-1., 1., 2.]]) result = self.plugin.process(self.cube, self.timestep, fill_value=-1.) self.assertArrayAlmostEqual(result.data, expected_output) def test_time_step(self): """Test outputs are OK for a time step with non-second components""" self.plugin.vel_x.data = self.plugin.vel_x.data / 6. self.plugin.vel_y.data = self.plugin.vel_y.data / 6. expected_output = np.array([[0., 0., 0.], [0., 0., 0.], [0., 2., 3.], [0., 1., 2.]]) result = self.plugin.process(self.cube, datetime.timedelta(hours=1)) self.assertArrayAlmostEqual(result.data, expected_output) def test_raises_grid_mismatch_error(self): """Test error is raised if cube grid does not match velocity grids""" x_coord = DimCoord(np.arange(5), 'projection_x_coordinate', units='km') y_coord = DimCoord(np.arange(4), 'projection_y_coordinate', units='km') cube = iris.cube.Cube(np.zeros(shape=(4, 5)), standard_name='rainfall_rate', units='mm h-1', dim_coords_and_dims=[(y_coord, 0), (x_coord, 1)]) cube.add_aux_coord(self.time_coord) msg = "Input data grid does not match advection velocities" with self.assertRaisesRegex(InvalidCubeError, msg): self.plugin.process(cube, self.timestep) def test_validity_time(self): """Test output cube time is correctly updated""" result = self.plugin.process(self.cube, self.timestep) output_cube_time, = \ (result.coord("time").units).num2date(result.coord("time").points) self.assertEqual(output_cube_time.year, 2018) self.assertEqual(output_cube_time.month, 2) self.assertEqual(output_cube_time.day, 20) self.assertEqual(output_cube_time.hour, 4) self.assertEqual(output_cube_time.minute, 10)