defmodule Microwaveprop.Weather.WeatherLayersTest do use ExUnit.Case, async: true alias Microwaveprop.Weather.SoundingParams alias Microwaveprop.Weather.WeatherLayers @sample_profile [ %{"pres" => 1000.0, "tmpc" => 28.0, "dwpc" => 20.0, "hght" => 100.0}, %{"pres" => 975.0, "tmpc" => 26.0, "dwpc" => 18.0, "hght" => 340.0}, %{"pres" => 950.0, "tmpc" => 24.0, "dwpc" => 16.0, "hght" => 580.0}, %{"pres" => 925.0, "tmpc" => 22.0, "dwpc" => 14.0, "hght" => 830.0}, %{"pres" => 900.0, "tmpc" => 20.0, "dwpc" => 12.0, "hght" => 1090.0}, %{"pres" => 875.0, "tmpc" => 18.0, "dwpc" => 10.0, "hght" => 1360.0}, %{"pres" => 850.0, "tmpc" => 16.0, "dwpc" => 8.0, "hght" => 1640.0}, %{"pres" => 825.0, "tmpc" => 14.0, "dwpc" => 6.0, "hght" => 1930.0}, %{"pres" => 800.0, "tmpc" => 12.0, "dwpc" => 4.0, "hght" => 2230.0}, %{"pres" => 775.0, "tmpc" => 10.0, "dwpc" => 2.0, "hght" => 2540.0}, %{"pres" => 750.0, "tmpc" => 8.0, "dwpc" => 0.0, "hght" => 2860.0}, %{"pres" => 725.0, "tmpc" => 6.0, "dwpc" => -2.0, "hght" => 3200.0}, %{"pres" => 700.0, "tmpc" => 4.0, "dwpc" => -4.0, "hght" => 3550.0} ] @inversion_profile [ %{"pres" => 1000.0, "tmpc" => 28.0, "dwpc" => 20.0, "hght" => 100.0}, %{"pres" => 975.0, "tmpc" => 26.0, "dwpc" => 18.0, "hght" => 340.0}, %{"pres" => 950.0, "tmpc" => 22.0, "dwpc" => 14.0, "hght" => 580.0}, %{"pres" => 925.0, "tmpc" => 25.0, "dwpc" => 10.0, "hght" => 830.0}, %{"pres" => 900.0, "tmpc" => 20.0, "dwpc" => 8.0, "hght" => 1090.0} ] defp sample_row(overrides \\ %{}) do Map.merge( %{ surface_temp_c: 28.0, surface_dewpoint_c: 20.0, surface_pressure_mb: 1000.0, surface_refractivity: 350.0, profile: @sample_profile, duct_characteristics: nil }, overrides ) end describe "derive/1 surface relative humidity" do test "computes surface RH from temp and dewpoint using Buck equation" do result = WeatherLayers.derive(sample_row()) expected_rh = 100.0 * SoundingParams.sat_vap_pres(20.0) / SoundingParams.sat_vap_pres(28.0) assert_in_delta result.surface_rh, expected_rh, 0.01 end end describe "derive/1 850mb extraction" do test "extracts temperature at 850mb from profile" do result = WeatherLayers.derive(sample_row()) assert result.temp_850mb == 16.0 end test "extracts dewpoint at 850mb from profile" do result = WeatherLayers.derive(sample_row()) assert result.dewpoint_850mb == 8.0 end end describe "derive/1 700mb extraction (cap diagnostics)" do test "extracts temperature at 700mb from profile" do result = WeatherLayers.derive(sample_row()) # 700 mb level in @sample_profile sits at 4.0 °C assert result.temp_700mb == 4.0 end test "extracts dewpoint at 700mb from profile" do result = WeatherLayers.derive(sample_row()) assert result.dewpoint_700mb == -4.0 end test "returns nil when profile does not reach 700 mb" do shallow = [ %{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0}, %{"pres" => 925.0, "tmpc" => 14.0, "dwpc" => 8.0, "hght" => 800.0} ] result = WeatherLayers.derive(sample_row(%{profile: shallow})) assert result.temp_700mb == nil assert result.dewpoint_700mb == nil end end describe "derive/1 mid-layer lapse rate (850-700 cap mechanism)" do test "computes lapse rate over the 850-700 mb layer in C/km" do result = WeatherLayers.derive(sample_row()) # 850 mb: 16.0 °C @ 1640 m, 700 mb: 4.0 °C @ 3550 m # dT/dh = (16.0 - 4.0) / ((3550 - 1640) / 1000) = 12.0 / 1.91 ≈ 6.28 expected = (16.0 - 4.0) / ((3550.0 - 1640.0) / 1000.0) assert_in_delta result.mid_lapse_rate, expected, 0.01 end test "returns nil when one of the bracketing levels is missing" do shallow = [ %{"pres" => 1000.0, "tmpc" => 20.0, "dwpc" => 15.0, "hght" => 100.0}, %{"pres" => 925.0, "tmpc" => 14.0, "dwpc" => 8.0, "hght" => 800.0} ] result = WeatherLayers.derive(sample_row(%{profile: shallow})) assert result.mid_lapse_rate == nil end end describe "derive/1 lapse rate" do test "computes lapse rate in C/km from surface to top of profile" do result = WeatherLayers.derive(sample_row()) # (28.0 - 4.0) / ((3550.0 - 100.0) / 1000.0) = 24.0 / 3.45 = ~6.957 expected = (28.0 - 4.0) / ((3550.0 - 100.0) / 1000.0) assert_in_delta result.lapse_rate, expected, 0.01 end end describe "derive/1 inversion detection" do test "detects no inversion in standard lapse profile" do result = WeatherLayers.derive(sample_row()) assert result.inversion_strength == 0.0 end test "detects inversion strength and base height" do result = WeatherLayers.derive(sample_row(%{profile: @inversion_profile})) # Inversion: 950mb (22C, 580m) -> 925mb (25C, 830m), strength = 3.0 assert_in_delta result.inversion_strength, 3.0, 0.01 # Base height AGL: 580m - 100m (surface) = 480m assert_in_delta result.inversion_base_m, 480.0, 0.01 end end describe "derive/1 duct characteristics" do test "extracts duct base and strength from duct_characteristics" do ducts = [ %{"base" => 500.0, "top" => 800.0, "strength" => 12.5}, %{"base" => 200.0, "top" => 400.0, "strength" => 8.0} ] result = WeatherLayers.derive(sample_row(%{duct_characteristics: ducts})) # min base assert result.duct_base_m == 200.0 # max strength assert result.duct_strength == 12.5 end test "handles nil duct_characteristics" do result = WeatherLayers.derive(sample_row(%{duct_characteristics: nil})) assert result.duct_base_m == nil assert result.duct_strength == nil end end describe "derive/1 nil/empty profile" do test "handles nil profile gracefully" do result = WeatherLayers.derive(sample_row(%{profile: nil})) assert result.temp_850mb == nil assert result.dewpoint_850mb == nil assert result.temp_700mb == nil assert result.dewpoint_700mb == nil assert result.lapse_rate == nil assert result.mid_lapse_rate == nil assert result.inversion_strength == 0.0 assert result.inversion_base_m == nil end test "handles empty profile gracefully" do result = WeatherLayers.derive(sample_row(%{profile: []})) assert result.temp_850mb == nil assert result.dewpoint_850mb == nil assert result.temp_700mb == nil assert result.dewpoint_700mb == nil assert result.lapse_rate == nil assert result.mid_lapse_rate == nil assert result.inversion_strength == 0.0 assert result.inversion_base_m == nil end end describe "derive/1 surface refractivity passthrough" do test "passes through surface_refractivity unchanged" do result = WeatherLayers.derive(sample_row(%{surface_refractivity: 342.7})) assert result.surface_refractivity == 342.7 end end end