Score time-of-day per grid point using longitude/15 solar offset instead of hardcoded CST/CDT. Add PWAT as 10th scoring factor. Refine pressure thresholds. Update ML model and training pipeline to use local solar time.
332 lines
8.1 KiB
Elixir
332 lines
8.1 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}
|
|
@shallow_bl_threshold_m 300
|
|
@shallow_bl_score 82
|
|
|
|
@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 => 32,
|
|
3 => 22,
|
|
4 => 55,
|
|
5 => 68,
|
|
6 => 90,
|
|
7 => 95,
|
|
8 => 75,
|
|
9 => 78,
|
|
10 => 82,
|
|
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
|
|
|
|
@doc "Returns the shallow boundary layer threshold in meters."
|
|
@spec shallow_bl_threshold_m() :: number()
|
|
def shallow_bl_threshold_m, do: @shallow_bl_threshold_m
|
|
|
|
@doc "Returns the score applied when boundary layer is shallow."
|
|
@spec shallow_bl_score() :: number()
|
|
def shallow_bl_score, do: @shallow_bl_score
|
|
end
|