prop/test/microwaveprop/propagation/sporadic_e_test.exs
Graham McIntire 36adf875b7
Add 50/144/222 MHz bands, rename 440→432
Expands submittable-contact bands to include 6m, 2m, 1.25m, and 70cm
(as 432 rather than the old 440 placeholder). Each new band gets an
explicit allocation window in BandResolver.nearest_band so ADIF FREQ
fields near the amateur allocations resolve correctly while 60-900 MHz
frequencies outside those windows are still rejected. Microwave
(>= 900 MHz) snapping is unchanged — nearest-band match across the
full @allowed_bands list.

Also adds BandConfig entries for 50 and 222 (tropo-only config, same
pattern as 144/432). Sporadic-E / F2 / meteor scatter modeling is not
yet in scope — ionosphere data is only used to compute an Es readout
on /path for 50/144/222/432.
2026-04-16 12:36:10 -05:00

86 lines
3.5 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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