defmodule Microwaveprop.Propagation.ScorerTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Propagation.Scorer @contact %{ utc_hour: 18, utc_minute: 30, month: 6, longitude: -97.0, latitude: 32.9, sky_cover_pct: 25.0, wind_speed_kts: 5.0, pressure_mb: 1013.0, prev_pressure_mb: 1015.0, abs_humidity: 10.0, temp_f: 80.0, dewpoint_f: 60.0, min_refractivity_gradient: -150.0, bl_depth_m: 500.0, rain_rate_mmhr: 0.0 } describe "score/2" do test "returns a map with score, contributions, breakdown" do result = Scorer.score(@contact, 10_000) assert is_integer(result.score) assert result.score >= 0 and result.score <= 100 assert result.contributions != [] assert Map.has_key?(result, :breakdown) end test "score increases with favorable conditions" do # Same month, same location — dawn inversion peak scores higher than afternoon mixing. # At longitude -97.0 the solar offset is ~-6.5 h, so 13 UTC ≈ 6:30 local (peak inversion) # and 21 UTC ≈ 14:30 local (deep convective mixing). good_contact = Map.merge(@contact, %{utc_hour: 13, utc_minute: 0}) bad_contact = Map.merge(@contact, %{utc_hour: 21, utc_minute: 0}) good_score = Scorer.score(good_contact, 10_000) bad_score = Scorer.score(bad_contact, 10_000) assert good_score.score > bad_score.score end test "band config selection picks correct humidity effect" do result_10g = Scorer.score(@contact, 10_000) result_24g = Scorer.score(@contact, 24_000) assert result_10g.band_mhz == 10_000 assert result_24g.band_mhz == 24_000 end test "score returns nil for invalid band" do assert Scorer.score(@contact, 999) == nil end end describe "path_integrated_conditions/2" do test "averages scalar fields and picks worst pressure / strongest gradient" do profiles = [ %{ surface_temp_c: 25.0, surface_dewpoint_c: 15.0, surface_pressure_mb: 1010.0, min_refractivity_gradient: -300.0, hpbl_m: 500.0, pwat_mm: 20.0 }, %{ surface_temp_c: 25.0, surface_dewpoint_c: 15.0, surface_pressure_mb: 1020.0, min_refractivity_gradient: -400.0, hpbl_m: 1500.0, pwat_mm: 30.0 } ] cond_map = Scorer.path_integrated_conditions(profiles, @contact) assert %{temp_f: _} = cond_map # Averages carry through to Fahrenheit equivalents. assert_in_delta cond_map.temp_f, 77.0, 0.1 assert_in_delta cond_map.dewpoint_f, 59.0, 0.1 # min() picks the worst pressure and the strongest (most-negative) # refractivity gradient. assert cond_map.pressure_mb == 1010.0 assert cond_map.min_refractivity_gradient == -400.0 # avg() smooths HPBL + PWAT. assert cond_map.bl_depth_m == 1000.0 assert cond_map.pwat_mm == 25.0 # Time/position fields come from the contact. assert cond_map.utc_hour == 18 assert cond_map.utc_minute == 30 assert cond_map.month == 6 assert cond_map.longitude == -97.0 # Fields that the path integration can't infer from HRRR are nil. assert cond_map.wind_speed_kts == nil assert cond_map.sky_cover_pct == nil assert cond_map.prev_pressure_mb == nil assert cond_map.rain_rate_mmhr == 0.0 end test "returns nil when every profile is missing surface temperature" do profiles = [ %{ surface_temp_c: nil, surface_dewpoint_c: 10.0, surface_pressure_mb: 1020.0, min_refractivity_gradient: -200.0, hpbl_m: 500.0, pwat_mm: 20.0 } ] assert Scorer.path_integrated_conditions(profiles, @contact) == nil end test "returns nil when every profile is missing surface dewpoint" do profiles = [ %{ surface_temp_c: 20.0, surface_dewpoint_c: nil, surface_pressure_mb: 1020.0, min_refractivity_gradient: -200.0, hpbl_m: 500.0, pwat_mm: 20.0 } ] assert Scorer.path_integrated_conditions(profiles, @contact) == nil end test "nil profile list yields nil" do assert Scorer.path_integrated_conditions([], @contact) == nil end test "tolerates profiles with missing non-critical scalars (pressure, gradient, HPBL, PWAT)" do # Only temp + dewpoint are required; everything else can be nil # across all profiles and the conditions map still comes back # with nil / avg-over-empty for those fields. profiles = [ %{ surface_temp_c: 22.0, surface_dewpoint_c: 15.0, surface_pressure_mb: nil, min_refractivity_gradient: nil, hpbl_m: nil, pwat_mm: nil } ] cond_map = Scorer.path_integrated_conditions(profiles, @contact) assert %{temp_f: _} = cond_map assert cond_map.pressure_mb == nil assert cond_map.min_refractivity_gradient == nil assert cond_map.bl_depth_m == nil assert cond_map.pwat_mm == nil end test "uses -97.0 as longitude when contact.pos1 is missing the lon key" do # Stale contacts written before pos1 was normalised can lack a # lon field. The function falls back to CONUS-centre (-97.0). profiles = [ %{ surface_temp_c: 22.0, surface_dewpoint_c: 15.0, surface_pressure_mb: 1015.0, min_refractivity_gradient: -200.0, hpbl_m: 1000.0, pwat_mm: 25.0 } ] contact_no_lon = %{utc_hour: 12, utc_minute: 0, month: 3, latitude: 32.9} cond_map = Scorer.path_integrated_conditions(profiles, contact_no_lon) assert cond_map.longitude == -97.0 end end end