88 lines
3.3 KiB
Elixir
88 lines
3.3 KiB
Elixir
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
|