From 91c8cc9bc710e09c61ae104d14ccbf05972a9893 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 15 Apr 2026 14:22:32 -0500 Subject: [PATCH] Add 144 MHz and 440 MHz bands (tropo only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/microwaveprop/propagation/band_config.ex | 64 +++++++++++++++++++ .../propagation/band_config_test.exs | 39 ++++++++++- test/microwaveprop/propagation_test.exs | 4 +- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/lib/microwaveprop/propagation/band_config.ex b/lib/microwaveprop/propagation/band_config.ex index 1009bd4b..4eebd0c9 100644 --- a/lib/microwaveprop/propagation/band_config.ex +++ b/lib/microwaveprop/propagation/band_config.ex @@ -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", diff --git a/test/microwaveprop/propagation/band_config_test.exs b/test/microwaveprop/propagation/band_config_test.exs index cebc4530..ef3c70ee 100644 --- a/test/microwaveprop/propagation/band_config_test.exs +++ b/test/microwaveprop/propagation/band_config_test.exs @@ -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 diff --git a/test/microwaveprop/propagation_test.exs b/test/microwaveprop/propagation_test.exs index a51c3abc..e81191c6 100644 --- a/test/microwaveprop/propagation_test.exs +++ b/test/microwaveprop/propagation_test.exs @@ -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