snesbitt's picture
Initial commit — UpdraftForcing Dash app
87602e0
Raw
History Blame Contribute Delete
4.93 kB
"""Sounding and storm-scale diagnostic calculations."""
from __future__ import annotations
import numpy as np
def bunkers_storm_motion(
z: np.ndarray,
u: np.ndarray,
v: np.ndarray,
z_top_km: float = 6.0,
deviation_ms: float = 7.5,
) -> dict:
"""Bunkers et al. (2000) right- and left-mover storm motion.
Uses the 0–``z_top_km`` mean wind plus ±``deviation_ms`` m/s
perpendicular to the 0–``z_top_km`` shear vector.
Returns dict: rm_u, rm_v (right mover), lm_u, lm_v (left mover),
mean_u, mean_v.
"""
z = np.asarray(z, dtype=float)
u = np.asarray(u, dtype=float)
v = np.asarray(v, dtype=float)
mask = z <= z_top_km * 1000.0
if mask.sum() < 2:
return {"rm_u": 0.0, "rm_v": 0.0, "lm_u": 0.0, "lm_v": 0.0,
"mean_u": 0.0, "mean_v": 0.0}
mean_u = float(np.trapezoid(u[mask], z[mask]) / (z[mask][-1] - z[mask][0]))
mean_v = float(np.trapezoid(v[mask], z[mask]) / (z[mask][-1] - z[mask][0]))
shr_u = float(u[mask][-1] - u[mask][0])
shr_v = float(v[mask][-1] - v[mask][0])
mag = max(np.hypot(shr_u, shr_v), 1e-6)
# Right mover: + deviation perpendicular to shear (rotated +90°)
rm_u = mean_u + deviation_ms * shr_v / mag
rm_v = mean_v - deviation_ms * shr_u / mag
# Left mover: − deviation
lm_u = mean_u - deviation_ms * shr_v / mag
lm_v = mean_v + deviation_ms * shr_u / mag
return {
"rm_u": rm_u, "rm_v": rm_v,
"lm_u": lm_u, "lm_v": lm_v,
"mean_u": mean_u, "mean_v": mean_v,
}
def srh(
z: np.ndarray,
u: np.ndarray,
v: np.ndarray,
storm_u: float,
storm_v: float,
z_bot_m: float,
z_top_m: float,
) -> float:
"""Storm-Relative Helicity (m² s⁻²) for the specified layer.
Uses the discrete hodograph-area formula:
SRH = Σ [(u_{n+1} − cu)(v_n − cv) − (u_n − cu)(v_{n+1} − cv)]
"""
z = np.asarray(z, dtype=float)
u = np.asarray(u, dtype=float)
v = np.asarray(v, dtype=float)
mask = (z >= z_bot_m) & (z <= z_top_m)
u_l = u[mask] - storm_u
v_l = v[mask] - storm_v
if len(u_l) < 2:
return 0.0
return float(np.sum((u_l[1:] - u_l[:-1]) * (v_l[1:] + v_l[:-1]) -
(v_l[1:] - v_l[:-1]) * (u_l[1:] + u_l[:-1])) * 0.5)
def updraft_helicity(
w3d: np.ndarray,
zeta3d: np.ndarray,
z: np.ndarray,
z_bot_m: float,
z_top_m: float,
) -> float:
"""Updraft Helicity UH = ∫ w ζ_z dz over the updraft core center column."""
z = np.asarray(z, dtype=float)
mask = (z >= z_bot_m) & (z <= z_top_m)
if mask.sum() < 2:
return 0.0
cx = w3d.shape[0] // 2
cy = w3d.shape[1] // 2
w_col = w3d[cx, cy, mask]
z_col = zeta3d[cx, cy, mask]
return float(np.trapezoid(w_col * z_col, z[mask]))
def bulk_wind_shear(z: np.ndarray, u: np.ndarray, v: np.ndarray,
z_bot_m: float, z_top_m: float) -> float:
"""Bulk wind shear magnitude (m/s) in a layer."""
z = np.asarray(z, dtype=float)
u = np.asarray(u, dtype=float)
v = np.asarray(v, dtype=float)
u_bot = float(np.interp(z_bot_m, z, u))
v_bot = float(np.interp(z_bot_m, z, v))
u_top = float(np.interp(z_top_m, z, u))
v_top = float(np.interp(z_top_m, z, v))
return float(np.hypot(u_top - u_bot, v_top - v_bot))
def collect_diagnostics(
z: np.ndarray,
snd: dict,
parcel: dict,
u_hodo: np.ndarray,
v_hodo: np.ndarray,
z_hodo: np.ndarray,
w3d: np.ndarray,
zeta3d: np.ndarray,
) -> dict:
"""Compute and return all sounding/storm-scale diagnostics as a flat dict."""
z_hodo = np.asarray(z_hodo, dtype=float) * 1000.0 # km → m
u_env = np.interp(z, z_hodo, np.asarray(u_hodo, dtype=float))
v_env = np.interp(z, z_hodo, np.asarray(v_hodo, dtype=float))
bunk = bunkers_storm_motion(z_hodo, u_hodo, v_hodo)
srh_02 = srh(z_hodo, u_hodo, v_hodo, bunk["rm_u"], bunk["rm_v"], 0.0, 2000.0)
srh_25 = srh(z_hodo, u_hodo, v_hodo, bunk["rm_u"], bunk["rm_v"], 2000.0, 5000.0)
uh_02 = updraft_helicity(w3d, zeta3d, z, 0.0, 2000.0)
uh_25 = updraft_helicity(w3d, zeta3d, z, 2000.0, 5000.0)
bws_06 = bulk_wind_shear(z_hodo, u_hodo, v_hodo, 0.0, 6000.0)
cx = w3d.shape[0] // 2
cy = w3d.shape[1] // 2
w_max = float(np.max(w3d[cx, cy, :]))
return {
"CAPE": parcel["CAPE"],
"CIN": parcel["CIN"],
"LCL_m": parcel["LCL_m"],
"LFC_m": parcel["LFC_m"],
"EL_m": parcel["EL_m"],
"z_top_m": parcel.get("z_top_m", parcel["EL_m"]),
"SRH_02": srh_02,
"SRH_25": srh_25,
"UH_02": uh_02,
"UH_25": uh_25,
"BWS_06": bws_06,
"w_max": w_max,
"storm_u": bunk["rm_u"],
"storm_v": bunk["rm_v"],
"lm_u": bunk["lm_u"],
"lm_v": bunk["lm_v"],
"mean_u": bunk["mean_u"],
"mean_v": bunk["mean_v"],
}