Skip to content

Commit ee51dd1

Browse files
using the Psurge v2.9 regression method for filling in Rmax in OFCL advisories (#96)
* adding complete method for rmax forecast regression without smoothing or limiting * reformatting back to black * adding the upper and lower bounds for RMW forecast * changing upper bound to be larger of 120.0 nmi or rmw0 * changing sorting of advisories by index to avoid out of order isotach_radius * adding 3-pt rolling mean for the rmax forecasts * finalizing the smoothing which interpolates to 12-hr intervals where required and then computes a 24-hr moving mean * preserving 34-kt isotach for lead times > 72-hrs if Vmax still above 34-kt * adding changes to the forecasts where the isotachs are correctly ordered and filling in RMW using regression technique from NHC * adding changes to the OFCL deck for RMW regression * ensuring presence of rads * initialize rads * Update test configuration * Remove python 3.12 from test matrix * Quick-test on lowser supported python * Try test without multiworker * Fixing python version for quicktest * Use py3.10 instead of min for coverage test * adding RMW regression coefficients into const.py and making a test for the retrieval function * switching to keep 50-kt radius as well as 34-kt * modified OFCL RMW forecast tests preserving the 50-kt isotach as well as 34-kt --------- Co-authored-by: SorooshMani-NOAA <soroosh.mani@noaa.gov>
1 parent e9e1ae2 commit ee51dd1

File tree

10 files changed

+6021
-5897
lines changed

10 files changed

+6021
-5897
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- uses: actions/checkout@main
2323
- uses: actions/setup-python@main
2424
with:
25-
python-version: '3.x'
25+
python-version: '3.9'
2626
- uses: actions/cache@main
2727
id: cache
2828
with:
@@ -41,7 +41,7 @@ jobs:
4141
strategy:
4242
matrix:
4343
os: [ ubuntu-latest, macos-latest ]
44-
python: [ '3.9', '3.10' ]
44+
python: [ '3.9', '3.10', '3.11' ]
4545
steps:
4646
- uses: actions/checkout@main
4747
- uses: actions/setup-python@main
@@ -62,7 +62,7 @@ jobs:
6262
- uses: actions/checkout@main
6363
- uses: actions/setup-python@main
6464
with:
65-
python-version: '3.x'
65+
python-version: '3.10'
6666
- uses: actions/cache@main
6767
id: cache
6868
with:

.github/workflows/test_quick.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- uses: actions/checkout@main
1818
- uses: actions/setup-python@main
1919
with:
20-
python-version: '3.x'
20+
python-version: '3.9'
2121
- uses: actions/cache@main
2222
id: cache
2323
with:
@@ -37,7 +37,7 @@ jobs:
3737
- uses: actions/checkout@main
3838
- uses: actions/setup-python@main
3939
with:
40-
python-version: '3.x'
40+
python-version: '3.9'
4141
- uses: actions/cache@main
4242
id: cache
4343
with:

stormevents/nhc/const.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from numpy import isnan, array, argwhere
2+
3+
# Regression coefficients for the Rmax forecast
4+
# ref: Penny et al. (2023). https://doi.org/10.1175/WAF-D-22-0209.1
5+
fhrs = [12, 24, 36, 48, 72, 96, 120]
6+
RMW_regression_coefs = {
7+
3: [ # a0 #a1 #a2 #a3 #a4 #a5 #a6
8+
[3.1894, 0.3524, 0.1208, -0.1091, 0.5862, -0.8070, 0.0057],
9+
[4.4373, 0.1473, 0.1045, -0.1112, 0.7566, -1.0689, 0.0061],
10+
[4.9447, 0.0784, 0.1168, -0.1448, 0.8246, -1.1709, 0.0059],
11+
[5.1818, 0.0549, 0.1335, -0.2345, 0.8972, -1.2038, 0.0063],
12+
],
13+
2: [ # a0 #a1 #a2 #a3 #a5 #a6
14+
[3.1131, 0.3680, 0.1589, 0.4710, -0.9111, 0.0068],
15+
[4.1567, 0.1834, 0.2085, 0.5873, -1.1841, 0.0073],
16+
[4.6694, 0.1062, 0.2330, 0.6295, -1.3122, 0.0074],
17+
[4.9434, 0.0459, 0.3027, 0.5828, -1.3675, 0.0079],
18+
[4.7906, 0.0157, 0.3953, 0.5321, -1.3617, 0.0067],
19+
],
20+
1: [ # a0 #a1 #a2 #a5 #a6
21+
[2.6272, 0.4230, 0.6320, -0.9117, 0.0064],
22+
[3.6525, 0.2142, 0.8222, -1.2158, 0.0082],
23+
[4.2822, 0.0884, 0.9059, -1.3656, 0.0091],
24+
[4.7700, -0.0042, 0.9225, -1.4349, 0.0102],
25+
[4.7307, -0.0365, 0.9153, -1.3882, 0.0086],
26+
],
27+
0: [ # a0 #a1 #a5 #a6
28+
[2.1633, 0.6360, -0.3314, 0.0154],
29+
[3.7884, 0.3953, -0.5738, 0.0219],
30+
[5.0213, 0.1999, -0.7481, 0.0276],
31+
[5.8092, 0.0615, -0.8508, 0.0318],
32+
[6.3321, -0.0362, -0.9079, 0.0343],
33+
[6.6181, 0.0041, -0.9599, 0.0295],
34+
[6.7073, -0.0028, -0.9478, 0.0257],
35+
],
36+
}
37+
38+
39+
def get_RMW_regression_coefs(fcst_hr, radii_values):
40+
num_radii_available = (~isnan(radii_values)).sum()
41+
coefs_by_radii_available = array(RMW_regression_coefs[num_radii_available])
42+
fcst_index = argwhere(fhrs == fcst_hr)
43+
if fcst_index.size == 0 or fcst_index > coefs_by_radii_available.shape[0] - 1:
44+
return coefs_by_radii_available[-1].flatten()
45+
else:
46+
return coefs_by_radii_available[fcst_index].flatten()

stormevents/nhc/track.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from stormevents.nhc.atcf import get_atcf_entry
3434
from stormevents.nhc.atcf import read_atcf
3535
from stormevents.nhc.storms import nhc_storms
36+
from stormevents.nhc.const import get_RMW_regression_coefs
3637
from stormevents.utilities import subset_time_interval
3738

3839

@@ -1204,7 +1205,7 @@ def separate_tracks(data: DataFrame) -> Dict[str, Dict[str, DataFrame]]:
12041205
track_data = advisory_data[
12051206
advisory_data["track_start_time"]
12061207
== pandas.to_datetime(track_start_time)
1207-
].sort_values("forecast_hours")
1208+
].sort_index()
12081209

12091210
tracks[advisory][
12101211
f"{pandas.to_datetime(track_start_time):%Y%m%dT%H%M%S}"
@@ -1235,6 +1236,9 @@ def correct_ofcl_based_on_carq_n_hollandb(
12351236
:return: dictionary of forecasts for each advisory (aside from best track ``BEST``, which only has one hindcast) with corrected OFCL
12361237
"""
12371238

1239+
def clamp(n, minn, maxn):
1240+
return max(min(maxn, n), minn)
1241+
12381242
ofcl_tracks = tracks["OFCL"]
12391243
carq_tracks = tracks["CARQ"]
12401244

@@ -1269,10 +1273,75 @@ def correct_ofcl_based_on_carq_n_hollandb(
12691273
mslp_missing = missing.iloc[:, 1]
12701274
radp_missing = missing.iloc[:, 2]
12711275

1272-
# fill OFCL maximum wind radius with the first entry from 0-hr CARQ
1273-
forecast.loc[mrd_missing, "radius_of_maximum_winds"] = carq_ref[
1274-
"radius_of_maximum_winds"
1276+
# fill OFCL maximum wind radius based on regression method from
1277+
# Penny et al. (2023). https://doi.org/10.1175/WAF-D-22-0209.1
1278+
isotach_radii = forecast[
1279+
[
1280+
"isotach_radius_for_NEQ",
1281+
"isotach_radius_for_SEQ",
1282+
"isotach_radius_for_NWQ",
1283+
"isotach_radius_for_SWQ",
1284+
]
12751285
]
1286+
isotach_radii[isotach_radii == 0] = pandas.NA
1287+
rmw0 = carq_ref["radius_of_maximum_winds"]
1288+
fcst_hrs = (forecast.loc[mrd_missing, "forecast_hours"]).unique()
1289+
rads = numpy.array([numpy.nan]) # initializing to make sure available
1290+
for fcst_hr in fcst_hrs:
1291+
fcst_index = forecast["forecast_hours"] == fcst_hr
1292+
if fcst_hr < 12:
1293+
rmw_ = rmw0
1294+
else:
1295+
vmax = forecast.loc[fcst_index, "max_sustained_wind_speed"].iloc[0]
1296+
if numpy.isnan(isotach_radii.loc[fcst_index].to_numpy()).all():
1297+
# if no isotach's are found, preserve the isotach(s) if Vmax is greater
1298+
if vmax > 50:
1299+
rads = rads[0 : min(2, len(rads))]
1300+
elif vmax > 34:
1301+
rads = rads[[0]]
1302+
else:
1303+
rads = numpy.array([numpy.nan])
1304+
else:
1305+
rads = numpy.nanmean(
1306+
isotach_radii.loc[fcst_index].to_numpy(), axis=1
1307+
)
1308+
coefs = get_RMW_regression_coefs(fcst_hr, rads)
1309+
lat = forecast.loc[fcst_index, "latitude"].iloc[0]
1310+
bases = numpy.hstack((1.0, rmw0, rads[~numpy.isnan(rads)], vmax, lat))
1311+
rmw_ = (bases[1:-1] ** coefs[1:-1]).prod() * numpy.exp(
1312+
(coefs[[0, -1]] * bases[[0, -1]]).sum()
1313+
) # bound RMW as per Penny et al. (2023)
1314+
forecast.loc[fcst_index, "radius_of_maximum_winds"] = clamp(
1315+
rmw_, 5.0, max(120.0, rmw0)
1316+
)
1317+
# apply 24-HR moving mean to unique datetimes
1318+
fcsthr_index = forecast["forecast_hours"].drop_duplicates().index
1319+
df_temp = forecast.loc[fcsthr_index].copy()
1320+
# make sure 60, 84, and 108 are added
1321+
fcsthrs_12hr = numpy.unique(
1322+
numpy.append(df_temp["forecast_hours"].values, [60, 84, 108])
1323+
)
1324+
rmw_12hr = numpy.interp(
1325+
fcsthrs_12hr, df_temp["forecast_hours"], df_temp["radius_of_maximum_winds"]
1326+
)
1327+
dt_12hr = pandas.to_datetime(
1328+
fcsthrs_12hr, unit="h", origin=df_temp["datetime"].iloc[0]
1329+
)
1330+
df_temp = DataFrame(
1331+
data={"forecast_hours": fcsthrs_12hr, "radius_of_maximum_winds": rmw_12hr},
1332+
index=dt_12hr,
1333+
)
1334+
rmw_rolling = df_temp.rolling(window="24.01 H", center=True, min_periods=1)[
1335+
"radius_of_maximum_winds"
1336+
].mean()
1337+
for valid_time, rmw in rmw_rolling.items():
1338+
valid_index = forecast["datetime"] == valid_time
1339+
if (
1340+
valid_index.sum() == 0
1341+
or forecast.loc[valid_index, "forecast_hours"].iloc[0] == 0
1342+
):
1343+
continue
1344+
forecast.loc[valid_index, "radius_of_maximum_winds"] = rmw
12761345

12771346
# fill OFCL background pressure with the first entry from 0-hr CARQ background pressure (at sea level)
12781347
forecast.loc[radp_missing, "background_pressure"] = carq_ref[

0 commit comments

Comments
 (0)