defmodule Microwaveprop.Weather.ThetaE do @moduledoc """ Equivalent potential temperature (θₑ) computation using the Bolton (1980) approximation. θₑ combines temperature and moisture into a single conserved variable that characterizes how "juicy" an air parcel is. A sharp vertical θₑ gradient across the inversion top means strong thermodynamic decoupling between the boundary layer and the free atmosphere — which the meteorologist identified as a first-class propagation predictor. All inputs are SI: T in Kelvin, specific humidity in kg/kg, P in Pa. """ @doc """ Compute equivalent potential temperature (K) from temperature, specific humidity, and pressure. Uses the Bolton (1980) formulation: θₑ = θ_dry × exp((Lv × r) / (cp × T_lcl)) where r is mixing ratio, T_lcl is the lifting condensation level temperature, and θ_dry is the dry potential temperature. """ @spec compute(float, float, float) :: float def compute(temp_k, spfh, pressure_pa) do # Guard: zero or negative humidity makes the log terms undefined. # Clamp to a tiny positive value (equivalent to extremely dry air). spfh = max(spfh, 1.0e-8) # Mixing ratio from specific humidity: r = q / (1 - q) r = spfh / (1.0 - spfh) # Water vapor pressure (Pa): e = q * P / (0.622 + 0.378 * q) e = spfh * pressure_pa / (0.622 + 0.378 * spfh) # Dry potential temperature: θ = T * (P0/P_dry)^(Rd/cp) # where P_dry = P - e p_dry = pressure_pa - e theta_dry = temp_k * :math.pow(100_000.0 / p_dry, 0.2854) # Bolton (1980) LCL temperature (Eq. 15) t_lcl = 2840.0 / (3.5 * :math.log(temp_k) - :math.log(e / 100.0) - 4.805) + 55.0 # Bolton (1980) θₑ (Eq. 38, simplified) lv_cp = 2.5e6 / 1005.7 theta_dry * :math.exp(lv_cp * r / t_lcl) end @doc """ Dewpoint temperature (K) from specific humidity and pressure. Computes water vapor pressure from (q, P), then inverts the Magnus-Tetens formula to get Td. """ @spec dewpoint_from_spfh(float, float) :: float def dewpoint_from_spfh(spfh, pressure_pa) do # e = q * P / (0.622 + 0.378 * q) in Pa e = spfh * pressure_pa / (0.622 + 0.378 * spfh) e_hpa = e / 100.0 # Invert Magnus-Tetens: Td(°C) = (243.04 * ln(e/6.1078)) / (17.625 - ln(e/6.1078)) ln_ratio = :math.log(e_hpa / 6.1078) td_c = 243.04 * ln_ratio / (17.625 - ln_ratio) td_c + 273.15 end @doc """ θₑ jump (K) between two levels in a profile. Returns θₑ(top) − θₑ(base). A large positive value means the inversion top is much warmer+drier (thermodynamically decoupled) than the air below — which is what the meteorologist says we need. """ @spec theta_e_jump(map, non_neg_integer, non_neg_integer) :: float | nil def theta_e_jump(profile, base_idx, top_idx) do with t_base when is_float(t_base) <- Enum.at(profile.temp_k, base_idx), t_top when is_float(t_top) <- Enum.at(profile.temp_k, top_idx), q_base when is_float(q_base) <- Enum.at(profile.spfh, base_idx), q_top when is_float(q_top) <- Enum.at(profile.spfh, top_idx), p_base when is_float(p_base) <- Enum.at(profile.pressure_pa, base_idx), p_top when is_float(p_top) <- Enum.at(profile.pressure_pa, top_idx) do compute(t_top, q_top, p_top) - compute(t_base, q_base, p_base) else _ -> nil end end end