class Test__check_time_bounds_ranges(IrisTest): """Test the _check_time_bounds_ranges method""" def setUp(self): """Set up some cubes with different time bounds ranges""" frt = dt(2017, 11, 9, 21, 0) times = [ dt(2017, 11, 10, 3, 0), dt(2017, 11, 10, 4, 0), dt(2017, 11, 10, 5, 0) ] time_bounds = np.array([ [dt(2017, 11, 10, 2, 0), dt(2017, 11, 10, 3, 0)], [dt(2017, 11, 10, 3, 0), dt(2017, 11, 10, 4, 0)], [dt(2017, 11, 10, 4, 0), dt(2017, 11, 10, 5, 0)], ]) cubes = iris.cube.CubeList([]) for tpoint, tbounds in zip(times, time_bounds): cube = set_up_probability_cube( 0.6 * np.ones((2, 3, 3), dtype=np.float32), np.array([278.0, 280.0], dtype=np.float32), time=tpoint, frt=frt, time_bounds=tbounds, ) cubes.append(cube) self.matched_cube = cubes.merge_cube() time_bounds[2, 0] = dt(2017, 11, 10, 2, 0) cubes = iris.cube.CubeList([]) for tpoint, tbounds in zip(times, time_bounds): cube = set_up_probability_cube( 0.6 * np.ones((2, 3, 3), dtype=np.float32), np.array([278.0, 280.0], dtype=np.float32), time=tpoint, frt=frt, time_bounds=tbounds, ) cubes.append(cube) self.unmatched_cube = cubes.merge_cube() self.plugin = MergeCubes() def test_basic(self): """Test no error when bounds match""" self.plugin._check_time_bounds_ranges(self.matched_cube) def test_inverted(self): """Test no error when bounds ranges match but bounds are in the wrong order""" inverted_bounds = np.flip( self.matched_cube.coord("time").bounds.copy(), axis=1) self.matched_cube.coord("time").bounds = inverted_bounds self.plugin._check_time_bounds_ranges(self.matched_cube) def test_error(self): """Test error when bounds do not match""" msg = "Cube with mismatching time bounds ranges" with self.assertRaisesRegex(ValueError, msg): self.plugin._check_time_bounds_ranges(self.unmatched_cube) def test_no_error_missing_coord(self): """Test missing time or forecast period coordinate does not raise error""" self.matched_cube.remove_coord("forecast_period") self.plugin._check_time_bounds_ranges(self.matched_cube)
def test_basic(self): """Test default parameters""" plugin = MergeCubes() self.assertSequenceEqual(plugin.silent_attributes, ["history", "title", "mosg__grid_version"])
def process( *cubes: cli.inputcube, coordinate, central_point: float, units=None, width: float = None, calendar="gregorian", blend_time_using_forecast_period=False, ): """Runs weighted blending across adjacent points. Uses the TriangularWeightedBlendAcrossAdjacentPoints to blend across a particular coordinate. It does not collapse the coordinate, but instead blends across adjacent points and puts the blended values back in the original coordinate, with adjusted bounds. Args: cubes (list of iris.cube.Cube): A list of cubes including and surrounding the central point. coordinate (str): The coordinate over which the blending will be applied. central_point (float): Central point at which the output from the triangular weighted blending will be calculated. This should be in the units of the units argument that is passed in. This value should be a point on the coordinate for blending over. units (str): Units of the central_point and width width (float): Width of the triangular weighting function used in the blending, in the units of the units argument. calendar (str) Calendar for parameter_unit if required. blend_time_using_forecast_period (bool): If True, we are blending over time but using the forecast period coordinate as a proxy. Note, this should only be used when time and forecast_period share a dimension: i.e when all cubes provided are from the same forecast cycle. Returns: iris.cube.Cube: A processed Cube Raises: ValueError: If coordinate has "time" in it. ValueError: If blend_time_forecast_period is not used with forecast_period coordinate. """ from cf_units import Unit from iris.cube import CubeList from improver.blending.blend_across_adjacent_points import ( TriangularWeightedBlendAcrossAdjacentPoints, ) from improver.utilities.cube_manipulation import MergeCubes # TriangularWeightedBlendAcrossAdjacentPoints can't currently handle # blending over times where iris reads the coordinate points as datetime # objects. Fail here to avoid unhelpful errors downstream. if "time" in coordinate: msg = ("Cannot blend over {} coordinate (points encoded as datetime " "objects)".format(coordinate)) raise ValueError(msg) # This is left as a placeholder for when we have this capability if coordinate == "time": units = Unit(units, calendar) cubes = CubeList(cubes) if blend_time_using_forecast_period and coordinate == "forecast_period": cube = MergeCubes()(cubes, check_time_bounds_ranges=True) elif blend_time_using_forecast_period: msg = ('"--blend-time-using-forecast-period" can only be used with ' '"forecast_period" coordinate') raise ValueError(msg) else: cube = MergeCubes()(cubes) blending_plugin = TriangularWeightedBlendAcrossAdjacentPoints( coordinate, central_point, units, width) result = blending_plugin(cube) return result
class Test_process(IrisTest): """Test the process method (see also test_merge_cubes.py)""" def setUp(self): """Use temperature exceedance probability cubes to test with.""" data = np.ones((2, 3, 3), dtype=np.float32) thresholds = np.array([274, 275], dtype=np.float32) time_point = dt(2015, 11, 23, 7) # set up some UKV cubes with 4, 5 and 6 hour forecast periods and # different histories self.cube_ukv = set_up_probability_cube( data.copy(), thresholds.copy(), standard_grid_metadata="uk_det", time=time_point, frt=dt(2015, 11, 23, 3), attributes={"history": "something"}, ) self.cube_ukv_t1 = set_up_probability_cube( data.copy(), thresholds.copy(), standard_grid_metadata="uk_det", time=time_point, frt=dt(2015, 11, 23, 2), attributes={"history": "different"}, ) self.cube_ukv_t2 = set_up_probability_cube( data.copy(), thresholds.copy(), standard_grid_metadata="uk_det", time=time_point, frt=dt(2015, 11, 23, 1), attributes={"history": "entirely"}, ) self.plugin = MergeCubes() def test_basic(self): """Test that the utility returns an iris.cube.Cube""" result = self.plugin.process([self.cube_ukv, self.cube_ukv_t1]) self.assertIsInstance(result, iris.cube.Cube) def test_null(self): """Test single cube is returned unmodified""" cube = self.cube_ukv.copy() result = self.plugin.process(cube) self.assertArrayAlmostEqual(result.data, self.cube_ukv.data) self.assertEqual(result.metadata, self.cube_ukv.metadata) def test_single_item_list(self): """Test cube from single item list is returned unmodified""" cubelist = iris.cube.CubeList([self.cube_ukv.copy()]) result = self.plugin.process(cubelist) self.assertArrayAlmostEqual(result.data, self.cube_ukv.data) self.assertEqual(result.metadata, self.cube_ukv.metadata) def test_unmatched_attributes(self): """Test that unmatched attributes are removed without modifying the input cubes""" result = self.plugin.process([self.cube_ukv, self.cube_ukv_t1]) self.assertNotIn("history", result.attributes.keys()) self.assertEqual(self.cube_ukv.attributes["history"], "something") self.assertEqual(self.cube_ukv_t1.attributes["history"], "different") def test_identical_cubes(self): """Test that merging identical cubes fails.""" cubes = iris.cube.CubeList([self.cube_ukv, self.cube_ukv]) msg = "failed to merge into a single cube" with self.assertRaisesRegex(DuplicateDataError, msg): self.plugin.process(cubes) def test_lagged_ukv(self): """Test lagged UKV merge OK (forecast periods in seconds)""" expected_fp_points = 3600 * np.array([6, 5, 4], dtype=np.int32) cubes = iris.cube.CubeList( [self.cube_ukv, self.cube_ukv_t1, self.cube_ukv_t2]) result = self.plugin.process(cubes) self.assertIsInstance(result, iris.cube.Cube) self.assertArrayAlmostEqual( result.coord("forecast_period").points, expected_fp_points) def test_check_time_bounds_ranges(self): """Test optional failure when time bounds ranges are not matched (eg if merging cubes with different accumulation periods)""" time_point = dt(2015, 11, 23, 7) time_bounds = [dt(2015, 11, 23, 4), time_point] cube1 = set_up_variable_cube( self.cube_ukv.data.copy(), standard_grid_metadata="uk_det", time=time_point, frt=dt(2015, 11, 23, 3), time_bounds=time_bounds, ) cube2 = cube1.copy() cube2.coord("forecast_reference_time").points = ( cube2.coord("forecast_reference_time").points + 3600) cube2.coord("time").bounds = [ cube2.coord("time").bounds[0, 0] + 3600, cube2.coord("time").bounds[0, 1], ] cube2.coord("forecast_period").bounds = [ cube2.coord("forecast_period").bounds[0, 0] + 3600, cube2.coord("forecast_period").bounds[0, 1], ] msg = "Cube with mismatching time bounds ranges cannot be blended" with self.assertRaisesRegex(ValueError, msg): self.plugin.process([cube1, cube2], check_time_bounds_ranges=True)
def process(self, cube_t0, cube_t1): """ Interpolate data to intermediate times between validity times of cube_t0 and cube_t1. Args: cube_t0 (iris.cube.Cube): A diagnostic cube valid at the beginning of the period within which interpolation is to be permitted. cube_t1 (iris.cube.Cube): A diagnostic cube valid at the end of the period within which interpolation is to be permitted. Returns: iris.cube.CubeList: A list of cubes interpolated to the desired times. Raises: TypeError: If cube_t0 and cube_t1 are not of type iris.cube.Cube. CoordinateNotFoundError: The input cubes contain no time coordinate. ValueError: Cubes contain multiple validity times. ValueError: The input cubes are ordered such that the initial time cube has a later validity time than the final cube. """ if not isinstance(cube_t0, iris.cube.Cube) or not isinstance( cube_t1, iris.cube.Cube): msg = ("Inputs to TemporalInterpolation are not of type " "iris.cube.Cube, first input is type " "{}, second input is type {}".format( type(cube_t0), type(cube_t1))) raise TypeError(msg) try: (initial_time, ) = iris_time_to_datetime(cube_t0.coord("time")) (final_time, ) = iris_time_to_datetime(cube_t1.coord("time")) except CoordinateNotFoundError: msg = ("Cube provided to TemporalInterpolation contains no time " "coordinate.") raise CoordinateNotFoundError(msg) except ValueError: msg = ("Cube provided to TemporalInterpolation contains multiple " "validity times, only one expected.") raise ValueError(msg) if initial_time > final_time: raise ValueError("TemporalInterpolation input cubes " "ordered incorrectly" ", with the final time being before the initial " "time.") time_list = self.construct_time_list(initial_time, final_time) cubes = iris.cube.CubeList([cube_t0, cube_t1]) cube = MergeCubes()(cubes) interpolated_cube = cube.interpolate(time_list, iris.analysis.Linear()) self.enforce_time_coords_dtype(interpolated_cube) interpolated_cubes = iris.cube.CubeList() if self.interpolation_method == "solar": interpolated_cubes = self.solar_interpolate( cube, interpolated_cube) elif self.interpolation_method == "daynight": interpolated_cubes = self.daynight_interpolate(interpolated_cube) else: for single_time in interpolated_cube.slices_over("time"): interpolated_cubes.append(single_time) return interpolated_cubes