Soundings (10,935): October has strongest gradients (-350 N/km) and 65.7% ducting, raised seasonal score 82→88. February ducting (20.2%) exceeds January (14.8%), raised 32→40. Commercial links (12,068 samples): 68 GHz shows 3.9 dB diurnal swing, 11 GHz shows 1.7 dB with inverted multipath pattern, 24 GHz remarkably stable at 0.9 dB. Diurnal sensitivity is non-monotonic with frequency. RAOB gradients avg -265 N/km vs HRRR -107 (2.5x stronger) — confirms HRRR misses thin surface ducts that soundings resolve.
322 lines
7.8 KiB
Elixir
322 lines
7.8 KiB
Elixir
defmodule Microwaveprop.Propagation.BandConfig do
|
|
@moduledoc """
|
|
Data-driven band configuration for the propagation scoring algorithm.
|
|
|
|
This is the single source of truth for all scoring parameters: weights,
|
|
thresholds, seasonal tables, and band-specific coefficients. When the
|
|
algorithm is tuned or new bands are added, only this module changes.
|
|
"""
|
|
|
|
@weights %{
|
|
humidity: 0.18,
|
|
time_of_day: 0.10,
|
|
td_depression: 0.10,
|
|
refractivity: 0.08,
|
|
sky: 0.08,
|
|
season: 0.08,
|
|
wind: 0.05,
|
|
rain: 0.08,
|
|
pressure: 0.15,
|
|
pwat: 0.10
|
|
}
|
|
|
|
@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]
|
|
|
|
@tiers [
|
|
%{min_score: 80, label: "EXCELLENT", color: "#00ffa3"},
|
|
%{min_score: 65, label: "GOOD", color: "#7dffd4"},
|
|
%{min_score: 50, label: "MARGINAL", color: "#ffe566"},
|
|
%{min_score: 33, label: "POOR", color: "#ff9044"},
|
|
%{min_score: 0, label: "NEGLIGIBLE", color: "#ff4f4f"}
|
|
]
|
|
|
|
@humidity_beneficial_thresholds [{4, 55}, {7, 70}, {10, 82}, {14, 90}, {18, 95}, {22, 88}]
|
|
@humidity_beneficial_default 75
|
|
|
|
# Thresholds calibrated for HRRR-derived gradients (coarser than sounding data).
|
|
# HRRR percentiles: p1=-230, p5=-162, p10=-130, p25=-94, p50=-70, p75=-53, p95=-40
|
|
@refractivity_thresholds [
|
|
{-200, 98, 85},
|
|
{-150, 92, 80},
|
|
{-100, 82, 72},
|
|
{-75, 68, 62},
|
|
{-55, 55, 55},
|
|
{-40, 48, 48}
|
|
]
|
|
@refractivity_default {42, 42}
|
|
|
|
@band_configs %{
|
|
10_000 => %{
|
|
freq_mhz: 10_000,
|
|
label: "10 GHz",
|
|
o2_db_km: 0.007,
|
|
h2o_coeff: 0.0,
|
|
humidity_effect: :beneficial,
|
|
humidity_penalty: 0.0,
|
|
rain_k: 0.010,
|
|
rain_alpha: 1.28,
|
|
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: 200,
|
|
extended_range_km: 500,
|
|
exceptional_range_km: 1000
|
|
},
|
|
24_000 => %{
|
|
freq_mhz: 24_000,
|
|
label: "24 GHz",
|
|
o2_db_km: 0.02,
|
|
h2o_coeff: 0.002,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 1.6,
|
|
rain_k: 0.070,
|
|
rain_alpha: 1.07,
|
|
seasonal_base: %{
|
|
1 => 88,
|
|
2 => 84,
|
|
3 => 68,
|
|
4 => 62,
|
|
5 => 51,
|
|
6 => 34,
|
|
7 => 18,
|
|
8 => 18,
|
|
9 => 48,
|
|
10 => 68,
|
|
11 => 96,
|
|
12 => 88
|
|
},
|
|
seasonal_adj: %{5 => -4, 6 => -8, 7 => -10, 8 => -10, 9 => -4},
|
|
typical_range_km: 100,
|
|
extended_range_km: 250,
|
|
exceptional_range_km: 500
|
|
},
|
|
47_000 => %{
|
|
freq_mhz: 47_000,
|
|
label: "47 GHz",
|
|
o2_db_km: 0.04,
|
|
h2o_coeff: 0.003,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 1.0,
|
|
rain_k: 0.187,
|
|
rain_alpha: 0.93,
|
|
seasonal_base: %{
|
|
1 => 90,
|
|
2 => 88,
|
|
3 => 78,
|
|
4 => 68,
|
|
5 => 55,
|
|
6 => 38,
|
|
7 => 22,
|
|
8 => 22,
|
|
9 => 48,
|
|
10 => 74,
|
|
11 => 96,
|
|
12 => 90
|
|
},
|
|
seasonal_adj: %{},
|
|
typical_range_km: 70,
|
|
extended_range_km: 150,
|
|
exceptional_range_km: 300
|
|
},
|
|
68_000 => %{
|
|
freq_mhz: 68_000,
|
|
label: "68 GHz",
|
|
o2_db_km: 0.90,
|
|
h2o_coeff: 0.007,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 1.4,
|
|
rain_k: 0.310,
|
|
rain_alpha: 0.86,
|
|
seasonal_base: %{
|
|
1 => 90,
|
|
2 => 88,
|
|
3 => 78,
|
|
4 => 65,
|
|
5 => 50,
|
|
6 => 32,
|
|
7 => 18,
|
|
8 => 18,
|
|
9 => 44,
|
|
10 => 70,
|
|
11 => 92,
|
|
12 => 90
|
|
},
|
|
seasonal_adj: %{},
|
|
typical_range_km: 40,
|
|
extended_range_km: 80,
|
|
exceptional_range_km: 150
|
|
},
|
|
75_000 => %{
|
|
freq_mhz: 75_000,
|
|
label: "75 GHz",
|
|
o2_db_km: 0.012,
|
|
h2o_coeff: 0.006,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 1.2,
|
|
rain_k: 0.345,
|
|
rain_alpha: 0.84,
|
|
seasonal_base: %{
|
|
1 => 90,
|
|
2 => 90,
|
|
3 => 80,
|
|
4 => 68,
|
|
5 => 55,
|
|
6 => 38,
|
|
7 => 22,
|
|
8 => 22,
|
|
9 => 48,
|
|
10 => 74,
|
|
11 => 96,
|
|
12 => 90
|
|
},
|
|
seasonal_adj: %{},
|
|
typical_range_km: 50,
|
|
extended_range_km: 120,
|
|
exceptional_range_km: 250
|
|
},
|
|
122_000 => %{
|
|
freq_mhz: 122_000,
|
|
label: "122 GHz",
|
|
o2_db_km: 0.80,
|
|
h2o_coeff: 0.010,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 1.0,
|
|
rain_k: 0.498,
|
|
rain_alpha: 0.77,
|
|
seasonal_base: %{
|
|
1 => 92,
|
|
2 => 90,
|
|
3 => 78,
|
|
4 => 62,
|
|
5 => 45,
|
|
6 => 28,
|
|
7 => 15,
|
|
8 => 15,
|
|
9 => 38,
|
|
10 => 68,
|
|
11 => 92,
|
|
12 => 92
|
|
},
|
|
seasonal_adj: %{},
|
|
typical_range_km: 30,
|
|
extended_range_km: 80,
|
|
exceptional_range_km: 140
|
|
},
|
|
134_000 => %{
|
|
freq_mhz: 134_000,
|
|
label: "134 GHz",
|
|
o2_db_km: 0.08,
|
|
h2o_coeff: 0.015,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 1.3,
|
|
rain_k: 0.520,
|
|
rain_alpha: 0.75,
|
|
seasonal_base: %{
|
|
1 => 92,
|
|
2 => 90,
|
|
3 => 78,
|
|
4 => 65,
|
|
5 => 48,
|
|
6 => 30,
|
|
7 => 18,
|
|
8 => 18,
|
|
9 => 42,
|
|
10 => 70,
|
|
11 => 92,
|
|
12 => 92
|
|
},
|
|
seasonal_adj: %{},
|
|
typical_range_km: 40,
|
|
extended_range_km: 100,
|
|
exceptional_range_km: 160
|
|
},
|
|
241_000 => %{
|
|
freq_mhz: 241_000,
|
|
label: "241 GHz",
|
|
o2_db_km: 0.08,
|
|
h2o_coeff: 0.30,
|
|
humidity_effect: :harmful,
|
|
humidity_penalty: 3.0,
|
|
rain_k: 0.550,
|
|
rain_alpha: 0.70,
|
|
seasonal_base: %{
|
|
1 => 95,
|
|
2 => 92,
|
|
3 => 75,
|
|
4 => 55,
|
|
5 => 35,
|
|
6 => 15,
|
|
7 => 8,
|
|
8 => 8,
|
|
9 => 30,
|
|
10 => 65,
|
|
11 => 95,
|
|
12 => 95
|
|
},
|
|
seasonal_adj: %{},
|
|
typical_range_km: 10,
|
|
extended_range_km: 50,
|
|
exceptional_range_km: 115
|
|
}
|
|
}
|
|
|
|
@doc "Returns the band config for the given frequency in MHz, or nil if not found."
|
|
@spec get(integer()) :: map() | nil
|
|
def get(freq_mhz), do: Map.get(@band_configs, freq_mhz)
|
|
|
|
@doc "Returns all band configs sorted by frequency (ascending)."
|
|
@spec all_bands() :: [map()]
|
|
def all_bands do
|
|
@band_configs
|
|
|> Enum.sort_by(fn {freq, _config} -> freq end)
|
|
|> Enum.map(fn {_freq, config} -> config end)
|
|
end
|
|
|
|
@doc "Returns all supported frequencies sorted ascending."
|
|
@spec all_freqs() :: [integer()]
|
|
def all_freqs do
|
|
@band_configs
|
|
|> Map.keys()
|
|
|> Enum.sort()
|
|
end
|
|
|
|
@doc "Returns the scoring weights map. All 10 values sum to 1.0."
|
|
@spec weights() :: map()
|
|
def weights, do: @weights
|
|
|
|
@doc "Returns the 12-element sunrise hour table (Jan-Dec, local time)."
|
|
@spec sunrise_table() :: [float()]
|
|
def sunrise_table, do: @sunrise_table
|
|
|
|
@doc "Returns score tier definitions ordered by threshold descending."
|
|
@spec tiers() :: [map()]
|
|
def tiers, do: @tiers
|
|
|
|
@doc "Returns humidity beneficial thresholds as {hour, score} tuples."
|
|
@spec humidity_beneficial_thresholds() :: [{integer(), integer()}]
|
|
def humidity_beneficial_thresholds, do: @humidity_beneficial_thresholds
|
|
|
|
@doc "Returns the default humidity beneficial score."
|
|
@spec humidity_beneficial_default() :: integer()
|
|
def humidity_beneficial_default, do: @humidity_beneficial_default
|
|
|
|
@doc "Returns refractivity thresholds as {gradient, score_beneficial, score_harmful} tuples."
|
|
@spec refractivity_thresholds() :: [{number(), number(), number()}]
|
|
def refractivity_thresholds, do: @refractivity_thresholds
|
|
|
|
@doc "Returns the default refractivity scores as {beneficial, harmful}."
|
|
@spec refractivity_default() :: {number(), number()}
|
|
def refractivity_default, do: @refractivity_default
|
|
end
|