|
33 | 33 | from stormevents.nhc.atcf import get_atcf_entry
|
34 | 34 | from stormevents.nhc.atcf import read_atcf
|
35 | 35 | from stormevents.nhc.storms import nhc_storms
|
| 36 | +from stormevents.nhc.const import get_RMW_regression_coefs |
36 | 37 | from stormevents.utilities import subset_time_interval
|
37 | 38 |
|
38 | 39 |
|
@@ -1204,7 +1205,7 @@ def separate_tracks(data: DataFrame) -> Dict[str, Dict[str, DataFrame]]:
|
1204 | 1205 | track_data = advisory_data[
|
1205 | 1206 | advisory_data["track_start_time"]
|
1206 | 1207 | == pandas.to_datetime(track_start_time)
|
1207 |
| - ].sort_values("forecast_hours") |
| 1208 | + ].sort_index() |
1208 | 1209 |
|
1209 | 1210 | tracks[advisory][
|
1210 | 1211 | 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(
|
1235 | 1236 | :return: dictionary of forecasts for each advisory (aside from best track ``BEST``, which only has one hindcast) with corrected OFCL
|
1236 | 1237 | """
|
1237 | 1238 |
|
| 1239 | + def clamp(n, minn, maxn): |
| 1240 | + return max(min(maxn, n), minn) |
| 1241 | + |
1238 | 1242 | ofcl_tracks = tracks["OFCL"]
|
1239 | 1243 | carq_tracks = tracks["CARQ"]
|
1240 | 1244 |
|
@@ -1269,10 +1273,75 @@ def correct_ofcl_based_on_carq_n_hollandb(
|
1269 | 1273 | mslp_missing = missing.iloc[:, 1]
|
1270 | 1274 | radp_missing = missing.iloc[:, 2]
|
1271 | 1275 |
|
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 | + ] |
1275 | 1285 | ]
|
| 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 |
1276 | 1345 |
|
1277 | 1346 | # fill OFCL background pressure with the first entry from 0-hr CARQ background pressure (at sea level)
|
1278 | 1347 | forecast.loc[radp_missing, "background_pressure"] = carq_ref[
|
|
0 commit comments