Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 4m38s
- F-level (228): replace length/1 with Enum.count_until/2 or pattern matching; convert Enum.flat_map+if to Enum.filter+Enum.map; fix identity case in narr_client - W-level (46): normalize dual atom/string key access in weather_layers, beacon_measurements, surface, skewt_live, contact_live/show; add :data_provider and weather-map assigns to ignored_assigns in credo config (consumed by child components credo can't trace); remove weak is_list assertion; remove explicit assert_receive timeout - R-level (6): replace 'This module provides...' moduledocs with meaningful descriptions - Also fix 4 compile-connected xref issues by deferring BandConfig.band_options() from module attribute to runtime Co-Authored-By: Claude <noreply@anthropic.com>
117 lines
4.9 KiB
Elixir
117 lines
4.9 KiB
Elixir
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 Map.has_key?(params, :lcl_pressure_mb)
|
|
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 Enum.count_until(params[:parcel_trace], 2) == 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 Map.has_key?(params, :sbcape)
|
|
assert params[:sbcape] == nil
|
|
assert params[:lcl_pressure_mb] == nil
|
|
assert params[:parcel_trace] == []
|
|
end
|
|
end
|
|
end
|