prop/lib/microwaveprop/weather/theta_e.ex

88 lines
3.3 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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