defmodule Microwaveprop.Propagation.BandConfigTest do use ExUnit.Case, async: true alias Microwaveprop.Propagation.BandConfig @all_freqs [ 50, 144, 222, 432, 902, 1_296, 2_304, 3_400, 5_760, 10_000, 24_000, 47_000, 68_000, 75_000, 122_000, 134_000, 142_000, 145_000, 241_000, 288_000, 322_000, 403_000, 411_000 ] describe "get/1" do test "returns config for 10 GHz" do config = BandConfig.get(10_000) assert config.freq_mhz == 10_000 assert config.label == "10 GHz" assert config.humidity_effect == :beneficial assert config.humidity_penalty == 0.0 assert config.rain_k == 0.010 assert config.rain_alpha == 1.28 assert config.typical_range_km == 200 assert config.extended_range_km == 500 assert config.exceptional_range_km == 1000 end test "returns config for 24 GHz" do config = BandConfig.get(24_000) assert config.freq_mhz == 24_000 assert config.label == "24 GHz" assert config.humidity_effect == :harmful assert config.humidity_penalty == 1.6 assert config.rain_k == 0.070 assert config.rain_alpha == 1.07 assert config.typical_range_km == 100 assert config.extended_range_km == 250 assert config.exceptional_range_km == 500 end test "returns config for 47 GHz" do config = BandConfig.get(47_000) assert config.freq_mhz == 47_000 assert config.humidity_effect == :harmful assert config.humidity_penalty == 1.0 assert config.rain_k == 0.187 assert config.rain_alpha == 0.93 end test "returns config for 68 GHz with atmospheric absorption" do config = BandConfig.get(68_000) assert config.o2_db_km == 0.90 assert config.h2o_coeff == 0.007 assert config.humidity_penalty == 1.4 assert config.rain_k == 0.310 assert config.rain_alpha == 0.86 assert config.typical_range_km == 40 assert config.extended_range_km == 80 assert config.exceptional_range_km == 150 end test "returns config for 75 GHz" do config = BandConfig.get(75_000) assert config.o2_db_km == 0.012 assert config.h2o_coeff == 0.006 assert config.humidity_penalty == 1.2 assert config.rain_k == 0.345 assert config.rain_alpha == 0.84 end test "returns config for 122 GHz" do config = BandConfig.get(122_000) assert config.o2_db_km == 0.80 assert config.h2o_coeff == 0.010 assert config.humidity_penalty == 1.0 assert config.rain_k == 0.498 assert config.rain_alpha == 0.77 end test "returns config for 134 GHz" do config = BandConfig.get(134_000) assert config.o2_db_km == 0.08 assert config.h2o_coeff == 0.015 assert config.humidity_penalty == 1.3 assert config.rain_k == 0.520 assert config.rain_alpha == 0.75 end test "returns config for 241 GHz" do config = BandConfig.get(241_000) assert config.o2_db_km == 0.08 assert config.h2o_coeff == 0.30 assert config.humidity_penalty == 3.0 assert config.rain_k == 0.550 assert config.rain_alpha == 0.70 assert config.typical_range_km == 10 assert config.extended_range_km == 50 assert config.exceptional_range_km == 115 end test "returns nil for unknown band" do assert BandConfig.get(999) == nil end test "returns config for 144 MHz (2m VHF)" do config = BandConfig.get(144) assert config.freq_mhz == 144 assert config.label == "144 MHz" # Tropo ducting physics is the same as low microwave — humidity # builds refractivity, rain and atmospheric absorption are # negligible at VHF. assert config.humidity_effect == :beneficial assert config.humidity_penalty == 0.0 assert config.rain_k == 0.0 assert config.rain_alpha == 1.0 assert config.o2_db_km == 0.0 assert config.h2o_coeff == 0.0 # 2m tropo ducts can reach 2500+ km under exceptional conditions # (e.g. documented Hawaii ↔ California openings). assert config.exceptional_range_km >= 2000 end test "returns config for 432 MHz (70cm UHF)" do config = BandConfig.get(432) assert config.freq_mhz == 432 assert config.label == "432 MHz" assert config.humidity_effect == :beneficial assert config.humidity_penalty == 0.0 assert config.rain_k == 0.0 assert config.rain_alpha == 1.0 assert config.h2o_coeff == 0.0 # 70cm tropo is strong but slightly less extreme than 2m. assert config.exceptional_range_km >= 1500 end test "returns config for 142 GHz (between H2O 118 and 183 lines)" do config = BandConfig.get(142_000) assert config.freq_mhz == 142_000 assert config.humidity_effect == :harmful assert config.rain_alpha < 1.0 end test "returns config for 145 GHz" do config = BandConfig.get(145_000) assert config.freq_mhz == 145_000 assert config.humidity_effect == :harmful end test "returns config for 288 GHz (sub-mm window)" do config = BandConfig.get(288_000) assert config.freq_mhz == 288_000 assert config.humidity_effect == :harmful assert config.typical_range_km <= 10 end test "returns config for 322 GHz" do assert %{freq_mhz: 322_000, humidity_effect: :harmful} = BandConfig.get(322_000) end test "returns config for 403 GHz" do assert %{freq_mhz: 403_000, humidity_effect: :harmful} = BandConfig.get(403_000) end test "returns config for 411 GHz" do assert %{freq_mhz: 411_000, humidity_effect: :harmful} = BandConfig.get(411_000) end end describe "all_bands/0" do test "returns 23 bands (13 original + 142/145/288/322/403/411 GHz + 50/144/222/432 MHz)" do bands = BandConfig.all_bands() assert length(bands) == 23 end test "bands are sorted by frequency" do freqs = Enum.map(BandConfig.all_bands(), & &1.freq_mhz) assert freqs == @all_freqs end end describe "all_freqs/0" do test "returns sorted frequency list" do assert BandConfig.all_freqs() == @all_freqs end end describe "weights/0" do test "returns all 10 factor keys" do weights = BandConfig.weights() expected_keys = MapSet.new([ :humidity, :time_of_day, :td_depression, :refractivity, :sky, :season, :wind, :rain, :pwat, :pressure ]) assert MapSet.new(Map.keys(weights)) == expected_keys end test "weights sum to 1.0" do total = BandConfig.weights() |> Map.values() |> Enum.sum() assert_in_delta total, 1.0, 0.001 end test "individual weights have correct values" do weights = BandConfig.weights() assert weights.humidity == 0.1262 assert weights.time_of_day == 0.0380 assert weights.td_depression == 0.1010 assert weights.refractivity == 0.0986 assert weights.sky == 0.0841 assert weights.season == 0.1134 assert weights.wind == 0.0841 assert weights.rain == 0.1431 assert weights.pwat == 0.1147 assert weights.pressure == 0.0967 end end describe "weights/1 — band-aware" do test "returns the default weights when the band has no override" do # 50 MHz has 0 contacts in the corpus → no per-band fit → default weights. band = BandConfig.get(50) assert BandConfig.weights(band) == BandConfig.weights() end test "returns the override weights when the band has a :weights key" do band = %{freq_mhz: 99_999, weights: %{rain: 0.5, humidity: 0.5}} assert BandConfig.weights(band) == %{rain: 0.5, humidity: 0.5} end test "returns defaults when passed nil" do assert BandConfig.weights(nil) == BandConfig.weights() end end describe "sunrise_table/0" do test "returns 12 monthly values" do table = BandConfig.sunrise_table() assert length(table) == 12 end test "returns expected sunrise hours" do assert BandConfig.sunrise_table() == [ 7.4, 7.3, 7.0, 6.7, 6.35, 6.25, 6.35, 6.65, 6.9, 7.1, 7.35, 7.45 ] end end describe "tiers/0" do test "returns 5 tiers" do tiers = BandConfig.tiers() assert length(tiers) == 5 end test "tiers are ordered by threshold descending" do thresholds = Enum.map(BandConfig.tiers(), & &1.min_score) assert thresholds == [80, 65, 50, 33, 0] end test "tier labels and colors are correct" do tiers = BandConfig.tiers() [excellent, good, marginal, poor, negligible] = tiers assert excellent == %{min_score: 80, label: "EXCELLENT", color: "#059669"} assert good == %{min_score: 65, label: "GOOD", color: "#0d9488"} assert marginal == %{min_score: 50, label: "MARGINAL", color: "#ca8a04"} assert poor == %{min_score: 33, label: "POOR", color: "#ea580c"} assert negligible == %{min_score: 0, label: "NEGLIGIBLE", color: "#dc2626"} end end describe "seasonal bases" do test "every band has 12 months in seasonal_base" do for freq <- @all_freqs do config = BandConfig.get(freq) assert map_size(config.seasonal_base) == 12, "#{freq} missing months in seasonal_base" assert Enum.all?(1..12, &Map.has_key?(config.seasonal_base, &1)) end end test "10 GHz seasonal base peaks in summer" do config = BandConfig.get(10_000) assert config.seasonal_base[7] == 95 assert config.seasonal_base[3] == 22 end test "24 GHz seasonal base peaks in winter" do config = BandConfig.get(24_000) assert config.seasonal_base[11] == 96 assert config.seasonal_base[7] == 18 end test "24 GHz has seasonal adjustments" do config = BandConfig.get(24_000) assert config.seasonal_adj[7] == -10 assert config.seasonal_adj[5] == -4 end test "10 GHz has empty seasonal adjustments" do config = BandConfig.get(10_000) assert config.seasonal_adj == %{} end end describe "humidity_beneficial_thresholds/0" do test "returns threshold tuples sorted by hour" do thresholds = BandConfig.humidity_beneficial_thresholds() assert thresholds == [{4, 55}, {7, 70}, {10, 82}, {14, 90}, {18, 95}, {22, 88}] end end describe "humidity_beneficial_default/0" do test "returns 75" do assert BandConfig.humidity_beneficial_default() == 75 end end describe "refractivity_thresholds/0" do test "returns gradient/score tuples" do thresholds = BandConfig.refractivity_thresholds() assert thresholds == [ {-200, 98, 85}, {-150, 92, 80}, {-100, 82, 72}, {-75, 68, 62}, {-55, 55, 55}, {-40, 48, 48} ] end end describe "refractivity_default/0" do test "returns default scores" do assert BandConfig.refractivity_default() == {42, 42} end end describe "only 10 GHz is beneficial" do test "10 GHz has beneficial humidity effect" do assert BandConfig.get(10_000).humidity_effect == :beneficial end test "sub-10 GHz bands are beneficial, 24+ GHz are harmful" do beneficial = [50, 144, 222, 432, 902, 1_296, 2_304, 3_400, 5_760, 10_000] harmful = @all_freqs -- beneficial for freq <- beneficial do assert BandConfig.get(freq).humidity_effect == :beneficial, "#{freq} should be :beneficial" end for freq <- harmful do assert BandConfig.get(freq).humidity_effect == :harmful, "#{freq} should be :harmful" end end end end