defmodule Microwaveprop.Propagation.SporadicETest do use ExUnit.Case, async: true alias Microwaveprop.Propagation.SporadicE describe "single_hop_muf/2" do test "returns 0.0 for nil or non-positive inputs" do assert SporadicE.single_hop_muf(nil, 2000) == 0.0 assert SporadicE.single_hop_muf(5.0, nil) == 0.0 assert SporadicE.single_hop_muf(0.0, 2000) == 0.0 assert SporadicE.single_hop_muf(5.0, 0) == 0.0 assert SporadicE.single_hop_muf(-1.0, 2000) == 0.0 end test "matches the thin-layer secant-of-incidence formula for known distances" do # h = 110 km. sec(i) = sqrt(1 + (D/(2h))^2). Values cross-checked # against literature (Davies 1990, Ionospheric Radio Eq. 6.26). # At D=2000 km, sec(i) ≈ 9.146, so foEs × 9.146 MHz. assert_in_delta SporadicE.single_hop_muf(10.0, 2000), 10.0 * 9.146, 0.05 assert_in_delta SporadicE.single_hop_muf(5.0, 1000), 5.0 * 4.654, 0.05 assert_in_delta SporadicE.single_hop_muf(8.0, 1500), 8.0 * 6.891, 0.05 end test "degenerate short range approaches foEs (vertical incidence)" do # As D → 0 the ray is nearly vertical and MUF → foEs. assert_in_delta SporadicE.single_hop_muf(5.0, 1), 5.0, 0.001 end end describe "es_score/3" do test "is 0 when foEs is nil or zero" do assert SporadicE.es_score(nil, 144, 2000) == 0 assert SporadicE.es_score(0.0, 144, 2000) == 0 end test "is 0 for paths shorter than the single-hop minimum" do # Es geometry doesn't support paths much under ~500 km (the # elevation angle is too steep for the layer to reflect a VHF # signal regardless of foEs). assert SporadicE.es_score(20.0, 144, 100) == 0 assert SporadicE.es_score(20.0, 144, 400) == 0 end test "is 0 for paths beyond the single-hop maximum (~2500 km)" do # Beyond ~2500 km the geometry exits the single-hop window. We # don't model multi-hop Es here — that's a different factor. assert SporadicE.es_score(20.0, 144, 3000) == 0 end test "scores 50 MHz as ROUTINE when foEs is typical summer Es (6-8 MHz) at 2000 km" do # foEs 6 MHz × sec(i) at 2000 km ≈ 54.9 MHz, just above 50 MHz. # 50 MHz Es at 2000 km with foEs=6 is a classic summer opening — # should score high. score = SporadicE.es_score(6.0, 50, 2000) assert score >= 80 end test "scores 144 MHz as NONE when foEs is typical summer Es" do # foEs=6 at 2000 km ≈ 54.9 MHz MUF — well below 144 MHz. # Typical summer Es doesn't propagate 2m. assert SporadicE.es_score(6.0, 144, 2000) == 0 end test "scores 144 MHz as VIABLE when foEs reaches intense-Es levels (16+ MHz) at 2000 km" do # foEs=16 × 9.15 ≈ 146 MHz MUF → 2m Es is feasible. These are # rare but documented events (June/July peaks). score = SporadicE.es_score(16.0, 144, 2000) assert score >= 50 end test "scores 432 MHz as NONE even at extreme foEs (sporadic-E does not reach 70cm)" do # foEs would need to be ~47 MHz to get 432 MHz via single-hop Es # — way beyond any physically observed value. assert SporadicE.es_score(25.0, 432, 2000) == 0 end test "higher foEs strictly increases the score at a given band/distance (monotonic)" do weak = SporadicE.es_score(10.0, 50, 2000) mid = SporadicE.es_score(14.0, 50, 2000) strong = SporadicE.es_score(20.0, 50, 2000) assert weak <= mid assert mid <= strong assert strong == 100 end end end