Source code for improver.nowcasting.pysteps_advection
# (C) Crown Copyright, Met Office. All rights reserved.
#
# This file is part of 'IMPROVER' and is released under the BSD 3-Clause license.
# See LICENSE in the root of the repository for full licensing details.
"""Semi-Lagrangian backward advection plugin using pysteps"""
from datetime import timedelta
from typing import Dict, List, Optional
import numpy as np
from iris.coords import AuxCoord
from iris.cube import Cube, CubeList
from numpy import ndarray
from improver import BasePlugin
from improver.metadata.amend import amend_attributes, set_history_attribute
from improver.metadata.utilities import generate_mandatory_attributes
from improver.nowcasting.utilities import ApplyOrographicEnhancement
from improver.utilities.redirect_stdout import redirect_stdout
from improver.utilities.spatial import (
calculate_grid_spacing,
check_if_grid_is_equal_area,
)
from improver.utilities.temporal import datetime_to_iris_time, iris_time_to_datetime
[docs]
class PystepsExtrapolate(BasePlugin):
"""Wrapper for the pysteps semi-Lagrangian extrapolation method
Reference:
https://pysteps.readthedocs.io/en/latest/generated/
pysteps.extrapolation.semilagrangian.extrapolate.html
"""
[docs]
def __init__(self, interval: int, max_lead_time: int) -> None:
"""
Initialise the plugin
Args:
interval:
Lead time interval, in minutes
max_lead_time:
Maximum lead time required, in minutes
"""
self.interval = interval
self.num_timesteps = max_lead_time // interval
[docs]
def _get_advectable_precip_rate(self) -> ndarray:
"""
From the initial cube, generate a precipitation rate array in mm h-1
with orographic enhancement subtracted, as required for advection
Returns:
2D precipitation rate array in mm h-1
"""
(self.analysis_cube,) = ApplyOrographicEnhancement("subtract").process(
self.analysis_cube, self.orogenh
)
self.analysis_cube.convert_units("mm h-1")
return np.ma.filled(self.analysis_cube.data, np.nan)
[docs]
def _generate_displacement_array(self, ucube: Cube, vcube: Cube) -> ndarray:
"""
Create displacement array of shape (2 x m x n) required by pysteps
algorithm
Args:
ucube:
Cube of x-advection velocities
vcube:
Cube of y-advection velocities
Returns:
Array of shape (2, m, n) containing the x- and y-components
of the m*n displacement field (format required for pysteps
extrapolation algorithm)
"""
def _calculate_displacement(
cube: Cube, interval: int, gridlength: float
) -> ndarray:
"""
Calculate displacement for each time step using velocity cube and
time interval
Args:
cube:
Cube of velocities in the x or y direction
interval:
Lead time interval, in minutes
gridlength:
Size of grid square, in metres
Returns:
Array of displacements in grid squares per time step
"""
cube_ms = cube.copy()
cube_ms.convert_units("m s-1")
displacement = cube_ms.data * interval * 60.0 / gridlength
return np.ma.filled(displacement, np.nan)
gridlength = calculate_grid_spacing(self.analysis_cube, "metres")
udisp = _calculate_displacement(ucube, self.interval, gridlength)
vdisp = _calculate_displacement(vcube, self.interval, gridlength)
displacement = np.array([udisp, vdisp])
return displacement
[docs]
def _reformat_analysis_cube(self, attribute_changes):
"""
Add forecast reference time and forecast period coordinates (if they do
not already exist) and nowcast attributes to analysis cube
"""
coords = [coord.name() for coord in self.analysis_cube.coords()]
if "forecast_reference_time" not in coords:
frt_coord = self.analysis_cube.coord("time").copy()
frt_coord.rename("forecast_reference_time")
self.analysis_cube.add_aux_coord(frt_coord)
if "forecast_period" not in coords:
self.analysis_cube.add_aux_coord(
AuxCoord(np.array([0], dtype=np.int32), "forecast_period", "seconds")
)
self.analysis_cube.attributes = generate_mandatory_attributes(
[self.analysis_cube]
)
self.analysis_cube.attributes["source"] = "MONOW"
self.analysis_cube.attributes["title"] = (
"MONOW Extrapolation Nowcast on UK 2 km Standard Grid"
)
set_history_attribute(self.analysis_cube, "Nowcast")
if attribute_changes is not None:
amend_attributes(self.analysis_cube, attribute_changes)
[docs]
def _set_up_output_cubes(self, all_forecasts: ndarray) -> CubeList:
"""
Convert 3D numpy array into list of cubes with correct time metadata.
All other metadata are inherited from self.analysis_cube.
Args:
all_forecasts:
Array of 2D forecast fields returned by extrapolation function
Returns:
List of extrapolated cubes with correct time coordinates
"""
current_datetime = iris_time_to_datetime(self.analysis_cube.coord("time"))[0]
forecast_cubes = [self.analysis_cube.copy()]
for i in range(len(all_forecasts)):
# copy forecast data into template cube
new_cube = self.analysis_cube.copy(
data=all_forecasts[i, :, :].astype(np.float32)
)
# update time and forecast period coordinates
current_datetime += timedelta(seconds=self.interval * 60)
current_time = datetime_to_iris_time(current_datetime)
new_cube.coord("time").points = np.array([current_time], dtype=np.int64)
new_cube.coord("forecast_period").points = np.array(
[(i + 1) * self.interval * 60], dtype=np.int32
)
forecast_cubes.append(new_cube)
return forecast_cubes
[docs]
def _generate_forecast_cubes(
self, all_forecasts: ndarray, attributes_dict: Optional[Dict]
) -> List[Cube]:
"""
Convert forecast arrays into IMPROVER output cubes with re-added
orographic enhancement
Args:
all_forecasts:
Array of 2D forecast fields returned by extrapolation function
attributes_dict:
Dictionary containing information for amending the attributes
of the output cube.
Returns:
List of iris.cube.Cube instances containing forecasts at all
required lead times, and conforming to the IMPROVER metadata
standard.
"""
# re-mask forecast data
all_forecasts = np.ma.masked_invalid(all_forecasts)
# put forecast data arrays into cubes
self._reformat_analysis_cube(attributes_dict)
timestamped_cubes = self._set_up_output_cubes(all_forecasts)
# re-convert cubes to original units and add orographic enhancement
forecast_cubes = []
for cube in timestamped_cubes:
cube.convert_units(self.required_units)
if self.orogenh:
(cube,) = ApplyOrographicEnhancement("add").process(cube, self.orogenh)
forecast_cubes.append(cube)
return forecast_cubes
[docs]
def process(
self,
initial_cube: Cube,
ucube: Cube,
vcube: Cube,
orographic_enhancement: Cube,
attributes_dict: Optional[Dict] = None,
) -> List[Cube]:
"""
Extrapolate the initial precipitation field using the velocities
provided to the required forecast lead times
Args:
initial_cube:
Cube of precipitation at initial time
ucube:
x-advection velocities
vcube:
y-advection velocities
orographic_enhancement:
Cube containing orographic enhancement fields at all required
lead times
attributes_dict:
Dictionary containing information for amending the attributes
of the output cube.
Returns:
List of extrapolated iris.cube.Cube instances at the required
lead times (including T+0 / analysis time)
"""
# ensure input cube is suitable for advection
if "rate" not in initial_cube.name():
msg = "{} is not a precipitation rate cube"
raise ValueError(msg.format(initial_cube.name()))
check_if_grid_is_equal_area(initial_cube)
self.analysis_cube = initial_cube.copy()
self.required_units = initial_cube.units
self.orogenh = orographic_enhancement
# get unmasked precipitation rate array with orographic enhancement
# subtracted to input into advection
precip_rate = self._get_advectable_precip_rate()
# calculate displacement in grid squares per time step
displacement = self._generate_displacement_array(ucube, vcube)
# PySteps prints a message on import to stdout - trap this
# This should be removed for PySteps v1.1.0 which has a configuration setting
# for this
# Import here to minimise dependencies
with redirect_stdout():
from pysteps.extrapolation.semilagrangian import extrapolate
# call pysteps extrapolation method; using interp_order=0 which is
# nearest neighbour
all_forecasts = extrapolate(
precip_rate,
displacement,
self.num_timesteps,
allow_nonfinite_values=True,
interp_order=0,
)
# repackage data as IMPROVER masked cubes
forecast_cubes = self._generate_forecast_cubes(all_forecasts, attributes_dict)
return forecast_cubes