Loss improved 72% (0.42 → 0.12). Key changes: - rain: 0.08 → 0.136 (+70%) — strongest discriminator - season: 0.08 → 0.111 (+39%) - wind: 0.05 → 0.08 (+60%) - refractivity: 0.08 → 0.105 (+31%) - time_of_day: 0.10 → 0.050 (-50%) — was overweighted by contest bias - pressure: 0.15 → 0.103 (-31%) - humidity: 0.18 → 0.124 (-31%) Validated by native profile backtest (11,431 profiles): theta_e_jump strongest native discriminator, duct_usable_* and bulk_richardson dropped as dead features.
291 lines
8.6 KiB
Elixir
291 lines
8.6 KiB
Elixir
defmodule Microwaveprop.Propagation.BandConfigTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Propagation.BandConfig
|
|
|
|
@all_freqs [902, 1_296, 2_304, 3_456, 5_760, 10_000, 24_000, 47_000, 68_000, 75_000, 122_000, 134_000, 241_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
|
|
end
|
|
|
|
describe "all_bands/0" do
|
|
test "returns 13 bands" do
|
|
bands = BandConfig.all_bands()
|
|
assert length(bands) == 13
|
|
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.1243
|
|
assert weights.time_of_day == 0.0496
|
|
assert weights.td_depression == 0.0978
|
|
assert weights.refractivity == 0.1049
|
|
assert weights.sky == 0.08
|
|
assert weights.season == 0.1112
|
|
assert weights.wind == 0.08
|
|
assert weights.rain == 0.1362
|
|
assert weights.pwat == 0.1128
|
|
assert weights.pressure == 0.1032
|
|
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: "#00ffa3"}
|
|
assert good == %{min_score: 65, label: "GOOD", color: "#7dffd4"}
|
|
assert marginal == %{min_score: 50, label: "MARGINAL", color: "#ffe566"}
|
|
assert poor == %{min_score: 33, label: "POOR", color: "#ff9044"}
|
|
assert negligible == %{min_score: 0, label: "NEGLIGIBLE", color: "#ff4f4f"}
|
|
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 = [902, 1_296, 2_304, 3_456, 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
|