Skip to content

Commit 04bc119

Browse files
Adding feature for filling pressure using a regression method (#117)
* moving carq_ref to above holland B calculation and just using that to compute it instead of needing to take the nanmean * add Pc filling method and setting to the existing persistentB option as default * changing PcFillMethod option names to keep the existing convection * adding general formula and constants for the chavas 2025 Pc regression method * corrections to code to pass existing tests, adding test for the new Pc filling regression method by Chavas et al 2025 * finalizing the Chavas method using one regression function where R34 is available and another one when it is not * length should be 11 like rmw with smoothing * adding PcFillMethod option to stormevent.py * correcting doi url for the Chavas paper * Fix USGS test * adding the Courtney Knaff 2009 regression method for filling Pc, changing the assunption on translation speed units to kts, this needs revisiting to be sure * Fix storm moving speed unit conversion for Pc fill * Fix style * Revert previous change and make sure storm moving speed is always stored in knots --------- Co-authored-by: SorooshMani-NOAA <[email protected]>
1 parent e4a3aab commit 04bc119

28 files changed

+29477
-29287
lines changed

stormevents/nhc/const.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ class RMWFillMethod(Enum):
1212
regression_penny_2023_no_smoothing = auto()
1313

1414

15+
class PcFillMethod(Enum):
16+
none = None
17+
persistent_holland_b = auto()
18+
regression_chavas_2025 = auto()
19+
regression_courtney_knaff_2009 = auto()
20+
21+
22+
# constants for the Chavas et al. (2025) regression Pc fill method
23+
OMEGA = 7.292e-5 # [1/s]
24+
BETA_00 = -6.60
25+
BETA_V20 = -0.0127
26+
BETA_fR = -5.506
27+
BETA_fRdV = 109.013
28+
BETA_01 = -13.37
29+
BETA_V21 = -0.0157
30+
1531
# Bias correction values for the Rmax forecast
1632
# ref: Penny et al. (2023). https://doi.org/10.1175/WAF-D-22-0209.1
1733
bias_lat = [

stormevents/nhc/track.py

Lines changed: 158 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@
3737
get_RMW_regression_coefs,
3838
RMW_bias_correction,
3939
RMWFillMethod,
40+
PcFillMethod,
41+
OMEGA,
42+
BETA_00,
43+
BETA_V20,
44+
BETA_fR,
45+
BETA_fRdV,
46+
BETA_01,
47+
BETA_V21,
4048
)
4149
from stormevents.utilities import subset_time_interval
4250

@@ -55,6 +63,7 @@ def __init__(
5563
advisories: List[ATCF_Advisory] = None,
5664
forecast_time: datetime = None,
5765
rmw_fill: RMWFillMethod = RMWFillMethod.regression_penny_2023,
66+
pc_fill: PcFillMethod = PcFillMethod.persistent_holland_b,
5867
):
5968
"""
6069
:param storm: storm ID, or storm name and year
@@ -110,6 +119,7 @@ def __init__(
110119
self.advisories = advisories
111120
self.file_deck = file_deck
112121
self.rmw_fill = rmw_fill
122+
self.pc_fill = pc_fill
113123

114124
self.__previous_configuration = self.__configuration
115125

@@ -129,6 +139,7 @@ def from_storm_name(
129139
advisories: List[ATCF_Advisory] = None,
130140
forecast_time: datetime = None,
131141
rmw_fill: RMWFillMethod = RMWFillMethod.regression_penny_2023,
142+
pc_fill: PcFillMethod = PcFillMethod.persistent_holland_b,
132143
) -> "VortexTrack":
133144
"""
134145
:param name: storm name
@@ -153,6 +164,7 @@ def from_storm_name(
153164
advisories=advisories,
154165
forecast_time=forecast_time,
155166
rmw_fill=rmw_fill,
167+
pc_fill=pc_fill,
156168
)
157169

158170
@classmethod
@@ -165,6 +177,7 @@ def from_file(
165177
advisories: List[ATCF_Advisory] = None,
166178
forecast_time: datetime = None,
167179
rmw_fill: RMWFillMethod = RMWFillMethod.regression_penny_2023,
180+
pc_fill: PcFillMethod = PcFillMethod.persistent_holland_b,
168181
) -> "VortexTrack":
169182
"""
170183
:param path: file path to ATCF data
@@ -197,6 +210,7 @@ def from_file(
197210
advisories=advisories,
198211
forecast_time=forecast_time,
199212
rmw_fill=rmw_fill,
213+
pc_fill=pc_fill,
200214
)
201215

202216
@property
@@ -499,7 +513,7 @@ def filename(self, filename: PathLike):
499513
@property
500514
def rmw_fill(self) -> RMWFillMethod:
501515
"""
502-
:return: file path to read file (optional)
516+
:return: RMW filling method
503517
"""
504518

505519
return self.__rmw_fill
@@ -511,6 +525,21 @@ def rmw_fill(self, rmw_fill: RMWFillMethod):
511525

512526
self.__rmw_fill = rmw_fill
513527

528+
@property
529+
def pc_fill(self) -> PcFillMethod:
530+
"""
531+
:return: Pc filling method
532+
"""
533+
534+
return self.__pc_fill
535+
536+
@pc_fill.setter
537+
def pc_fill(self, pc_fill: PcFillMethod):
538+
if pc_fill is None or not isinstance(pc_fill, PcFillMethod):
539+
pc_fill = PcFillMethod.none
540+
541+
self.__pc_fill = pc_fill
542+
514543
@property
515544
def data(self) -> DataFrame:
516545
"""
@@ -1068,7 +1097,7 @@ def unfiltered_data(self, dataframe: DataFrame):
10681097
tracks = separate_tracks(dataframe)
10691098
if all(adv in tracks for adv in ["OFCL", "CARQ"]):
10701099
tracks = correct_ofcl_based_on_carq_n_hollandb(
1071-
tracks, rmw_fill=self.rmw_fill
1100+
tracks, rmw_fill=self.rmw_fill, pc_fill=self.pc_fill
10721101
)
10731102
dataframe = combine_tracks(tracks)
10741103

@@ -1090,6 +1119,7 @@ def __configuration(self) -> Dict[str, Any]:
10901119
"advisories": self.advisories,
10911120
"filename": self.filename,
10921121
"rmw_fill": self.rmw_fill,
1122+
"pc_fill": self.pc_fill,
10931123
}
10941124

10951125
@staticmethod
@@ -1148,7 +1178,7 @@ def __compute_velocity(data: DataFrame) -> DataFrame:
11481178
bearings.ffill(inplace=True)
11491179
speeds.bfill(inplace=True)
11501180
bearings.bfill(inplace=True)
1151-
advisory_data["speed"] = speeds
1181+
advisory_data["speed"] = speeds * 1.9438 # m/s to kt
11521182
advisory_data["direction"] = bearings
11531183

11541184
data.loc[data["advisory"] == advisory] = advisory_data
@@ -1226,6 +1256,95 @@ def max_sustained_wind_speed(
12261256
)
12271257

12281258

1259+
def chavas_2025_Pc(data: DataFrame):
1260+
"""
1261+
perform the Chavas et al. (2025) regression method for filling in central pressure
1262+
Ref:
1263+
Chavas, D. R., Knaff, J. A., & Klotzbach, P. (2025).
1264+
A Simple Model for Predicting Tropical Cyclone Minimum Central Pressure from Intensity and Size.
1265+
Weather and Forecasting, 40, 333–346.
1266+
https://doi.org/10.1175/WAF-D-24-0031.1
1267+
1268+
:param data: data frame of track with missing entries
1269+
:return: central pressure values
1270+
"""
1271+
1272+
fo2 = OMEGA * numpy.sin(numpy.deg2rad(data.latitude)) # half coriolis [1/s]
1273+
Vmax = 0.5144 * (
1274+
data.max_sustained_wind_speed - 0.55 * data.speed
1275+
) # azimuthal mean Vmax [m/s]
1276+
isotach_radii = data[
1277+
[
1278+
"isotach_radius_for_NEQ",
1279+
"isotach_radius_for_SEQ",
1280+
"isotach_radius_for_NWQ",
1281+
"isotach_radius_for_SWQ",
1282+
]
1283+
]
1284+
# make any 0 value NaN
1285+
isotach_radii[isotach_radii == 0] = pandas.NA
1286+
R34 = 0.85 * isotach_radii.mean(axis=1) * 1852 # average R34 radius [m]
1287+
# forward fill to fill in 50-kt and 64-kt rows with the R34 value as
1288+
R34[data.isotach_radius > 35] = pandas.NA
1289+
R34[data.isotach_radius > 35] = R34.ffill()[data.isotach_radius > 35]
1290+
# equation where R34 is available
1291+
deltaP = (
1292+
BETA_00
1293+
+ BETA_V20 * Vmax * Vmax
1294+
+ BETA_fR * fo2 * R34
1295+
+ BETA_fRdV * fo2 * R34 / Vmax
1296+
) # [hPa]
1297+
# equation where R34 isn't available
1298+
deltaP[deltaP.isna()] = BETA_01 + BETA_V21 * Vmax * Vmax # [hPa]
1299+
return data.background_pressure + 2 + deltaP # Pc
1300+
1301+
1302+
def courtney_knaff_2009_Pc(data: DataFrame):
1303+
"""
1304+
perform the Courtney & Knaff (2009) regression method for filling in central pressure
1305+
Ref:
1306+
Courtney, J. & Knaff, J. (2009).
1307+
Adapting the Knaff and Zehr wind-pressure relationship for operational use in Tropical Cyclone Warning Centers.
1308+
Australian Meteorological and Oceanographic Journal, 58, 167-179.
1309+
https://connectsci.au/es/article/58/3/167/264593/Adapting-the-Knaff-and-Zehr-wind-pressure
1310+
1311+
:param data: data frame of track with missing entries
1312+
:return: central pressure values
1313+
"""
1314+
1315+
Vmax = data.max_sustained_wind_speed # Vmax [kt]
1316+
Vsrm = Vmax - 1.5 * data.speed**0.63 # azimuthal mean Vmax [kt]
1317+
isotach_radii = data[
1318+
[
1319+
"isotach_radius_for_NEQ",
1320+
"isotach_radius_for_SEQ",
1321+
"isotach_radius_for_NWQ",
1322+
"isotach_radius_for_SWQ",
1323+
]
1324+
]
1325+
# make any 0 value NaN
1326+
isotach_radii[isotach_radii == 0] = pandas.NA
1327+
R34 = 0.85 * isotach_radii.mean(axis=1) # average R34 radius [n mi]
1328+
# forward fill to fill in 50-kt and 64-kt rows with the R34 value as
1329+
R34[data.isotach_radius > 35] = pandas.NA
1330+
R34[data.isotach_radius > 35] = R34.ffill()[data.isotach_radius > 35]
1331+
V500 = R34 / 9 - 3 # wind speed at 500 km radius
1332+
lat = data.latitude # latitude [deg]
1333+
x = 0.1147 + 0.0055 * Vmax - 0.001 * (lat - 25)
1334+
Rmax = 66.785 - 0.09102 * Vmax + 1.0619 * (lat - 25)
1335+
V500c = Vmax * (Rmax / 500) ** x # climatological wind speed at 500 km radius
1336+
S = V500 / V500c # normalized storm size
1337+
S[(S < 0.4) | (S.isna())] = 0.4 # lower bound/default value of 0.4
1338+
# equation for lat >= 18 deg
1339+
deltaP = (
1340+
23.286 - 0.483 * Vsrm - (Vsrm / 24.524) ** 2 - 12.587 * S - 0.483 * lat
1341+
) # [hPa]
1342+
# equation for lat < 18 deg
1343+
deltaP_lo = 5.962 - 0.267 * Vsrm - (Vsrm / 18.26) ** 2 - 6.8 * S # [hPa]
1344+
deltaP[lat < 18] = deltaP_lo[lat < 18]
1345+
return data.background_pressure + 2 + deltaP # Pc
1346+
1347+
12291348
def separate_tracks(data: DataFrame) -> Dict[str, Dict[str, DataFrame]]:
12301349
"""
12311350
separate the given track data frame into advisories and tracks (forecasts / hindcasts)
@@ -1275,6 +1394,7 @@ def combine_tracks(tracks: Dict[str, Dict[str, DataFrame]]) -> DataFrame:
12751394
def correct_ofcl_based_on_carq_n_hollandb(
12761395
tracks: Dict[str, Dict[str, DataFrame]],
12771396
rmw_fill: RMWFillMethod = RMWFillMethod.regression_penny_2023,
1397+
pc_fill: PcFillMethod = PcFillMethod.persistent_holland_b,
12781398
) -> Dict[str, Dict[str, DataFrame]]:
12791399
"""
12801400
Correct official forecast using consensus track along with holland-b
@@ -1347,19 +1467,19 @@ def movingmean(dff):
13471467
else:
13481468
carq_forecast = carq_tracks[list(carq_tracks)[0]]
13491469

1470+
# Get CARQ from forecast hour 0 and isotach 34kt (i.e. the first item)
1471+
carq_ref = carq_forecast.loc[carq_forecast.forecast_hours == 0].iloc[0]
1472+
1473+
# get the Holland B parameter for filling in central pressure
1474+
# with persistent Holland B option
13501475
relation = HollandBRelation()
13511476
holland_b = relation.holland_b(
1352-
max_sustained_wind_speed=carq_forecast["max_sustained_wind_speed"],
1353-
background_pressure=carq_forecast["background_pressure"],
1354-
central_pressure=carq_forecast["central_pressure"],
1477+
max_sustained_wind_speed=carq_ref["max_sustained_wind_speed"],
1478+
background_pressure=carq_ref["background_pressure"],
1479+
central_pressure=carq_ref["central_pressure"],
13551480
)
13561481

1357-
holland_b[holland_b == numpy.inf] = numpy.nan
1358-
holland_b = numpy.nanmean(holland_b)
1359-
1360-
# Get CARQ from forecast hour 0 and isotach 34kt (i.e. the first item)
1361-
carq_ref = carq_forecast.loc[carq_forecast.forecast_hours == 0].iloc[0]
1362-
1482+
# find locations where the pertinent variables are missing
13631483
columns_of_interest = forecast[
13641484
["radius_of_maximum_winds", "central_pressure", "background_pressure"]
13651485
]
@@ -1444,14 +1564,32 @@ def movingmean(dff):
14441564
"background_pressure"
14451565
]
14461566

1447-
# fill OFCL central pressure (at sea level), preserving Holland B from 0-hr CARQ
1448-
forecast.loc[mslp_missing, "central_pressure"] = relation.central_pressure(
1449-
max_sustained_wind_speed=forecast.loc[
1450-
mslp_missing, "max_sustained_wind_speed"
1451-
],
1452-
background_pressure=forecast.loc[mslp_missing, "background_pressure"],
1453-
holland_b=holland_b,
1454-
)
1567+
# fill OFCL central pressure (at sea level):
1568+
if pc_fill == PcFillMethod.persistent_holland_b:
1569+
# preserving Holland B from 0-hr CARQ
1570+
relation = HollandBRelation()
1571+
holland_b = relation.holland_b(
1572+
max_sustained_wind_speed=carq_ref["max_sustained_wind_speed"],
1573+
background_pressure=carq_ref["background_pressure"],
1574+
central_pressure=carq_ref["central_pressure"],
1575+
)
1576+
forecast.loc[mslp_missing, "central_pressure"] = relation.central_pressure(
1577+
max_sustained_wind_speed=forecast.loc[
1578+
mslp_missing, "max_sustained_wind_speed"
1579+
],
1580+
background_pressure=forecast.loc[mslp_missing, "background_pressure"],
1581+
holland_b=holland_b,
1582+
)
1583+
elif pc_fill == PcFillMethod.regression_chavas_2025:
1584+
# use the Chavas et al. (2025) regression method
1585+
forecast.loc[mslp_missing, "central_pressure"] = chavas_2025_Pc(
1586+
forecast.loc[mslp_missing, :]
1587+
)
1588+
elif pc_fill == PcFillMethod.regression_courtney_knaff_2009:
1589+
# use the Courtney & Knaff (2009) regression method
1590+
forecast.loc[mslp_missing, "central_pressure"] = courtney_knaff_2009_Pc(
1591+
forecast.loc[mslp_missing, :]
1592+
)
14551593

14561594
corr_ofcl_tracks[initial_time] = forecast
14571595

stormevents/stormevent.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from stormevents.nhc import VortexTrack
2828
from stormevents.nhc.atcf import ATCF_Advisory
2929
from stormevents.nhc.atcf import ATCF_FileDeck
30-
from stormevents.nhc.const import RMWFillMethod
30+
from stormevents.nhc.const import RMWFillMethod, PcFillMethod
3131
from stormevents.usgs import usgs_flood_storms
3232
from stormevents.usgs import USGS_StormEvent
3333
from stormevents.utilities import relative_to_time_interval
@@ -281,6 +281,7 @@ def track(
281281
filename: PathLike = None,
282282
forecast_time: datetime = None,
283283
rmw_fill: RMWFillMethod = RMWFillMethod.regression_penny_2023,
284+
pc_fill: PcFillMethod = PcFillMethod.persistent_holland_b,
284285
) -> VortexTrack:
285286
"""
286287
retrieve NHC ATCF track data
@@ -303,7 +304,7 @@ def track(
303304
end_date = self.end_date
304305

305306
if filename is not None:
306-
track = VortexTrack.from_file(filename)
307+
track = VortexTrack.from_file(filename, rmw_fill=rmw_fill, pc_fill=pc_fill)
307308
else:
308309
track = VortexTrack.from_storm_name(
309310
name=self.name,
@@ -314,6 +315,7 @@ def track(
314315
advisories=advisories,
315316
forecast_time=forecast_time,
316317
rmw_fill=rmw_fill,
318+
pc_fill=pc_fill,
317319
)
318320
return track
319321

0 commit comments

Comments
 (0)