"""
This file is part of CLIMADA.
Copyright (C) 2017 ETH Zurich, CLIMADA contributors listed in AUTHORS.
CLIMADA is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free
Software Foundation, version 3.
CLIMADA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with CLIMADA. If not, see <https://www.gnu.org/licenses/>.
---
Define TC rain hazard (TCRain class).
"""
__all__ = ['TCRain']
import datetime as dt
import itertools
import logging
from pathlib import Path
from typing import Optional, Tuple, List, Union
import numpy as np
import pathos.pools
from scipy import sparse
import xarray as xr
from climada.hazard import Hazard, TCTracks, TropCyclone, Centroids
from climada.hazard.trop_cyclone import (
get_close_centroids,
compute_angular_windspeeds,
tctrack_to_si,
H_TO_S,
KM_TO_M,
KN_TO_MS,
MODEL_VANG,
)
from climada.util import ureg
from climada.util.api_client import Client
import climada.util.constants as u_const
import climada.util.coordinates as u_coord
LOGGER = logging.getLogger(__name__)
HAZ_TYPE = 'TR'
"""Hazard type acronym for TC rain"""
DEF_MAX_DIST_EYE_KM = 300
"""Default value for the maximum distance (in km) of a centroid to the TC center at which rain
rate calculations are done."""
DEF_INTENSITY_THRES = 0.1
"""Default value for the threshold below which rain amounts (in mm) are stored as 0."""
DEF_MAX_MEMORY_GB = 8
"""Default value of the memory limit (in GB) for rain computations (in each thread)."""
MODEL_RAIN = {'R-CLIPER': 0, 'TCR': 1}
"""Enumerate different parametric TC rain models."""
D_TO_H = (1.0 * ureg.days).to(ureg.hours).magnitude
IN_TO_MM = (1.0 * ureg.inches).to(ureg.millimeters).magnitude
M_TO_MM = (1.0 * ureg.meter).to(ureg.millimeter).magnitude
"""Unit conversion factors for JIT functions that can't use ureg"""
H_TROP = 4000
"""Depth (in m) of lower troposphere"""
DELTA_T_TROPOPAUSE = 100
"""Difference between surface and tropopause temperature (in K): T_s - T_t"""
T_ICE_K = 273.16
"""Freezing temperatur of water (in K), for conversion between K and °C"""
L_EVAP_WATER = 2.5e6
"""Latent heat of the evaporation of water (in J/kg)"""
M_WATER = 18.01528
"""Molar mass of water vapor (in g/mol)"""
M_DRY_AIR = 28.9634
"""Molar mass of dry air (in g/mol)"""
R_GAS = 8.3144621
"""Molar gas constant (in J/molK)"""
R_DRY_AIR = 1000 * R_GAS / M_DRY_AIR
"""Specific gas constant of dry air (in J/kgK)"""
RHO_A_OVER_RHO_L = 0.00117
"""Density of water vapor divided by density of liquid water"""
GRADIENT_LEVEL_TO_SURFACE_WINDS = 0.8
"""Gradient-to-surface wind reduction factor according to Table 2 in:
Franklin, J.L., Black, M.L., Valde, K. (2003): GPS Dropwindsonde Wind Profiles in Hurricanes and
Their Operational Implications. Weather and Forecasting 18(1): 32–44.
https://doi.org/10.1175/1520-0434(2003)018<0032:GDWPIH>2.0.CO;2
Note that we here use a value different from the one in `climada.hazard.trop_cyclone` because the
focus is not only on the eyewall region, but also on the outer vortex, which is a little more
important for precipitation than for wind effects.
"""
def default_elevation_tif():
"""Topography (land surface elevation, 0 over oceans) raster data at 0.1 degree resolution
SRTM data upscaled to 0.1 degree resolution using the "average" method of gdalwarp.
"""
client = Client()
dsi = client.get_dataset_info(name='topography_land_360as', status='package-data')
_, [elevation_tif] = client.download_dataset(dsi)
return elevation_tif
def default_drag_tif():
"""Gradient-level drag coefficient raster data at 0.25 degree resolution
The ERA5 'forecast_surface_roughness' (fsr) variable has been transformed into drag
coefficients (C_D) following eqs. (7) and (8) in the following work:
Feldmann et al. (2019): Estimation of Atlantic Tropical Cyclone Rainfall Frequency in the
United States. Journal of Applied Meteorology and Climatology 58(8): 1853–1866.
https://doi.org/10.1175/JAMC-D-19-0011.1
"""
client = Client()
dsi = client.get_dataset_info(name='c_drag_500', status='package-data')
_, [drag_tif] = client.download_dataset(dsi)
return drag_tif
[docs]
class TCRain(Hazard):
"""
Contains rainfall from tropical cyclone events.
Attributes
----------
category : np.ndarray of ints
for every event, the TC category using the Saffir-Simpson scale:
* -1 tropical depression
* 0 tropical storm
* 1 Hurrican category 1
* 2 Hurrican category 2
* 3 Hurrican category 3
* 4 Hurrican category 4
* 5 Hurrican category 5
basin : list of str
Basin where every event starts:
* 'NA' North Atlantic
* 'EP' Eastern North Pacific
* 'WP' Western North Pacific
* 'NI' North Indian
* 'SI' South Indian
* 'SP' Southern Pacific
* 'SA' South Atlantic
rainrates : list of csr_matrix
For each event, the rain rates (in mm/h) at each centroid and track position in a sparse
matrix of shape (npositions, ncentroids).
"""
intensity_thres = DEF_INTENSITY_THRES
"""intensity threshold for storage in mm"""
vars_opt = Hazard.vars_opt.union({'category'})
"""Name of the variables that aren't needed to compute the impact."""
[docs]
def __init__(
self,
category: Optional[np.ndarray] = None,
basin: Optional[List] = None,
rainrates: Optional[List[sparse.csr_matrix]] = None,
**kwargs,
):
"""Initialize values.
Parameters
----------
category : np.ndarray of int, optional
For every event, the TC category using the Saffir-Simpson scale:
-1 tropical depression
0 tropical storm
1 Hurrican category 1
2 Hurrican category 2
3 Hurrican category 3
4 Hurrican category 4
5 Hurrican category 5
basin : list of str, optional
Basin where every event starts:
'NA' North Atlantic
'EP' Eastern North Pacific
'WP' Western North Pacific
'NI' North Indian
'SI' South Indian
'SP' Southern Pacific
'SA' South Atlantic
rainrates : list of csr_matrix, optional
For each event, the rain rates (in mm/h) at each centroid and track position in a
sparse matrix of shape (npositions, ncentroids).
**kwargs : Hazard properties, optional
All other keyword arguments are passed to the Hazard constructor.
"""
kwargs.setdefault('haz_type', HAZ_TYPE)
Hazard.__init__(self, **kwargs)
self.category = category if category is not None else np.array([], int)
self.basin = basin if basin is not None else []
self.rainrates = rainrates if rainrates is not None else []
[docs]
def set_from_tracks(self, *args, **kwargs):
"""This function is deprecated, use TCRain.from_tracks instead."""
LOGGER.warning("The use of TCRain.set_from_tracks is deprecated."
"Use TCRain.from_tracks instead.")
if "intensity_thres" not in kwargs:
# some users modify the threshold attribute before calling `set_from_tracks`
kwargs["intensity_thres"] = self.intensity_thres
if self.pool is not None and 'pool' not in kwargs:
kwargs['pool'] = self.pool
self.__dict__ = TCRain.from_tracks(*args, **kwargs).__dict__
[docs]
@classmethod
def from_tracks(
cls,
tracks: TCTracks,
centroids: Optional[Centroids] = None,
pool: Optional[pathos.pools.ProcessPool] = None,
model: str = 'R-CLIPER',
model_kwargs: Optional[dict] = None,
ignore_distance_to_coast: bool = False,
store_rainrates: bool = False,
metric: str = "equirect",
intensity_thres: float = DEF_INTENSITY_THRES,
max_latitude: float = 61,
max_dist_inland_km: float = 1000,
max_dist_eye_km: float = DEF_MAX_DIST_EYE_KM,
max_memory_gb: float = DEF_MAX_MEMORY_GB,
):
"""
Create new TCRain instance that contains rainfields from the specified tracks
This function sets the `intensity` attribute to contain, for each centroid,
the total amount of rain experienced over the whole period of each TC event in mm.
The amount of rain is set to 0 if it does not exceed the threshold `intensity_thres`.
The `category` attribute is set to the value of the `category`-attribute
of each of the given track data sets.
The `basin` attribute is set to the genesis basin for each event, which
is the first value of the `basin`-variable in each of the given track data sets.
Optionally, the time-dependent rain rates can be stored using the `store_rainrates`
function parameter (see below).
Currently, two models are supported to compute the rain rates: R-CLIPER and TCR. The
R-CLIPER model is documented in Tuleya et al. 2007. The TCR model was used by
Zhu et al. 2013 and Emanuel 2017 for the first time and is documented in detail in
Lu et al. 2018. This implementation of TCR includes improvements proposed in
Feldmann et al. 2019. TCR's accuracy is much higher than R-CLIPER's at the cost of
additional computational and data requirements.
When using the TCR model make sure that your TC track data includes the along-track
variables "t600" (temperature at 600 hPa) and "u850"/"v850" (wind speed at 850 hPa). Both
can be extracted from reanalysis or climate model outputs. For "t600", use the value at the
storm center. For "u850"/"v850", use the average over the 200-500 km annulus around the
storm center. If "u850"/"v850" is missing, this implementation sets the shear component of
the vertical velocity to 0. If "t600" is missing, the saturation specific humidity is set
to a universal estimate of 0.01 kg/kg. Both assumptions can have a large effect on the
results (see Lu et al. 2018).
Emanuel (2017): Assessing the present and future probability of Hurricane Harvey’s
rainfall. Proceedings of the National Academy of Sciences 114(48): 12681–12684.
https://doi.org/10.1073/pnas.1716222114
Lu et al. (2018): Assessing Hurricane Rainfall Mechanisms Using a Physics-Based Model:
Hurricanes Isabel (2003) and Irene (2011). Journal of the Atmospheric
Sciences 75(7): 2337–2358. https://doi.org/10.1175/JAS-D-17-0264.1
Feldmann et al. (2019): Estimation of Atlantic Tropical Cyclone Rainfall Frequency in the
United States. Journal of Applied Meteorology and Climatology 58(8): 1853–1866.
https://doi.org/10.1175/JAMC-D-19-0011.1
Tuleya et al. (2007): Evaluation of GFDL and Simple Statistical Model Rainfall Forecasts
for U.S. Landfalling Tropical Storms. Weather and Forecasting 22(1): 56–70.
https://doi.org/10.1175/WAF972.1
Zhu et al. (2013): Estimating tropical cyclone precipitation risk in Texas. Geophysical
Research Letters 40(23): 6225–6230. https://doi.org/10.1002/2013GL058284
Parameters
----------
tracks : climada.hazard.TCTracks
Tracks of storm events.
centroids : Centroids, optional
Centroids where to model TC. Default: global centroids at 360 arc-seconds resolution.
pool : pathos.pool, optional
Pool that will be used for parallel computation of rain fields. Default: None
model : str, optional
Parametric rain model to use: "R-CLIPER" (faster and requires less inputs, but
much less accurate, statistical approach, Tuleya et al. 2007), "TCR" (physics-based
approach, requires non-standard along-track variables, Zhu et al. 2013).
Default: "R-CLIPER".
model_kwargs: dict, optional
If given, forward these kwargs to the selected model. The implementation of the
R-CLIPER model currently does not allow modifications, so that `model_kwargs` is
ignored with `model="R-CLIPER"`. While the TCR model can be configured in several ways,
it is usually safe to go with the default settings. Here is the complete list of
`model_kwargs` and their meaning with `model="TCR"` (in alphabetical order):
c_drag_tif : Path or str, optional
Path to a GeoTIFF file containing gridded drag coefficients (bottom friction). If
not specified, an ERA5-based data set provided with CLIMADA is used. Default: None
e_precip : float, optional
Precipitation efficiency (unitless), the fraction of the vapor flux falling to the
surface as rainfall (Lu et al. 2018, eq. (14)). Note that we follow the MATLAB
reference implementation and use 0.5 as a default value instead of the 0.9 that was
proposed in Lu et al. 2018. Default: 0.5
elevation_tif : Path or str, optional
Path to a GeoTIFF file containing digital elevation model data (in m). If not
specified, an SRTM-based topography at 0.1 degree resolution provided with CLIMADA
is used. Default: None
matlab_ref_mode : bool, optional
This implementation is based on a (proprietary) reference implementation in MATLAB.
However, some (minor) changes have been applied in the CLIMADA implementation
compared to the reference:
* In the computation of horizontal wind speeds, we compute the Coriolis parameter
from latitude. The MATLAB code assumes a constant parameter value (5e-5).
* As a rescaling factor from surface to gradient winds, we use a factor from the
literature. The factor in MATLAB is very similar, but does not specify a
source.
* Instead of the "specific humidity", the (somewhat simpler) formula for the
"mixing ratio" is used in the MATLAB code. These quantities are almost the same
in practice.
* We use the approximation of the Clausius-Clapeyron equation used by the ECMWF
(Buck 1981) instead of the one used in the MATLAB code (Bolton 1980).
Since it might be useful to have a version that replicates the behavior of the
reference implementation, this parameter can be set to True to enforce the exact
behavior of the reference implementation. Default: False
max_w_foreground : float, optional
The maximum value (in m/s) at which to clip the vertical velocity w before
subtracting the background subsidence velocity w_rad. Default: 7.0
min_c_drag : float, optional
The drag coefficient is clipped to this minimum value (esp. over ocean).
Default: 0.001
q_950 : float, optional
If the track data does not include "t600" values, assume this constant value of
saturation specific humidity (in kg/kg) at 950 hPa. Default: 0.01
res_radial_m : float, optional
Resolution (in m) in radial direction. This is used for the computation of discrete
derivatives of the horizontal wind fields and derived quantities. Default: 2000.0
w_rad : float, optional
Background subsidence velocity (in m/s) under radiative cooling. Default: 0.005
wind_model : str, optional
Parametric wind field model to use, see the `TropCyclone` class. Default: "ER11".
Default: None
ignore_distance_to_coast : boolean, optional
If True, centroids far from coast are not ignored. Default: False.
store_rainrates : boolean, optional
If True, the Hazard object gets a list `rainrates` of sparse matrices. For each track,
the rain rates (in mm/h) at each centroid and track position are stored in a sparse
matrix of shape (npositions, ncentroids). Default: False.
metric : str, optional
Specify an approximation method to use for earth distances:
* "equirect": Distance according to sinusoidal projection. Fast, but inaccurate for
large distances and high latitudes.
* "geosphere": Exact spherical distance. Much more accurate at all distances, but slow.
Default: "equirect".
intensity_thres : float, optional
Rain amounts (in mm) below this threshold are stored as 0. Default: 0.1
max_latitude : float, optional
No rain calculation is done for centroids with latitude larger than this parameter.
Default: 61
max_dist_inland_km : float, optional
No rain calculation is done for centroids with a distance (in km) to the coast larger
than this parameter. Default: 1000
max_dist_eye_km : float, optional
No rain calculation is done for centroids with a distance (in km) to the
TC center ("eye") larger than this parameter. Default: 300
max_memory_gb : float, optional
To avoid memory issues, the computation is done for chunks of the track sequentially.
The chunk size is determined depending on the available memory (in GB). Note that this
limit applies to each thread separately if a `pool` is used. Default: 8
Returns
-------
TCRain
"""
num_tracks = tracks.size
if centroids is None:
centroids = Centroids.from_base_grid(res_as=360, land=True)
if not centroids.coord.size:
centroids.set_meta_to_lat_lon()
if ignore_distance_to_coast:
# Select centroids with lat <= max_latitude
coastal_idx = (np.abs(centroids.lat) <= max_latitude).nonzero()[0]
else:
# Select centroids which are inside max_dist_inland_km and lat <= max_latitude
if not centroids.dist_coast.size:
centroids.set_dist_coast()
coastal_idx = ((centroids.dist_coast <= max_dist_inland_km * 1000)
& (np.abs(centroids.lat) <= max_latitude)).nonzero()[0]
# Filter early with a larger threshold, but inaccurate (lat/lon) distances.
# Later, there will be another filtering step with more accurate distances in km.
max_dist_eye_deg = max_dist_eye_km / (
u_const.ONE_LAT_KM * np.cos(np.radians(max_latitude))
)
# Restrict to coastal centroids within reach of any of the tracks
t_lon_min, t_lat_min, t_lon_max, t_lat_max = tracks.get_bounds(deg_buffer=max_dist_eye_deg)
t_mid_lon = 0.5 * (t_lon_min + t_lon_max)
coastal_centroids = centroids.coord[coastal_idx]
u_coord.lon_normalize(coastal_centroids[:, 1], center=t_mid_lon)
coastal_idx = coastal_idx[((t_lon_min <= coastal_centroids[:, 1])
& (coastal_centroids[:, 1] <= t_lon_max)
& (t_lat_min <= coastal_centroids[:, 0])
& (coastal_centroids[:, 0] <= t_lat_max))]
LOGGER.info('Mapping %s tracks to %s coastal centroids.', str(tracks.size),
str(coastal_idx.size))
if pool:
chunksize = max(min(num_tracks // pool.ncpus, 1000), 1)
tc_haz_list = pool.map(
cls._from_track, tracks.data,
itertools.repeat(centroids, num_tracks),
itertools.repeat(coastal_idx, num_tracks),
itertools.repeat(model, num_tracks),
itertools.repeat(model_kwargs, num_tracks),
itertools.repeat(store_rainrates, num_tracks),
itertools.repeat(metric, num_tracks),
itertools.repeat(intensity_thres, num_tracks),
itertools.repeat(max_dist_eye_km, num_tracks),
itertools.repeat(max_memory_gb, num_tracks),
chunksize=chunksize)
else:
last_perc = 0
tc_haz_list = []
for track in tracks.data:
perc = 100 * len(tc_haz_list) / len(tracks.data)
if perc - last_perc >= 10:
LOGGER.info("Progress: %d%%", perc)
last_perc = perc
tc_haz_list.append(
cls._from_track(track, centroids, coastal_idx,
model=model, model_kwargs=model_kwargs,
store_rainrates=store_rainrates,
metric=metric, intensity_thres=intensity_thres,
max_dist_eye_km=max_dist_eye_km,
max_memory_gb=max_memory_gb))
if last_perc < 100:
LOGGER.info("Progress: 100%")
LOGGER.debug('Concatenate events.')
haz = cls.concat(tc_haz_list)
haz.pool = pool
haz.intensity_thres = intensity_thres
LOGGER.debug('Compute frequency.')
TropCyclone.frequency_from_tracks(haz, tracks.data)
return haz
@classmethod
def _from_track(
cls,
track: xr.Dataset,
centroids: Centroids,
coastal_idx: np.ndarray,
model: str = 'R-CLIPER',
model_kwargs: Optional[dict] = None,
store_rainrates: bool = False,
metric: str = "equirect",
intensity_thres: float = DEF_INTENSITY_THRES,
max_dist_eye_km: float = DEF_MAX_DIST_EYE_KM,
max_memory_gb: float = DEF_MAX_MEMORY_GB,
):
"""
Generate a TC rain hazard object from a single track dataset
Parameters
----------
track : xr.Dataset
Single tropical cyclone track.
centroids : Centroids
Centroids instance.
coastal_idx : np.ndarray
Indices of centroids close to coast.
model : str, optional
Parametric rain model to use: "R-CLIPER" (faster and requires less inputs, but
much less accurate, statistical approach), "TCR" (physics-based approach, requires
non-standard along-track variables). Default: "R-CLIPER".
model_kwargs: dict, optional
If given, forward these kwargs to the selected model. Default: None
store_rainrates : boolean, optional
If True, store rain rates (in mm/h). Default: False.
metric : str, optional
Specify an approximation method to use for earth distances: "equirect" (faster) or
"geosphere" (more accurate). See `dist_approx` function in `climada.util.coordinates`.
Default: "equirect".
intensity_thres : float, optional
Rain amounts (in mm) below this threshold are stored as 0. Default: 0.1
max_dist_eye_km : float, optional
No rain calculation is done for centroids with a distance (in km) to the TC
center ("eye") larger than this parameter. Default: 300
max_memory_gb : float, optional
To avoid memory issues, the computation is done for chunks of the track sequentially.
The chunk size is determined depending on the available memory (in GB). Default: 8
Returns
-------
TCRain
"""
intensity_sparse, rainrates_sparse = _compute_rain_sparse(
track=track,
centroids=centroids,
coastal_idx=coastal_idx,
model=model,
model_kwargs=model_kwargs,
store_rainrates=store_rainrates,
metric=metric,
intensity_thres=intensity_thres,
max_dist_eye_km=max_dist_eye_km,
max_memory_gb=max_memory_gb,
)
new_haz = cls(haz_type=HAZ_TYPE)
new_haz.intensity_thres = intensity_thres
new_haz.intensity = intensity_sparse
if store_rainrates:
new_haz.rainrates = [rainrates_sparse]
new_haz.units = 'mm'
new_haz.centroids = centroids
new_haz.event_id = np.array([1])
new_haz.frequency = np.array([1])
new_haz.event_name = [track.sid]
new_haz.fraction = sparse.csr_matrix(new_haz.intensity.shape)
# store first day of track as date
new_haz.date = np.array([dt.datetime(
track["time"].dt.year.values[0],
track["time"].dt.month.values[0],
track["time"].dt.day.values[0]
).toordinal()])
new_haz.orig = np.array([track.orig_event_flag])
new_haz.category = np.array([track.category])
new_haz.basin = [str(track["basin"].values[0])]
return new_haz
def _compute_rain_sparse(
track: xr.Dataset,
centroids: Centroids,
coastal_idx: np.ndarray,
model: str = 'R-CLIPER',
model_kwargs: Optional[dict] = None,
store_rainrates: bool = False,
metric: str = "equirect",
intensity_thres: float = DEF_INTENSITY_THRES,
max_dist_eye_km: float = DEF_MAX_DIST_EYE_KM,
max_memory_gb: float = DEF_MAX_MEMORY_GB,
) -> Tuple[sparse.csr_matrix, Optional[sparse.csr_matrix]]:
"""Version of `compute_rain` that returns sparse matrices and limits memory usage
Parameters
----------
track : xr.Dataset
Single tropical cyclone track.
centroids : Centroids
Centroids instance.
coastal_idx : np.ndarray
Indices of centroids close to coast.
model : str, optional
Parametric rain model to use: "R-CLIPER" (faster and requires less inputs, but
much less accurate, statistical approach), "TCR" (physics-based approach, requires
non-standard along-track variables). Default: "R-CLIPER".
model_kwargs: dict, optional
If given, forward these kwargs to the selected model. Default: None
store_rainrates : boolean, optional
If True, store rain rates. Default: False.
metric : str, optional
Specify an approximation method to use for earth distances: "equirect" (faster) or
"geosphere" (more accurate). See `dist_approx` function in `climada.util.coordinates`.
Default: "equirect".
intensity_thres : float, optional
Wind speeds (in m/s) below this threshold are stored as 0. Default: 17.5
max_dist_eye_km : float, optional
No rain calculation is done for centroids with a distance (in km) to the TC
center ("eye") larger than this parameter. Default: 300
max_memory_gb : float, optional
To avoid memory issues, the computation is done for chunks of the track sequentially.
The chunk size is determined depending on the available memory (in GB). Default: 8
Raises
------
ValueError
Returns
-------
intensity : csr_matrix
Total amount of rain (in mm) in each centroid over the whole storm life time.
rainrates : csr_matrix or None
If store_rainrates is True, the rain rates at each centroid and track position
are stored in a sparse matrix of shape (npositions, ncentroids ).
If store_rainrates is False, `None` is returned.
"""
model_kwargs = {} if model_kwargs is None else model_kwargs
try:
mod_id = MODEL_RAIN[model]
except KeyError as err:
raise ValueError(f'Model not implemented: {model}.') from err
ncentroids = centroids.coord.shape[0]
coastal_centr = centroids.coord[coastal_idx]
npositions = track.sizes["time"]
rainrates_shape = (npositions, ncentroids)
intensity_shape = (1, ncentroids)
# start with the assumption that no centroids are within reach
rainrates_sparse = (
sparse.csr_matrix(([], ([], [])), shape=rainrates_shape)
if store_rainrates else None
)
intensity_sparse = sparse.csr_matrix(([], ([], [])), shape=intensity_shape)
# The TCR model requires at least three track positions because both forward and backward
# differences in time are used.
if npositions == 0 or model == "TCR" and npositions < 3:
return intensity_sparse, rainrates_sparse
# convert track variables to SI units
si_track = _track_to_si_with_q_and_shear(track, metric=metric, **model_kwargs)
t_lat, t_lon = si_track["lat"].values, si_track["lon"].values
# normalize longitudinal coordinates of centroids
u_coord.lon_normalize(coastal_centr[:, 1], center=si_track.attrs["mid_lon"])
# Restrict to the bounding box of the whole track first (this can already reduce the number of
# centroids that are considered by a factor larger than 30).
max_dist_eye_lat = max_dist_eye_km / u_const.ONE_LAT_KM
max_dist_eye_lon = max_dist_eye_km / (
u_const.ONE_LAT_KM * np.cos(np.radians(
np.fmin(89.999, np.abs(coastal_centr[:, 0]) + max_dist_eye_lat)
))
)
coastal_idx = coastal_idx[
(t_lat.min() - coastal_centr[:, 0] <= max_dist_eye_lat)
& (coastal_centr[:, 0] - t_lat.max() <= max_dist_eye_lat)
& (t_lon.min() - coastal_centr[:, 1] <= max_dist_eye_lon)
& (coastal_centr[:, 1] - t_lon.max() <= max_dist_eye_lon)
]
coastal_centr = centroids.coord[coastal_idx]
# restrict to centroids within rectangular bounding boxes around track positions
track_centr_msk = get_close_centroids(
t_lat, t_lon, coastal_centr, max_dist_eye_km, metric=metric,
)
coastal_idx = coastal_idx[track_centr_msk.any(axis=0)]
coastal_centr = centroids.coord[coastal_idx]
nreachable = coastal_centr.shape[0]
if nreachable == 0:
return intensity_sparse, rainrates_sparse
# the total memory requirement in GB if we compute everything without chunking:
# 8 Bytes per entry (float64), 25 arrays
total_memory_gb = npositions * nreachable * 8 * 25 / 1e9
if total_memory_gb > max_memory_gb and npositions > 3:
# If the number of positions is down to 3 already, we do not split any further. In that
# case, we just take the risk and try to do the computation anyway. It might still work
# since we have only computed an upper bound for the number of affected centroids.
# Split the track into chunks, compute the result for each chunk, and combine:
return _compute_rain_sparse_chunked(
track_centr_msk,
track,
centroids,
coastal_idx,
model=model,
model_kwargs=model_kwargs,
store_rainrates=store_rainrates,
metric=metric,
intensity_thres=intensity_thres,
max_dist_eye_km=max_dist_eye_km,
max_memory_gb=max_memory_gb,
)
rainrates, reachable_centr_idx = compute_rain(
si_track,
coastal_centr,
mod_id,
model_kwargs=model_kwargs,
metric=metric,
max_dist_eye_km=max_dist_eye_km,
)
reachable_coastal_centr_idx = coastal_idx[reachable_centr_idx]
npositions = rainrates.shape[0]
# obtain total rainfall in mm by multiplying by time step size (in hours) and summing up
intensity = (rainrates * track["time_step"].values[:, None]).sum(axis=0)
intensity[intensity < intensity_thres] = 0
intensity_sparse = sparse.csr_matrix(
(intensity, reachable_coastal_centr_idx, [0, intensity.size]),
shape=intensity_shape)
intensity_sparse.eliminate_zeros()
rainrates_sparse = None
if store_rainrates:
n_reachable_coastal_centr = reachable_coastal_centr_idx.size
indices = np.broadcast_to(
reachable_coastal_centr_idx[None],
(npositions, n_reachable_coastal_centr),
).ravel()
indptr = np.arange(npositions + 1) * n_reachable_coastal_centr
rainrates_sparse = sparse.csr_matrix((rainrates.ravel(), indices, indptr),
shape=rainrates_shape)
rainrates_sparse.eliminate_zeros()
return intensity_sparse, rainrates_sparse
def _compute_rain_sparse_chunked(
track_centr_msk: np.ndarray,
track: xr.Dataset,
*args,
max_memory_gb: float = DEF_MAX_MEMORY_GB,
**kwargs,
) -> Tuple[sparse.csr_matrix, Optional[sparse.csr_matrix]]:
"""Call `_compute_rain_sparse` for chunks of the track and re-assemble the results
Parameters
----------
track_centr_msk : np.ndarray
Each row is a mask that indicates the centroids within reach for one track position.
track : xr.Dataset
Single tropical cyclone track.
max_memory_gb : float, optional
Maximum memory requirements (in GB) for the computation of a single chunk of the track.
Default: 8
args, kwargs :
The remaining arguments are passed on to `_compute_rain_sparse`.
Returns
-------
intensity, rainrates :
See `_compute_rain_sparse` for a description of the return values.
"""
npositions = track.sizes["time"]
# The memory requirements for each track position are estimated for the case of 25 arrays
# containing `nreachable` float64 (8 Byte) values each. The chunking is only relevant in
# extreme cases with a very high temporal and/or spatial resolution.
max_nreachable = max_memory_gb * 1e9 / (8 * 25 * npositions)
split_pos = [0]
chunk_size = 3
while split_pos[-1] + chunk_size < npositions:
chunk_size += 1
# create overlap between consecutive chunks
chunk_start = max(0, split_pos[-1] - 2)
chunk_end = chunk_start + chunk_size
nreachable = track_centr_msk[chunk_start:chunk_end].any(axis=0).sum()
if nreachable > max_nreachable:
split_pos.append(chunk_end - 1)
chunk_size = 3
split_pos.append(npositions)
intensity = []
rainrates = []
for prev_chunk_end, chunk_end in zip(split_pos[:-1], split_pos[1:]):
chunk_start = max(0, prev_chunk_end - 2)
inten, rainr = _compute_rain_sparse(
track.isel(time=slice(chunk_start, chunk_end)), *args,
max_memory_gb=max_memory_gb, **kwargs,
)
intensity.append(inten)
rainrates.append(rainr)
intensity = sparse.csr_matrix(sparse.vstack(intensity).max(axis=0))
if rainrates[0] is not None:
# eliminate the overlap between consecutive chunks
rainrates = (
[rainrates[0][:-1, :]]
+ [rainr[1:-1, :] for rainr in rainrates[1:-1]]
+ [rainrates[-1][1:, :]]
)
rainrates = sparse.vstack(rainrates, format="csr")
return intensity, rainrates
def compute_rain(
si_track: xr.Dataset,
centroids: np.ndarray,
model: int,
model_kwargs: Optional[dict] = None,
metric: str = "equirect",
max_dist_eye_km: float = DEF_MAX_DIST_EYE_KM,
) -> Tuple[np.ndarray, np.ndarray]:
"""Compute rain rate (in mm/h) of the tropical cyclone
In a first step, centroids within reach of the track are determined so that rain rates will
only be computed and returned for those centroids. Still, since computing the distance of
the storm center to the centroids is computationally expensive, make sure to pre-filter the
centroids and call this function only for those centroids that are potentially affected.
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Which data variables are used in the computation of the rain
rates depends on the selected model.
centroids : np.ndarray with two dimensions
Each row is a centroid [lat, lon].
Centroids that are not within reach of the track are ignored.
model : int
TC rain model selection according to MODEL_RAIN.
model_kwargs: dict, optional
If given, forward these kwargs to the selected model. Default: None
metric : str, optional
Specify an approximation method to use for earth distances: "equirect" (faster) or
"geosphere" (more accurate). See `dist_approx` function in `climada.util.coordinates`.
Default: "equirect".
max_dist_eye_km : float, optional
No rain calculation is done for centroids with a distance (in km) to the TC center
("eye") larger than this parameter. Default: 300
Returns
-------
rainrates : np.ndarray of shape (npositions, nreachable)
Rain rates for each track position on those centroids within reach of the TC track.
reachable_centr_idx : np.ndarray of shape (nreachable,)
List of indices of input centroids within reach of the TC track.
"""
model_kwargs = {} if model_kwargs is None else model_kwargs
# start with the assumption that no centroids are within reach
npositions = si_track.sizes["time"]
reachable_centr_idx = np.zeros((0,), dtype=np.int64)
rainrates = np.zeros((npositions, 0), dtype=np.float64)
# exclude centroids that are too far from or too close to the eye
d_centr = _centr_distances(si_track, centroids, metric=metric, **model_kwargs)
close_centr_msk = (d_centr[""] <= max_dist_eye_km * KM_TO_M) & (d_centr[""] > 1)
if not np.any(close_centr_msk):
return rainrates, reachable_centr_idx
# restrict to the centroids that are within reach of any of the positions
track_centr_msk = close_centr_msk.any(axis=0)
track_centr = centroids[track_centr_msk]
close_centr_msk = close_centr_msk[:, track_centr_msk]
d_centr = {key: d[:, track_centr_msk, ...] for key, d in d_centr.items()}
if model == MODEL_RAIN["R-CLIPER"]:
rainrates = _rcliper(si_track, d_centr[""], close_centr_msk, **model_kwargs)
elif model == MODEL_RAIN["TCR"]:
rainrates = _tcr(si_track, track_centr, d_centr, close_centr_msk, **model_kwargs)
else:
raise NotImplementedError
[reachable_centr_idx] = track_centr_msk.nonzero()
return rainrates, reachable_centr_idx
def _track_to_si_with_q_and_shear(
track: xr.Dataset,
metric: str = "equirect",
q_950: float = 0.01,
matlab_ref_mode: bool = False,
**_kwargs,
) -> xr.Dataset:
"""Convert track data to SI units and add Q (humidity) and vshear variables
If the track data set does not contain the "q950" variable, but "t600", we compute the humidity
assuming a moist adiabatic lapse rate (see `_qs_from_t_diff_level`).
If the track data set does not contain the "vshear" variable, but "v850", we compute the wind
shear based on the Beta Advection Model (BAM):
v_trans = 0.8 * v850 + 0.2 * v250 + v_beta
=> 5 * (v_trans - v_beta - v850) v250 - v850 =: v_shear
Paramaters
----------
track : xr.Dataset
TC track data.
metric : str, optional
Specify an approximation method to use for earth distances: "equirect" (faster) or
"geosphere" (more accurate). See `dist_approx` function in `climada.util.coordinates`.
Default: "equirect".
q_950 : float, optional
If the track data does not include "t600" values, assume this constant value of saturation
specific humidity (in kg/kg) at 950 hPa. Default: 0.01
matlab_ref_mode : bool, optional
Do not apply the changes to the reference MATLAB implementation. Default: False
_kwargs : dict
Additional kwargs are ignored.
Returns
-------
xr.Dataset
"""
si_track = tctrack_to_si(track, metric=metric)
if "q950" in track.variables:
si_track["q950"] = track["q950"].copy()
elif "t600" not in track.variables:
si_track["q950"] = ("time", np.full_like(si_track["lat"].values, q_950))
else:
# Note that we follow the MATLAB reference in computing Q at 950 hPa as opposed to the
# pressure level used in Lu et al. 2018 (900 hPa)
pres_in = 600
pres_out = 950
si_track["q950"] = ("time", _qs_from_t_diff_level(
track["t600"].values,
si_track["vmax"].values,
pres_in,
pres_out,
matlab_ref_mode=matlab_ref_mode,
))
if "ushear" in track.variables:
si_track["vshear"] = (["time", "component"], (
np.stack([track[f"{d}shear"].values.copy() for d in ["v", "u"]], axis=1)
))
elif "u850" in track.variables:
si_track["v850"] = (["time", "component"], (
np.stack([track[f"{d}850"].values.copy() for d in ["v", "u"]], axis=1)
))
# v_drift (or v_beta) is set to be a 1.4 m/s drift in meridional direction (away from the
# equator), because that's the value used in the proprietary synthetic track generator by
# WindRiskTech. Note, however, that a value of 2.5 m/s seems to be more common in the
# literature (e.g. Emanuel et al. 2006 or Lee et al. 2018).
v_beta_lat = 1.4
si_track["vdrift"] = xr.zeros_like(si_track["v850"])
si_track["vdrift"].values[:, 0] = (
v_beta_lat
* si_track.attrs["latsign"]
* np.cos(np.radians(si_track["lat"].values))
)
si_track["vshear"] = 5 * (si_track["vtrans"] - si_track["vdrift"] - si_track["v850"])
return si_track
def _centr_distances(
si_track: xr.Dataset,
centroids: np.ndarray,
metric: str = "equirect",
res_radial_m: float = 2000.0,
**_kwargs,
) -> dict:
"""Compute distances of centroids to storm locations required for `_compute_vertical_velocity`
In addition to the distances to the centroids, the distances to staggered centroid locations,
as well as the unit vectors pointing from the storm center to each centroid are returned.
Parameters
----------
si_track : xr.Dataset
TC track data in SI units, see `tctrack_to_si`.
centroids : ndarray
Each row is a pair of lat/lon coordinates.
metric : str, optional
Approximation method to use for earth distances: "equirect" (faster) or "geosphere" (more
accurate). See `dist_approx` function in `climada.util.coordinates`.
Default: "equirect".
res_radial_m : float, optional
Spatial resolution (in m) in radial direction. Default: 2000
_kwargs : dict
Additional keyword arguments are ignored.
Returns
-------
dict
"""
# d_centr : Distance (in m) from eyes to centroids .
# v_centr : Vector pointing from storm center to centroids. The directional components are
# lat-lon, i. e. the y (meridional) direction is listed first.
[d_centr], [v_centr] = u_coord.dist_approx(
si_track["lat"].values[None], si_track["lon"].values[None],
centroids[None, :, 0], centroids[None, :, 1],
log=True, normalize=False, method=metric, units="m")
return {
"": d_centr,
"+": d_centr + res_radial_m,
"-": np.fmax(0, d_centr - res_radial_m),
"+h": d_centr + 0.5 * res_radial_m,
"-h": np.fmax(0, d_centr - 0.5 * res_radial_m),
"dir": v_centr / np.fmax(1e-3, d_centr[:, :, None]),
}
def _rcliper(
si_track: xr.Dataset,
d_centr: np.ndarray,
close_centr: np.ndarray,
) -> np.ndarray:
"""Compute rain rate (in mm/h) from maximum wind speeds using the R-CLIPER model
The model is defined in equations (3)-(5) and Table 2 (NHC) in the following publication:
Tuleya et al. (2007): Evaluation of GFDL and Simple Statistical Model Rainfall Forecasts for
U.S. Landfalling Tropical Storms. Weather and Forecasting 22(1): 56–70.
https://doi.org/10.1175/WAF972.1
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Only the "vmax" data variable is used.
d_centr : np.ndarray of shape (npositions, ncentroids)
Distance (in m) between centroids and track positions.
close_centr : np.ndarray of shape (npositions, ncentroids)
For each track position one row indicating which centroids are within reach.
Returns
-------
ndarray of shape (npositions, ncentroids)
"""
rainrate = np.zeros_like(d_centr)
d_centr, v_max = [
ar[close_centr] for ar in np.broadcast_arrays(
d_centr, si_track["vmax"].values[:, None],
)
]
# bias-adjusted coefficients from Tuleya et al. 2007, Table 2 (NHC)
# a1, a2, b1, b2 are in "inch per day" units
# a3, a4, b3, b4 are in "km" units
a1 = -1.10
a2 = -1.60
a3 = 64.5
a4 = 150.0
b1 = 3.96
b2 = 4.80
b3 = -13.0
b4 = -16.0
# u_norm : normalized maximum winds (unitless)
u_norm = 1. + (v_max / KN_TO_MS - 35.) / 33.
# rainr_0 : rain rate at r=0 (in "inch per day")
rainr_0 = a1 + b1 * u_norm
# rainr_m : rain rate at r=rad_m (in "inch per day")
rainr_m = a2 + b2 * u_norm
# rad_m : radial extent of the inner core (in km)
rad_m = a3 + b3 * u_norm
# rad_e : measure of the radial extent of the TC rain field (in km)
rad_e = a4 + b4 * u_norm
# convert radii to km
d_centr_km = d_centr / KM_TO_M
rainrate_close = np.zeros_like(d_centr)
# rain rate inside and outside of inner core
msk = (d_centr_km <= rad_m)
rainrate_close[msk] = (
rainr_0[msk] + (rainr_m[msk] - rainr_0[msk]) * (d_centr_km[msk] / rad_m[msk])
)
msk = ~msk
rainrate_close[msk] = rainr_m[msk] * np.exp(-(d_centr_km[msk] - rad_m[msk]) / rad_e[msk])
rainrate_close[np.isnan(rainrate_close)] = 0
rainrate_close[rainrate_close < 0] = 0
# convert from "inch per day" to mm/h
rainrate_close *= IN_TO_MM / D_TO_H
rainrate[close_centr] = rainrate_close
return rainrate
def _tcr(
si_track: xr.Dataset,
centroids: np.ndarray,
d_centr: dict,
close_centr: np.ndarray,
e_precip: float = 0.5,
**kwargs,
) -> np.ndarray:
"""Compute rain rate (in mm/h) using the TCR model
This follows the TCR model that was used by Zhu et al. 2013 and Emanuel 2017 for the first time
and documented in Lu et al. 2018. This implementation includes improvements proposed by
Feldmann et al. 2019:
Zhu et al. (2013): Estimating tropical cyclone precipitation risk in Texas. Geophysical
Research Letters 40(23): 6225–6230. https://doi.org/10.1002/2013GL058284
Emanuel (2017): Assessing the present and future probability of Hurricane Harvey’s rainfall.
Proceedings of the National Academy of Sciences 114(48): 12681–12684.
https://doi.org/10.1073/pnas.1716222114
Lu et al. (2018): Assessing Hurricane Rainfall Mechanisms Using a Physics-Based Model:
Hurricanes Isabel (2003) and Irene (2011). Journal of the Atmospheric
Sciences 75(7): 2337–2358. https://doi.org/10.1175/JAS-D-17-0264.1
Feldmann et al. (2019): Estimation of Atlantic Tropical Cyclone Rainfall Frequency in the
United States. Journal of Applied Meteorology and Climatology 58(8): 1853–1866.
https://doi.org/10.1175/JAMC-D-19-0011.1
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Which data variables are used in the computation of the rain
rates depends on the selected wind model.
d_centr : dict
Output of `_centr_distances`.
close_centr : np.ndarray of shape (npositions, ncentroids)
For each track position one row indicating which centroids are within reach.
e_precip : float, optional
Precipitation efficiency (unitless), the fraction of the vapor flux falling to the surface
as rainfall (Lu et al. 2018, eq. (14)). Note that we follow the MATLAB reference
implementation and use 0.5 as a default value instead of the 0.9 that was proposed in
Lu et al. 2018. Default: 0.5
kwargs :
The remaining arguments are passed on to _compute_vertical_velocity.
Returns
-------
ndarray of shape (npositions, ncentroids)
"""
# w is of shape (ntime, ncentroids)
w = _compute_vertical_velocity(si_track, centroids, d_centr, close_centr, **kwargs)
# derive vertical vapor flux wq by multiplying with saturation specific humidity Q950
wq = si_track["q950"].values[:, None] * w
# convert rainrate from "meters per second" to "milimeters per hour"
rainrate = (M_TO_MM * H_TO_S) * e_precip * RHO_A_OVER_RHO_L * wq
return rainrate
def _compute_vertical_velocity(
si_track: xr.Dataset,
centroids: np.ndarray,
d_centr: dict,
close_centr: np.ndarray,
wind_model: str = "ER11",
elevation_tif: Optional[Union[str, Path]] = None,
c_drag_tif: Optional[Union[str, Path]] = None,
w_rad: float = 0.005,
res_radial_m: float = 2000.0,
min_c_drag: float = 0.001,
max_w_foreground: float = 7.0,
matlab_ref_mode: bool = False,
) -> np.ndarray:
"""Compute the vertical wind velocity at locations along a tropical cyclone track
This implements eqs. (6)-(13) from:
Lu et al. (2018): Assessing Hurricane Rainfall Mechanisms Using a Physics-Based Model:
Hurricanes Isabel (2003) and Irene (2011). Journal of the Atmospheric
Sciences 75(7): 2337–2358. https://doi.org/10.1175/JAS-D-17-0264.1
with improvements from:
Feldmann et al. (2019): Estimation of Atlantic Tropical Cyclone Rainfall Frequency in the
United States. Journal of Applied Meteorology and Climatology 58(8): 1853–1866.
https://doi.org/10.1175/JAMC-D-19-0011.1
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Which data variables are used depends on the wind model.
centroids : ndarray
Each row is a pair of lat/lon coordinates.
d_centr : ndarray of shape (npositions, ncentroids)
Distances from storm centers to centroids.
close_centr : np.ndarray of shape (npositions, ncentroids)
For each track position one row indicating which centroids are within reach.
wind_model : str, optional
Parametric wind field model to use, see TropCyclone. Default: "ER11".
elevation_tif : Path or str, optional
Path to a GeoTIFF file containing digital elevation model data (in m). If not specified, an
SRTM-based topography at 0.1 degree resolution provided with CLIMADA is used. Default: None
c_drag_tif : Path or str, optional
Path to a GeoTIFF file containing gridded drag coefficients (bottom friction). If not
specified, an ERA5-based data set provided with CLIMADA is used. Default: None
w_rad : float, optional
Background subsidence velocity (in m/s) under radiative cooling. Default: 0.005
res_radial_m : float, optional
Spatial resolution (in m) in radial direction. Default: 2000
min_c_drag : float, optional
The drag coefficient is clipped to this minimum value (esp. over ocean). Default: 0.001
max_w_foreground : float, optional
The maximum value (in m/s) at which to clip the vertical velocity w before subtracting the
background subsidence velocity w_rad. The default value is taken from the MATLAB reference
implementation. Default: 7.0
matlab_ref_mode : bool, optional
Do not apply the changes to the reference MATLAB implementation. Default: False
Returns
-------
ndarray of shape (ntime, ncentroids)
"""
h_winds = _horizontal_winds(
si_track, d_centr, close_centr, MODEL_VANG[wind_model], matlab_ref_mode=matlab_ref_mode,
)
# Currently, the `close_centr` mask is ignored in the computation of the components, but it is
# applied only afterwards. This is because it seems like the code readability would suffer a
# lot from this. However, this might be one aspect where computational performance can be
# improved in the future.
w = np.zeros_like(d_centr[""])
w_f_plus_w_t = _w_frict_stretch(
si_track, d_centr, h_winds, centroids,
res_radial_m=res_radial_m, c_drag_tif=c_drag_tif, min_c_drag=min_c_drag,
)[close_centr]
w_h = _w_topo(si_track, d_centr, h_winds, centroids, elevation_tif=elevation_tif)[close_centr]
w_s = _w_shear(si_track, d_centr, h_winds, res_radial_m=res_radial_m)[close_centr]
w[close_centr] = np.fmax(np.fmin(w_f_plus_w_t + w_h + w_s, max_w_foreground) - w_rad, 0)
return w
def _horizontal_winds(
si_track: xr.Dataset,
d_centr: dict,
close_centr: np.ndarray,
model: int,
matlab_ref_mode: bool = False,
) -> dict:
"""Compute all horizontal wind speed variables required for `_compute_vertical_velocity`
Wind speeds are not only computed on the given centroids and for the given times, but also at
staggered locations for further use in finite difference computations.
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Which data variables are used depends on the wind model.
d_centr : dict
Output of `_centr_distances`.
close_centr : np.ndarray of shape (npositions, ncentroids)
For each track position one row indicating which centroids are within reach.
model : int
Wind profile model selection according to MODEL_VANG.
matlab_ref_mode : bool, optional
Do not apply the changes to the reference MATLAB implementation. Default: False
Returns
-------
dict
"""
ntime = si_track.sizes["time"]
ncentroids = d_centr[""].shape[1]
# all of the following are in meters per second:
winds = {
# the cyclostrophic wind direction, the meridional direction is listed first!
"dir": (
si_track.attrs["latsign"]
* np.array([1.0, -1.0])[..., :]
* d_centr["dir"][:, :, ::-1]
),
# radial windprofile without influence from coriolis force
"nocoriolis": _windprofile(
si_track, d_centr[""], close_centr, model,
cyclostrophic=True, matlab_ref_mode=matlab_ref_mode,
),
}
# winds['r±,t±'] : radial windprofile with offset in radius and/or time
steps = ["", "+", "-"]
for rstep in steps:
for tstep in steps:
result = np.zeros((ntime, ncentroids))
if tstep == "":
result[:, :] = _windprofile(
si_track, d_centr[rstep], close_centr, model,
cyclostrophic=False, matlab_ref_mode=matlab_ref_mode,
)
else:
# NOTE: For the computation of time derivatives, the eye of the storm is held
# fixed while only the wind profile varies (see MATLAB code)
sl = slice(2, None) if tstep == "+" else slice(None, -2)
result[1:-1, :] = _windprofile(
si_track.isel(time=sl), d_centr[rstep][1:-1], close_centr[1:-1], model,
cyclostrophic=False, matlab_ref_mode=matlab_ref_mode,
)
winds[f"r{rstep},t{tstep}"] = result
# winds['r±h,t'] : radial windprofile with half-sized offsets in radius
for rstep in ["+", "-"]:
winds[f"r{rstep}h,t"] = 0.5 * (winds["r,t"] + winds[f"r{rstep},t"])
return winds
def _windprofile(
si_track: xr.Dataset,
d_centr: dict,
close_centr: np.ndarray,
model: int,
cyclostrophic: bool = False,
matlab_ref_mode: bool = False,
) -> np.ndarray:
"""Compute (absolute) angular wind speeds according to a parametric wind profile
Wrapper around `compute_angular_windspeeds` (from climada.trop_cyclone) that adjusts the
Coriolis parameter if matlab_ref_mode is True.
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Which data variables are used depends on the wind model.
d_centr : ndarray of shape (npositions, ncentroids)
Distances from storm centers to centroids.
close_centr : np.ndarray of shape (npositions, ncentroids)
For each track position one row indicating which centroids are within reach.
model : int
Wind profile model selection according to MODEL_VANG.
cyclostrophic : bool, optional
If True, don't apply the influence of the Coriolis force (set the Coriolis terms to 0).
Default: False
matlab_ref_mode : bool, optional
Do not apply the changes to the reference MATLAB implementation. Default: False
Returns
-------
ndarray of shape (npositions, ncentroids)
"""
if matlab_ref_mode:
# In the MATLAB implementation, the Coriolis parameter is chosen to be 5e-5 (independent of
# latitude), following formula (2) and the remark in Section 3 of Emanuel & Rotunno (2011).
si_track = si_track.copy(deep=True)
si_track["cp"].values[:] = 5e-5
return compute_angular_windspeeds(
si_track, d_centr, close_centr, model, cyclostrophic=cyclostrophic,
)
def _w_shear(
si_track: xr.Dataset,
d_centr: dict,
h_winds: dict,
res_radial_m: float = 2000.0,
) -> np.ndarray:
"""Compute the shear component of the vertical wind velocity
This implements eq. (12) from:
Lu et al. (2018): Assessing Hurricane Rainfall Mechanisms Using a Physics-Based Model:
Hurricanes Isabel (2003) and Irene (2011). Journal of the Atmospheric
Sciences 75(7): 2337–2358. https://doi.org/10.1175/JAS-D-17-0264.1
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. The data variables used by this function are "cp" and "vshear".
If the "vshear" variable is not available, the result is 0 everywhere.
d_centr : dict
Output of `_centr_distances`.
h_winds : dict
Output of `_horizontal_winds`.
res_radial_m : float, optional
Spatial resolution (in m) in radial direction. Default: 2000
Returns
-------
ndarray of shape (ntime, ncentroids)
"""
if "vshear" not in si_track.variables:
return np.zeros_like(h_winds["r,t"])
# fac_scalar : scalar factor from eq. (12) in Lu et al. 2018:
# g / (cp * (Ts - Tt) * (1 - ep) * N**2
# g : gravitational acceleration (10 m*s**-2)
# cp : isobaric specific heat of dry air (1000 J*(kg*K)**-1)
# Ts - Tt : difference between surface and tropopause temperature (100 K)
# ep : precipitation efficiency (0.5)
# N : buoyancy frequency for dry air (2e-2 s**-1)
# While Lu et al. 2018 assume a factor of 0.5, we follow the MATLAB reference implementation
# and use a factor of 1.0 because the precipitation efficiency (ep) is higher (0.75 instead
# of 0.5) in the TC than in the normal tropical environment.
fac_scalar = 1.0
fac = fac_scalar * (
si_track["cp"].values[:, None]
+ h_winds["r,t"] / (1 + d_centr[""])
+ (h_winds["r+,t"] - h_winds["r-,t"]) / (2.0 * res_radial_m)
)
return h_winds["nocoriolis"] * fac * (
d_centr["dir"] * si_track["vshear"].values[:, None, :]
).sum(axis=-1)
def _w_topo(
si_track: xr.Dataset,
d_centr: dict,
h_winds: dict,
centroids: np.ndarray,
elevation_tif: Optional[Union[str, Path]] = None,
) -> np.ndarray:
"""Compute the topographic component w_h of the vertical wind velocity
This implements eq. (7) from:
Lu et al. (2018): Assessing Hurricane Rainfall Mechanisms Using a Physics-Based Model:
Hurricanes Isabel (2003) and Irene (2011). Journal of the Atmospheric
Sciences 75(7): 2337–2358. https://doi.org/10.1175/JAS-D-17-0264.1
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. Only the "vtrans" data variable is used by this function.
d_centr : dict
Output of `_centr_distances`.
h_winds : dict
Output of `_horizontal_winds`.
centroids : ndarray
Each row is a pair of lat/lon coordinates.
elevation_tif : Path or str, optional
Path to a GeoTIFF file containing digital elevation model data (in m). If not specified, an
SRTM-based topography at 0.1 degree resolution provided with CLIMADA is used. Default: None
Returns
-------
ndarray of shape (ntime, ncentroids)
"""
if elevation_tif is None:
elevation_tif = default_elevation_tif()
# Note that the gradient of the raster products is smoothed (as in the reference MATLAB
# implementation), even though it should be piecewise constant since the data itself is read
# with bilinear interpolation.
h, h_grad = u_coord.read_raster_sample_with_gradients(
elevation_tif, centroids[:, 0], centroids[:, 1], method=("linear", "linear"),
)
# only consider interaction with terrain over land
mask_onland = (h > -1)
h[~mask_onland] = -1
h_grad[~mask_onland, :] *= 0
# reduce effect of translation speed and orography outside of storm core
vtrans_red = si_track["vtrans"].values[:, None, :] * (
np.clip((300 * KM_TO_M - d_centr[""]) / (50 * KM_TO_M), 0, 1)[:, :, None]
)
h_grad_red = h_grad[None, :, :] * (
np.clip((150 * KM_TO_M - d_centr[""]) / (30 * KM_TO_M), 0.2, 0.6)[:, :, None]
)
return (
(vtrans_red + h_winds["nocoriolis"][..., None] * h_winds["dir"]) * h_grad_red
).sum(axis=-1)
def _w_frict_stretch(
si_track: xr.Dataset,
d_centr: dict,
h_winds: dict,
centroids: np.ndarray,
res_radial_m: float = 2000.0,
c_drag_tif: Optional[Union[str, Path]] = None,
min_c_drag: float = 0.001,
) -> np.ndarray:
"""Compute the sum of the frictional and stretching components w_f and w_t
This implements eq. (6) and (11) from:
Lu et al. (2018): Assessing Hurricane Rainfall Mechanisms Using a Physics-Based Model:
Hurricanes Isabel (2003) and Irene (2011). Journal of the Atmospheric
Sciences 75(7): 2337–2358. https://doi.org/10.1175/JAS-D-17-0264.1
Parameters
----------
si_track : xr.Dataset
Output of `tctrack_to_si`. The data variables used by this function are "cp", "rad",
"tstep" and "vtrans".
d_centr : dict
Output of `_centr_distances`.
h_winds : dict
Output of `_horizontal_winds`.
centroids : ndarray
Each row is a pair of lat/lon coordinates.
res_radial_m : float, optional
Spatial resolution (in m) in radial direction. Default: 2000
c_drag_tif : Path or str, optional
Path to a GeoTIFF file containing gridded drag coefficients (bottom friction). If not
specified, an ERA5-based data set provided with CLIMADA is used. Default: None
min_c_drag : float, optional
The drag coefficient is clipped to this minimum value (esp. over ocean). Default: 0.001
Returns
-------
ndarray of shape (ntime, ncentroids)
"""
# sum of frictional and stretching components w_f and w_t
if c_drag_tif is None:
c_drag_tif = default_drag_tif()
# vnet : absolute value of the total surface wind
vnet = {
f"r{rstep}h,t": np.linalg.norm(
(
h_winds[f"r{rstep}h,t"][..., None] * h_winds["dir"]
+ si_track["vtrans"].values[:, None, :]
),
axis=-1,
)
for rstep in ["+", "-"]
}
# Note that the gradient of the raster products is smoothed (as in the reference MATLAB
# implementation), even though it should be piecewise constant since the data itself is read
# with bilinear interpolation.
cd, cd_grad = u_coord.read_raster_sample_with_gradients(
c_drag_tif, centroids[:, 0], centroids[:, 1], method=("linear", "linear"),
)
mask_onland = (cd >= min_c_drag)
cd[~mask_onland] = min_c_drag
cd_grad[~mask_onland, :] *= 0
cd_hstep = (cd_grad[None] * (0.5 * res_radial_m * d_centr["dir"])).sum(axis=-1)
cd = {
"": cd,
"r+h": np.clip(cd + cd_hstep, 0.0, 0.01),
"r-h": np.clip(cd - cd_hstep, 0.0, 0.01),
}
# tau : azimuthal surface stress, equation (8) in (Lu et al. 2018)
tau = {
f"r{rstep}h,t": (
-cd[f"r{rstep}h"] * h_winds[f"r{rstep}h,t"] * vnet[f"r{rstep}h,t"]
) for rstep in ["+", "-"]
}
# evaluate derivative of angular momentum M wrt r
# M = r * V + 0.5 * f * r^2
# dMdr = r * (f + dVdr) + V
dMdr = {
f"r{rstep}h,t": (
d_centr[f"{rstep}h"] * (
si_track["cp"].values[:, None]
+ (1 if rstep == "+" else -1) * (
h_winds[f"r{rstep},t"] - h_winds[f"r,t"]
) / res_radial_m
) + h_winds[f"r{rstep}h,t"]
)
for rstep in ["+", "-"]
}
# compute derivative of angular momentum M wrt t
# M = r * V + 0.5 * f * r^2
# dMdt = r * dVdt
# combine equations (6) and (11) in (Lu et al. 2018):
# w_f + w_t = (1 / r) * (d/dr) [pre_wf_wt]
# where [pre_wf_wt] := [r^2 / dMdr * (H_TROP * dVdt - tau)]
pre_wf_wt = {
f"r{rstep}h,t": (
d_centr[rstep]**2 / np.fmax(10, dMdr[f"r{rstep}h,t"]) * (
# NOTE: The attenuation factor is not in Lu et al. 2018.
# It ranges from -1 (at the storm center) to 1 (at RMW and higher).
np.fmin(1, (-1 + 2 * (d_centr[rstep] / si_track["rad"].values[:, None])**2)) *
# continue with "official" formula:
H_TROP * (
h_winds[f"r{rstep},t+"] - h_winds[f"r{rstep},t-"]
) / (2 * si_track["tstep"].values[:, None])
- tau[f"r{rstep}h,t"]
)
) for rstep in ["+", "-"]
}
return (pre_wf_wt["r+h,t"] - pre_wf_wt["r-h,t"]) / (res_radial_m * d_centr[""])
def _qs_from_t_diff_level(
temps_in: np.ndarray,
vmax: np.ndarray,
pres_in: float,
pres_out: float,
cap_heat_air: float = 1005.0,
max_iter: int = 5,
matlab_ref_mode: bool = False,
) -> np.ndarray:
"""Compute the humidity from temperature assuming a moist adiabatic lapse rate
The input temperatures may be given on a different pressure level than the output humidities.
When computing Q from T on the same pressure level, see `_r_from_t_same_level` instead since
Q = r / (1 + r) for the mixing ratio r.
The approach assumes that the lapse rate dT/dz is given by the law for the moist adiabatic
lapse rate so that the following expression for the "total entropy" is a conserved quantity
across pressure levels (see eq. (4.5.9) in Emanuel (1994): Atmospheric convection):
s = cp * log(T) + Lv * r / T - Rd * log(p) = const.,
where:
T : temperature,
r : mixing ratio (equals Q/(1-Q) where Q is saturation specific humidity),
p : pressure,
cp : isobaric specific heat of dry air,
Lv : latent heat of the evaporation of water,
Rd : specific gas constant of dry air.
Since it's possible to compute Q from T on the same pressure level (see
`_r_from_t_same_level`), we can use this relationship to compute Q at one pressure level from
T given on a different pressure level. However, since we can't solve the equation for T
analytically, we use the Newton-Raphson method to find the solution.
Parameters
----------
temps_in : ndarray
Temperatures (in K) at the pressure level pres_in.
vmax : ndarray
Maximum surface wind speeds (in m/s).
pres_in : float
Pressure level (in hPa) at which the input temperatures are given.
pres_out : float
Pressure level (in hPa) at which the output humidity is computed.
cap_heat_air : float, optional
Isobaric specific heat of dry air (in J/(kg*K)). The value depends on env. conditions and
lies, for example, between 1003 (for 0 °C) and 1012 (typical room conditions).
Default: 1005
max_iter : int, optional
The number of Newton-Raphson steps to take. Default: 5
matlab_ref_mode : bool, optional
Do not apply the changes to the reference MATLAB implementation. Default: False
Returns
-------
q_out : ndarray
For each temperature value in temps_in, a value of saturation specific humidity (in kg/kg)
at the pressure level pres_out.
"""
# c_vmax : rescale factor from (squared) surface to (squared) gradient winds
# In the MATLAB implementation, c_vmax=1.6 is used, but this is almost the same as the
# value used here (0.8**-2).
c_vmax = 1.6 if matlab_ref_mode else GRADIENT_LEVEL_TO_SURFACE_WINDS**-2
# In the MATLAB implementation, the "Bolton1980" coefficients, and an approximative form of the
# derivative are used in the computation of the mixing ratio.
r_from_t_kwargs = dict(
tetens_coeffs="Bolton1980" if matlab_ref_mode else "Buck1981",
use_cc_derivative=matlab_ref_mode,
)
# first, calculate mixing ratio r_in from temps_in
r_in, _ = _r_from_t_same_level(pres_in, np.fmax(T_ICE_K - 50, temps_in), **r_from_t_kwargs)
# derive (temps_out, r_out) from (temps_in, r_in) iteratively (Newton-Raphson method)
r_out = np.zeros_like(r_in)
temps_out = temps_in.copy() + 20 # first guess, assuming that pres_out > pres_in
# exclude missing data (fill values) in the inputs
mask = (temps_in > 100)
# s : Total entropy, which is conserved across pressure levels when assuming a moist adiabatic
# lapse rate. The additional vmax-term is a correction to account for the fact that the
# eyewall is warmer than the environment at 600 hPa (thermal wind balance). For a reference
# of the vmax-term, see, e.g., the considerations in the following article:
#
# Emanuel, K. (1986): An Air-Sea Interaction Theory for Tropical Cyclones. Part I:
# Steady-State Maintenance. Journal of the Atmospheric Sciences 43(6): 585–605.
# https://doi.org/10.1175/1520-0469(1986)043<0585:AASITF>2.0.CO;2
#
# Compared to eq. (34), the last term is neglected due to V >> fr. The boundary layer
# theta_e under the eyewall is equated with that of the far environment, and the saturation
# theta_e of the eyewall (which is constant along an M surface) is equated with the
# saturation theta_e of the sea surface.
s_in = (
cap_heat_air * np.log(temps_in[mask])
+ L_EVAP_WATER * r_in[mask] / temps_in[mask]
- R_DRY_AIR * np.log(pres_in)
+ c_vmax * vmax[mask]**2 / DELTA_T_TROPOPAUSE
)
# solve `s_out(T_out) - s_in = 0` using the Newton-Raphson method
for it in range(max_iter):
# compute new estimate of r_out from current estimate of T_out
r_out[mask], drdT = _r_from_t_same_level(
pres_out, temps_out[mask], gradient=True, **r_from_t_kwargs,
)
s_out = (
cap_heat_air * np.log(temps_out[mask])
+ L_EVAP_WATER * r_out[mask] / temps_out[mask]
- R_DRY_AIR * np.log(pres_out)
)
dsdT = (
cap_heat_air * temps_out[mask]
+ L_EVAP_WATER * (drdT * temps_out[mask] - r_out[mask])
) / temps_out[mask]**2
# take Newton step
temps_out[mask] -= (s_out - s_in) / dsdT
if matlab_ref_mode:
# In the MATLAB implementation, this function does actually return the mixing ratio which
# is almost the same as the "specific humidity" in practice.
q_out = r_out
else:
q_out = r_out / (1 + r_out)
return q_out
def _r_from_t_same_level(
p_ref: float,
temps: np.ndarray,
gradient: bool = False,
tetens_coeffs: str = "Buck1981",
use_cc_derivative: bool = False,
) -> Tuple[np.ndarray, Optional[np.ndarray]]:
"""Compute the mixing ratio from temperature at a given pressure level
The physical background is the Clausius-Clapeyron equation for water vapor pressure under
typical atmospheric conditions, but since this differential equation does not have an explicit
solution, the implementation uses the Tetens (or August-Roche-Magnus) approximation formula
with coefficients given in:
Murray (1967): On the Computation of Saturation Vapor Pressure. Journal of Applied Meteorology
and Climatology 6(1): 203–204. http://doi.org/10.1175/1520-0450(1967)006<0203:OTCOSV>2.0.CO;2
Bolton (1980): The Computation of Equivalent Potential Temperature. Monthly Weather Review
108(7): 1046–1053. https://doi.org/10.1175/1520-0493(1980)108<1046:TCOEPT>2.0.CO;2
Buck (1981): New Equations for Computing Vapor Pressure and Enhancement Factor. Journal of
Applied Meteorology and Climatology 20(12): 1527-1532.
https://doi.org/10.1175/1520-0450(1981)020<1527:NEFCVP>2.0.CO;2
Alduchov and Eskridge (1996): Improved Magnus Form Approximation of Saturation Vapor Pressure.
Journal of Applied Meteorology and Climatology 35(4): 601–609.
https://doi.org/10.1175/1520-0450(1996)035<0601:imfaos>2.0.co;2
The default coefficients (Buck 1981) are also used by the ECMWF, see Section 7.2.1 (b) of the
following document:
ECMWF (2023): IFS Documentation CY48R1, Part IV: Physical Processes.
https://www.ecmwf.int/en/elibrary/81370-ifs-documentation-cy48r1-part-iv-physical-processes
Parameters
----------
p_ref : float
Reference pressure level (in hPa) at which the input temperatures are given and at which
output mixing ratio values are computed.
temps : ndarray
Temperatures (in K) at the pressure level p_ref.
gradient : bool, optional
If True, compute the derivative of the functional relationship between Q and T.
tetens_coeffs : str, optional
Coefficients to use for the Tetens formula. One of "Alduchov1996", "Buck1981",
"Bolton1980", or "Murray1967". Default: "Buck1981"
use_cc_derivative : bool, optional
Instead of the actual derivative, use an approximation of the derivative (that is used in
the MATLAB code) based on the original Clausius-Clapeyron equation for water vapor under
typical atmospheric conditions:
des/dT = (Lv * es) / (Rv * T**2)
The approximation is used in the MATLAB reference implementation under the assumption that
it is only used in the Newton-Raphson iteration where little errors do not matter.
Default: False
Returns
-------
r : ndarray
For each temperature value in temp, a value of saturation specific humidity (in kg/kg).
drdT : ndarray
If `gradient` is False, this is None. Otherwise, the derivative of Q with respect to T is
returned.
"""
try:
a, b, c = {
"Murray1967": (17.2693882, 35.86, 6.1078),
"Bolton1980": (17.67, 29.65, 6.112),
"Buck1981": (17.502, 32.19, 6.1121),
"Alduchov1996": (17.625, 30.12, 6.1094),
}[tetens_coeffs]
except KeyError as err:
raise ValueError(f"Unknown Tetens coefficients: {tetens_coeffs}") from err
# es : saturation vapor pressure (in hPa)
es = c * np.exp(a * (temps - T_ICE_K) / (temps - b))
fact = M_WATER / M_DRY_AIR
r_mix = fact * es / (p_ref - es)
drdT = None
if gradient:
if use_cc_derivative:
# Specific gas constant of water vapor (in J / kgK)
r_water = 1000 * R_GAS / M_WATER
drdT = (L_EVAP_WATER / r_water) / temps**2 * r_mix
# Note that the full C-C formula including the deriative of r_mix with respect to es
# (as in dr/dT = dr/des * des/dT) would be as follows:
# drdT = (L_EVAP_WATER / r_water) / temps**2 * r_mix * (1 + r_mix / fact)
else:
drdT = a * (T_ICE_K - b) / (temps - b)**2 * r_mix * (1 + r_mix / fact)
return r_mix, drdT