def enforce_time_coords_dtype(cube: Cube) -> Cube: """ Enforce the data type of the time, forecast_reference_time and forecast_period within the cube, so that time coordinates do not become mis-represented. The units of the time and forecast_reference_time are enforced to be "seconds since 1970-01-01 00:00:00" with a datatype of int64. The units of forecast_period are enforced to be seconds with a datatype of int32. This functions modifies the cube in-place. Args: cube: The cube that will have the datatype and units for the time, forecast_reference_time and forecast_period coordinates enforced. Returns: Cube where the datatype and units for the time, forecast_reference_time and forecast_period coordinates have been enforced. """ for coord_name in [ "time", "forecast_reference_time", "forecast_period" ]: coord_spec = TIME_COORDS[coord_name] if cube.coords(coord_name): coord = cube.coord(coord_name) coord.convert_units(coord_spec.units) coord.points = round_close(coord.points, dtype=coord_spec.dtype) if hasattr(coord, "bounds") and coord.bounds is not None: coord.bounds = round_close(coord.bounds, dtype=coord_spec.dtype) return cube
def _get_cycletime_point(input_cube, cycletime): """ For cycle and model blending, establish the single forecast reference time to set on the cube after blending. Args: input_cube (iris.cube.Cube): Cube to be blended cycletime (str or None): The cycletime in a YYYYMMDDTHHMMZ format e.g. 20171122T0100Z. If None, the latest forecast reference time is used. Returns: numpy.int64: Forecast reference time point in units of input cube coordinate """ frt_coord = input_cube.coord("forecast_reference_time") if cycletime is None: return np.max(frt_coord.points) frt_units = frt_coord.units.origin frt_calendar = frt_coord.units.calendar cycletime_point = cycletime_to_number(cycletime, time_unit=frt_units, calendar=frt_calendar) return round_close(cycletime_point, dtype=np.int64)
def _create_frt_type_coord( cube: Cube, point: datetime, name: str = "forecast_reference_time" ) -> DimCoord: """Create a new auxiliary coordinate based on forecast reference time Args: cube: Input cube with scalar forecast reference time coordinate points Single datetime point for output coord name Name of aux coord to be returned Returns: New auxiliary coordinate """ frt_coord_name = "forecast_reference_time" coord_type_spec = TIME_COORDS[frt_coord_name] coord_units = Unit(coord_type_spec.units) new_points = round_close([coord_units.date2num(point)], dtype=coord_type_spec.dtype) try: new_coord = DimCoord(new_points, standard_name=name, units=coord_units) except ValueError: new_coord = DimCoord(new_points, long_name=name, units=coord_units) return new_coord
def _standardise_dtypes_and_units(cube): """ Modify input cube in place to conform to mandatory dtype and unit standards. Args: cube (iris.cube.Cube: Cube to be updated in place """ def as_correct_dtype(obj, required_dtype): """ Returns an object updated if necessary to the required dtype Args: obj (np.ndarray): The object to be updated required_dtype (np.dtype): The dtype required Returns: np.ndarray """ if obj.dtype != required_dtype: return obj.astype(required_dtype) return obj cube.data = as_correct_dtype(cube.data, get_required_dtype(cube)) for coord in cube.coords(): if coord.name() in TIME_COORDS and not check_units(coord): coord.convert_units(get_required_units(coord)) req_dtype = get_required_dtype(coord) # ensure points and bounds have the same dtype if np.issubdtype(req_dtype, np.integer): coord.points = round_close(coord.points) coord.points = as_correct_dtype(coord.points, req_dtype) if coord.has_bounds(): if np.issubdtype(req_dtype, np.integer): coord.bounds = round_close(coord.bounds) coord.bounds = as_correct_dtype(coord.bounds, req_dtype)
def _add_forecast_reference_time(input_time, advected_cube): """Add or replace a forecast reference time on the advected cube""" try: advected_cube.remove_coord("forecast_reference_time") except CoordinateNotFoundError: pass frt_coord_name = "forecast_reference_time" frt_coord_spec = TIME_COORDS[frt_coord_name] frt_coord = input_time.copy() frt_coord.rename(frt_coord_name) frt_coord.convert_units(frt_coord_spec.units) frt_coord.points = round_close(frt_coord.points, dtype=frt_coord_spec.dtype) advected_cube.add_aux_coord(frt_coord)
def _get_cycletime_point(self, cube): """ For cycle and model blending, establish the current cycletime to set on the cube after blending. Returns: numpy.int64: Cycle time point in units matching the input cube forecast reference time coordinate """ frt_coord = cube.coord("forecast_reference_time") frt_units = frt_coord.units.origin frt_calendar = frt_coord.units.calendar # raises TypeError if cycletime is None cycletime_point = cycletime_to_number( self.cycletime, time_unit=frt_units, calendar=frt_calendar ) return round_close(cycletime_point, dtype=np.int64)
def unify_cycletime(cubes, cycletime): """ Function to unify the forecast_reference_time and update forecast_period. The cycletime specified is used as the forecast_reference_time, and the forecast_period is recalculated using the time coordinate and updated forecast_reference_time. Args: cubes (iris.cube.CubeList or list of iris.cube.Cube): Cubes that will have their forecast_reference_time and forecast_period updated. Any bounds on the forecast_reference_time coordinate will be discarded. cycletime (datetime.datetime): Datetime for the cycletime that will be used to replace the forecast_reference_time on the individual cubes. Returns: iris.cube.CubeList: Updated cubes Raises: ValueError: if forecast_reference_time is a dimension coordinate """ result_cubes = iris.cube.CubeList([]) for cube in cubes: cube = cube.copy() frt_coord_name = "forecast_reference_time" coord_type_spec = TIME_COORDS[frt_coord_name] coord_units = Unit(coord_type_spec.units) frt_points = round_close([coord_units.date2num(cycletime)], dtype=coord_type_spec.dtype) frt_coord = cube.coord(frt_coord_name).copy(points=frt_points) cube.remove_coord(frt_coord_name) cube.add_aux_coord(frt_coord, data_dims=None) # Update the forecast period for consistency within each cube if cube.coords("forecast_period"): cube.remove_coord("forecast_period") fp_coord = forecast_period_coord(cube, force_lead_time_calculation=True) cube.add_aux_coord(fp_coord, data_dims=cube.coord_dims("time")) result_cubes.append(cube) return result_cubes
def _update_time(input_time, advected_cube, timestep): """Increment validity time on the advected cube Args: input_time (iris.coords.Coord): Time coordinate from source cube advected_cube (iris.cube.Cube): Cube containing advected data (modified in place) timestep (datetime.timedelta) Time difference between the advected output and the source """ original_datetime = next(input_time.cells())[0] new_datetime = original_datetime + timestep new_time = input_time.units.date2num(new_datetime) time_coord_name = "time" time_coord_spec = TIME_COORDS[time_coord_name] time_coord = advected_cube.coord(time_coord_name) time_coord.points = new_time time_coord.convert_units(time_coord_spec.units) time_coord.points = round_close(time_coord.points, dtype=time_coord_spec.dtype)
def _get_cycletime_point(cube: Cube, cycletime: str) -> int64: """ For cycle and model blending, establish the current cycletime to set on the cube after blending. Args: blended_cube cycletime: Current cycletime in YYYYMMDDTHHmmZ format Returns: Cycle time point in units matching the input cube forecast reference time coordinate """ frt_coord = cube.coord("forecast_reference_time") frt_units = frt_coord.units.origin frt_calendar = frt_coord.units.calendar # raises TypeError if cycletime is None cycletime_point = cycletime_to_number(cycletime, time_unit=frt_units, calendar=frt_calendar) return round_close(cycletime_point, dtype=np.int64)
def test_3d_spot_cube_for_time(self): """Test output with two extra dimensions, one of which is time with forecast_period as an auxiliary coordinate""" data = np.ones((3, 2, 4), dtype=np.float32) time_spec = TIME_COORDS["time"] time_units = Unit(time_spec.units) time_as_dt = [ datetime(2021, 12, 25, 12, 0), datetime(2021, 12, 25, 12, 1) ] time_points = round_close( np.array([time_units.date2num(t) for t in time_as_dt]), dtype=time_spec.dtype, ) time_coord = DimCoord(time_points, units=time_units, standard_name="time") fp_spec = TIME_COORDS["forecast_period"] fp_units = Unit(fp_spec.units) fp_points = np.array([0, 3600], dtype=fp_spec.dtype) fp_coord = AuxCoord(fp_points, units=fp_units, standard_name="forecast_period") result = build_spotdata_cube( data, *self.args, grid_attributes=self.grid_attributes, additional_dims=[time_coord], additional_dims_aux=[[fp_coord]], ) self.assertArrayAlmostEqual(result.data, data) self.assertEqual(result.coord_dims("grid_attributes")[0], 0) self.assertEqual(result.coord_dims("time")[0], 1) self.assertEqual(result.coord_dims("forecast_period")[0], 1)
def test_error_not_close(): """Test error when output would require significant rounding""" with pytest.raises(ValueError): round_close(29.9)
def test_round_close_array(): """Test near-integer output from array input""" expected = np.array([30, 4], dtype=int) result = round_close(np.array([29.999999, 4.0000001])) np.testing.assert_array_equal(result, expected)
def test_dtype(): """Test near-integer output with specific dtype""" result = round_close(29.99999, dtype=np.int32) assert result == 30 assert isinstance(result, np.int32)
def test_round_close(): """Test output when input is nearly an integer""" result = round_close(29.99999) assert result == 30 assert isinstance(result, np.int64)
def _calculate_forecast_period(time_coord, frt_coord, dim_coord=False, coord_spec=TIME_COORDS["forecast_period"]): """ Calculate a forecast period from existing time and forecast reference time coordinates. Args: time_coord (iris.coords.Coord): Time coordinate frt_coord (iris.coords.Coord): Forecast reference coordinate dim_coord (bool): If true, create an iris.coords.DimCoord instance. Default is to create an iris.coords.AuxCoord. coord_spec (collections.namedtuple): Specification of units and dtype for the forecast_period coordinate. Returns: iris.coords.Coord: Forecast period coordinate corresponding to the input times and forecast reference times specified Warns: UserWarning: If any calculated forecast periods are negative """ # use cell() access method to get datetime.datetime instances time_points = np.array([c.point for c in time_coord.cells()]) forecast_reference_time_points = np.array( [c.point for c in frt_coord.cells()]) required_lead_times = time_points - forecast_reference_time_points required_lead_times = np.array( [x.total_seconds() for x in required_lead_times]) if time_coord.bounds is not None: time_bounds = np.array([c.bound for c in time_coord.cells()]) required_lead_time_bounds = time_bounds - forecast_reference_time_points required_lead_time_bounds = np.array( [[b.total_seconds() for b in x] for x in required_lead_time_bounds]) else: required_lead_time_bounds = None coord_type = DimCoord if dim_coord else AuxCoord result_coord = coord_type( required_lead_times, standard_name="forecast_period", bounds=required_lead_time_bounds, units="seconds", ) result_coord.convert_units(coord_spec.units) if coord_spec.dtype not in FLOAT_TYPES: result_coord.points = round_close(result_coord.points) if result_coord.bounds is not None: result_coord.bounds = round_close(result_coord.bounds) result_coord.points = result_coord.points.astype(coord_spec.dtype) if result_coord.bounds is not None: result_coord.bounds = result_coord.bounds.astype(coord_spec.dtype) if np.any(result_coord.points < 0): msg = ("The values for the time {} and " "forecast_reference_time {} coordinates from the " "input cube have produced negative values for the " "forecast_period. A forecast does not generate " "values in the past.").format(time_coord.points, frt_coord.points) warnings.warn(msg) return result_coord