Add 144 MHz and 440 MHz bands (tropo only)

Both bands share the physics of low microwave: humidity and refractivity
gradient build ducts the same way, and rain + gas absorption are
effectively zero at VHF/UHF. humidity_effect is :beneficial, rain_k /
rain_alpha are (0, 1), and o2_db_km / h2o_coeff are 0.

Range estimates reflect tropo reality: 2m ducts can reach 2500+ km
under exceptional conditions (e.g. documented Hawaii ↔ California
openings), 70cm a bit less. The seasonal base matches the low-microwave
summer-peak curve.

These configs do NOT capture sporadic-E, meteor scatter, or aurora —
those need ionospheric data (GIRO ionosondes, NOAA SWPC) we don't yet
ingest. The scoring on these bands is therefore "tropo-only" and will
underestimate openings driven by ionospheric modes.
This commit is contained in:
Graham McIntire 2026-04-15 14:22:32 -05:00
parent 1d0aa2848e
commit 91c8cc9bc7
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 102 additions and 5 deletions

View file

@ -48,6 +48,70 @@ defmodule Microwaveprop.Propagation.BandConfig do
@refractivity_default {42, 42}
@band_configs %{
# VHF / UHF bands share the tropo-ducting physics of low microwave
# (humidity and refractivity gradient build ducts the same way),
# so rain attenuation and gas absorption are effectively zero and
# humidity is :beneficial. The difference from 902+ is mostly in
# the achievable range — a 2m duct can reach 2500+ km under
# exceptional conditions (e.g. documented Hawaii ↔ California
# openings). These do NOT capture sporadic-E or meteor scatter,
# which need ionospheric data we don't yet ingest.
144 => %{
freq_mhz: 144,
label: "144 MHz",
o2_db_km: 0.0,
h2o_coeff: 0.0,
humidity_effect: :beneficial,
humidity_penalty: 0.0,
rain_k: 0.0,
rain_alpha: 1.0,
seasonal_base: %{
1 => 38,
2 => 40,
3 => 22,
4 => 55,
5 => 68,
6 => 90,
7 => 95,
8 => 75,
9 => 78,
10 => 88,
11 => 78,
12 => 25
},
seasonal_adj: %{},
typical_range_km: 350,
extended_range_km: 900,
exceptional_range_km: 2500
},
440 => %{
freq_mhz: 440,
label: "440 MHz",
o2_db_km: 0.0,
h2o_coeff: 0.0,
humidity_effect: :beneficial,
humidity_penalty: 0.0,
rain_k: 0.0,
rain_alpha: 1.0,
seasonal_base: %{
1 => 38,
2 => 40,
3 => 22,
4 => 55,
5 => 68,
6 => 90,
7 => 95,
8 => 75,
9 => 78,
10 => 88,
11 => 78,
12 => 25
},
seasonal_adj: %{},
typical_range_km: 300,
extended_range_km: 700,
exceptional_range_km: 1800
},
902 => %{
freq_mhz: 902,
label: "902 MHz",

View file

@ -4,6 +4,8 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
alias Microwaveprop.Propagation.BandConfig
@all_freqs [
144,
440,
902,
1_296,
2_304,
@ -116,6 +118,37 @@ defmodule Microwaveprop.Propagation.BandConfigTest 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 440 MHz (70cm UHF)" do
config = BandConfig.get(440)
assert config.freq_mhz == 440
assert config.label == "440 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
@ -150,9 +183,9 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
end
describe "all_bands/0" do
test "returns 19 bands (13 original + 142, 145, 288, 322, 403, 411 GHz)" do
test "returns 21 bands (13 original + 142/145/288/322/403/411 GHz + 144/440 MHz)" do
bands = BandConfig.all_bands()
assert length(bands) == 19
assert length(bands) == 21
end
test "bands are sorted by frequency" do
@ -328,7 +361,7 @@ defmodule Microwaveprop.Propagation.BandConfigTest do
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]
beneficial = [144, 440, 902, 1_296, 2_304, 3_456, 5_760, 10_000]
harmful = @all_freqs -- beneficial
for freq <- beneficial do

View file

@ -32,7 +32,7 @@ defmodule Microwaveprop.PropagationTest do
valid_time = ~U[2026-07-15 13:00:00Z]
results = Propagation.score_grid_point(hrrr_profile, valid_time, 33.0, -97.0)
assert length(results) == 19
assert length(results) == 21
Enum.each(results, fn result ->
assert result.score >= 0 and result.score <= 100
@ -98,7 +98,7 @@ defmodule Microwaveprop.PropagationTest do
valid_time = ~U[2026-07-15 13:00:00Z]
results = Propagation.score_grid_point(hrrr_profile_without_array, valid_time, 33.0, -97.0)
assert length(results) == 19
assert length(results) == 21
Enum.each(results, fn result ->
assert result.score >= 0 and result.score <= 100