defmodule Microwaveprop.Weather.SkewtParamsTest do use ExUnit.Case, async: true alias Microwaveprop.Weather.SkewtParams # A moderately unstable summer-like profile. Surface ≈ 30 °C / 22 °C # dewpoint, gradually drier and colder aloft. Used as a baseline for # LCL/LFC/EL/CAPE assertions. @summer_profile [ %{"pres" => 1000.0, "hght" => 100.0, "tmpc" => 30.0, "dwpc" => 22.0}, %{"pres" => 925.0, "hght" => 800.0, "tmpc" => 24.0, "dwpc" => 19.0}, %{"pres" => 850.0, "hght" => 1500.0, "tmpc" => 18.0, "dwpc" => 14.0}, %{"pres" => 700.0, "hght" => 3100.0, "tmpc" => 8.0, "dwpc" => -2.0}, %{"pres" => 500.0, "hght" => 5800.0, "tmpc" => -10.0, "dwpc" => -25.0}, %{"pres" => 300.0, "hght" => 9300.0, "tmpc" => -38.0, "dwpc" => -55.0}, %{"pres" => 200.0, "hght" => 12_000.0, "tmpc" => -60.0, "dwpc" => -80.0} ] # A capped / stable profile: subsidence inversion above the surface # mixed layer. Should produce 0 CAPE. @capped_profile [ %{"pres" => 1000.0, "hght" => 100.0, "tmpc" => 25.0, "dwpc" => 18.0}, %{"pres" => 925.0, "hght" => 800.0, "tmpc" => 28.0, "dwpc" => 5.0}, %{"pres" => 850.0, "hght" => 1500.0, "tmpc" => 25.0, "dwpc" => -5.0}, %{"pres" => 700.0, "hght" => 3100.0, "tmpc" => 12.0, "dwpc" => -15.0}, %{"pres" => 500.0, "hght" => 5800.0, "tmpc" => -8.0, "dwpc" => -30.0} ] describe "lcl/3" do test "returns LCL pressure and temperature consistent with Bolton (1980)" do # Surface 30°C / 22°C dewpoint at 1000 mb → LCL ~880-920 mb, # T_LCL ~17-22°C. Tolerances stay loose because Bolton is an # approximation and the surface RH already drives most of the # answer. {p_lcl, t_lcl} = SkewtParams.lcl(30.0, 22.0, 1000.0) assert p_lcl >= 850.0 and p_lcl <= 940.0 assert t_lcl >= 16.0 and t_lcl <= 23.0 end test "LCL pressure equals surface pressure when air is saturated" do {p_lcl, t_lcl} = SkewtParams.lcl(20.0, 20.0, 1000.0) assert_in_delta p_lcl, 1000.0, 1.0 assert_in_delta t_lcl, 20.0, 0.2 end end describe "derive/1" do test "produces every field documented in the public spec for a non-trivial profile" do params = SkewtParams.derive(@summer_profile) # Surface-based parcel parameters present. assert is_map(params) assert is_number(params[:lcl_pressure_mb]) assert is_number(params[:lcl_height_m]) assert is_number(params[:sbcape]) assert is_number(params[:sbcin]) assert is_number(params[:lifted_index]) # LFC/EL may be nil if no positive area exists — but for this # unstable summer profile they must resolve. assert is_number(params[:lfc_pressure_mb]) assert is_number(params[:el_pressure_mb]) # Cold-side milestones. assert is_number(params[:freezing_level_m]) assert is_number(params[:wbz_m]) # Layer lapse rates. assert is_number(params[:lapse_rate_700_500_c_per_km]) assert is_number(params[:lapse_rate_sfc_3km_c_per_km]) # Parcel trajectory used by SkewtSvg to draw the dashed grey line. assert is_list(params[:parcel_trace]) assert length(params[:parcel_trace]) >= 2 end test "summer profile is unstable: SBCAPE > 0, SBCIN <= 0, LFC < surface" do params = SkewtParams.derive(@summer_profile) assert params[:sbcape] > 0 # CIN by convention is negative (or zero). assert params[:sbcin] <= 0 assert params[:lfc_pressure_mb] < 1000.0 assert params[:el_pressure_mb] < params[:lfc_pressure_mb] # Lifted index < 0 for an unstable summer atmosphere. assert params[:lifted_index] < 0 end test "capped profile is stable: SBCAPE = 0, no LFC" do params = SkewtParams.derive(@capped_profile) assert params[:sbcape] == 0.0 assert params[:lfc_pressure_mb] == nil assert params[:el_pressure_mb] == nil # Lifted index > 0 in a stable atmosphere. assert params[:lifted_index] > 0 end test "freezing level is the height where T crosses 0 °C" do params = SkewtParams.derive(@summer_profile) # Profile goes 30→24→18→8→-10°C between 100 m and 5800 m. # 0°C falls between 700 mb (8°C, 3100 m) and 500 mb (-10°C, 5800 m). # Linear interpolation puts it ~4300 m. assert params[:freezing_level_m] >= 3700 assert params[:freezing_level_m] <= 4900 end test "lapse rate sfc-3km is the (T_sfc - T_3km) / 3 km gradient" do params = SkewtParams.derive(@summer_profile) # 30 °C at 100 m, ~9 °C at 3100 m → ~7 °C/km drop. assert params[:lapse_rate_sfc_3km_c_per_km] >= 5.5 assert params[:lapse_rate_sfc_3km_c_per_km] <= 9.0 end test "empty profile returns a fully-nil parameter map (no crash)" do params = SkewtParams.derive([]) assert is_map(params) assert params[:sbcape] == nil assert params[:lcl_pressure_mb] == nil assert params[:parcel_trace] == [] end end end