QSO enrichment now groups all path points by HRRR hour and creates one batch job per hour instead of one job per point. The batch job downloads the GRIB2 data once and extracts all needed points from the same binary. Legacy single-point jobs are still supported for backward compatibility.
37 KiB
Microwave Propagation Algorithm v2 — Data-Driven, Per-Band
Overview
This document revises the propagation scoring algorithm from algo1.md using empirical analysis of 57,492 QSOs with distance data across 13 ham radio bands from 10 GHz to 411 GHz. Weather data from 95 ASOS stations and 9 RAOB sounding stations is cross-referenced with QSO outcomes to validate and recalibrate the scoring model.
The algorithm serves two purposes:
- Current conditions display — Real-time band-by-band propagation scores
- Short-term predictions — Forecast for the next 3-6 hours based on diurnal trends, forecast data, and atmospheric trajectory
Key Findings From QSO Data Analysis
Dataset Summary
| Band (MHz) | QSOs | Median (km) | P90 (km) | P95 (km) | Max Tropo (km) |
|---|---|---|---|---|---|
| 10,000 | 52,456 | 195 | 383 | 461 | 1,609 |
| 24,000 | 3,439 | 99 | 179 | 217 | 710 |
| 47,000 | 576 | 71 | 126 | 160 | 343 |
| 75,000 | 81 | 57 | 208 | 208 | 289 |
| 122,000 | 11 | 52 | 139 | 139 | 139 |
| 134,000 | 3 | 114 | 149 | 153 | 157 |
| 142,000 | 4 | 48 | 74 | 77 | 80 |
| 241,000 | 5 | 80 | 114 | 114 | 114 |
Notes: All EME/satellite contacts filtered out. Very sparse data above 75 GHz.
Critical Finding: Humidity Effect Reverses By Frequency
This is the most important discovery. The relationship between moisture and distance INVERTS between 10 GHz and 24 GHz:
10 GHz — Humidity HELPS:
| Abs. Humidity | N | Avg Dist | P90 Dist |
|---|---|---|---|
| 5-8 g/m³ | 2,375 | 193 km | 342 km |
| 8-11 g/m³ | 4,425 | 209 km | 376 km |
| 11-14 g/m³ | 24,272 | 215 km | 383 km |
| 14-17 g/m³ | 20,689 | 219 km | 385 km |
| 17+ g/m³ | 691 | 230 km | 519 km |
At 10 GHz, gaseous absorption is negligible (~0.01 dB/km), so extra moisture primarily INCREASES surface refractivity N, enhancing ducting and super-refraction. More moisture = more bending = longer distances.
24 GHz — Humidity HURTS:
| Abs. Humidity | N | Avg Dist | P90 Dist |
|---|---|---|---|
| 5-8 g/m³ | 193 | 115 km | 154 km |
| 8-11 g/m³ | 370 | 143 km | 244 km |
| 11-14 g/m³ | 1,429 | 105 km | 174 km |
| 14-17 g/m³ | 1,390 | 89 km | 179 km |
| 17+ g/m³ | 50 | 53 km | 103 km |
At 24 GHz, proximity to the 22.235 GHz water vapor absorption line means humidity causes severe path loss (~0.1-0.15 dB/km at 7.5 g/m³). The absorption penalty outweighs refractivity benefits.
47 GHz — Humidity hurts, but less severely than 24 GHz:
| Abs. Humidity | N | Avg Dist | P90 Dist |
|---|---|---|---|
| 0-5 g/m³ | 6 | 191 km | 234 km |
| 5-8 g/m³ | 39 | 91 km | 126 km |
| 8-11 g/m³ | 37 | 104 km | 216 km |
| 11-14 g/m³ | 222 | 71 km | 114 km |
| 14-17 g/m³ | 264 | 74 km | 122 km |
47 GHz sits in an atmospheric window between the 22 GHz H₂O peak and 60 GHz O₂ band. H₂O absorption is much less than at 24 GHz (~0.02 dB/km vs ~0.10), but O₂ wing absorption adds ~0.04-0.05 dB/km. Very dry conditions produce dramatically better results.
Precipitable Water Column Confirms the Pattern
| PW (mm) | 10G Avg | 10G P90 | 24G Avg | 24G P90 | 47G Avg | 47G P90 |
|---|---|---|---|---|---|---|
| <15 | 193 | 341 | 123 | 240 | 115 | 207 |
| 15-25 | 209 | 378 | 125 | 200 | 74 | 126 |
| 25-35 | 215 | 383 | 96 | 171 | 70 | 114 |
| 35-45 | 228 | 391 | 108 | 179 | 86 | 160 |
10 GHz: Wet atmosphere → longer paths (refractivity dominates). 24 GHz: Dry atmosphere → longer paths (absorption dominates). 47 GHz: Very dry → longest paths; also a secondary peak in wet conditions, likely from duct events.
Boundary Layer Depth Matters More Than Ducting Detection
| BL Depth | 10G Avg | 10G P90 | 24G Avg | 24G P90 | 47G Avg | 47G P90 |
|---|---|---|---|---|---|---|
| <200m (shallow) | — | — | — | — | — | — |
| 200-500m (low) | 214 | 382 | 92 | 159 | 70 | 114 |
| 500-1000m (medium) | 239 | 461 | 115 | 210 | 79 | 122 |
| 1-2km (deep) | 214 | 382 | 112 | 183 | 84 | 126 |
A moderate BL depth (500-1000m) is the sweet spot across all bands. This indicates an elevated inversion layer that's high enough to trap signals but not so deep that full convective mixing has occurred. This corresponds to morning conditions after sunrise when the nocturnal inversion is lifting but hasn't fully broken.
Ducting Detection Alone Is a Weak Predictor
Binary "duct detected" from soundings shows surprisingly little correlation with distance:
| 10G Duct | 10G No Duct | 24G Duct | 24G No Duct | |
|---|---|---|---|---|
| Avg Dist | 214 | 216 | 101 | 100 |
| P90 Dist | 380 | 383 | 179 | 179 |
Why? Because (1) soundings are 12-hourly point samples while conditions evolve continuously, (2) the ±6hr matching window is too wide, and (3) contest operators work regardless of conditions, so the base population isn't filtered for "good" conditions. Duct characteristics (strength, height, thickness) matter more than binary detection.
Sky Condition Impact Scales With Frequency
| Sky | 10G Avg | 24G Avg | 47G Avg |
|---|---|---|---|
| CLR | 213 | 101 | 73 |
| FEW | 216 | 100 | 71 |
| SCT | 216 | 99 | 69 |
| BKN | 215 | 96 | 69 |
| OVC | 215 | 93 | 68 |
| VV | 216 | 87 | 66 |
10 GHz: Sky condition is almost irrelevant (213-216 range). 24/47 GHz: Clear skies help modestly. VV (fog/very low visibility) is a moderate penalty, likely because it indicates high near-surface moisture content.
Pressure: Low Pressure Correlates With Longer Distances
This contradicts algo1's scoring of high pressure as favorable:
| Pressure | 10G Avg | 10G P90 | 24G Avg | 47G Avg |
|---|---|---|---|---|
| <1010 | 262 | 506 | 119 | 111 |
| 1010-1015 | 215 | 383 | 104 | 74 |
| 1015-1020 | 217 | 383 | 97 | 74 |
| 1020-1025 | 201 | 360 | 109 | 105 |
| 1025+ | 196 | 354 | 122 | — |
Low pressure systems bring frontal boundaries with strong temperature/moisture gradients, creating inversions and ducts. The key is not the absolute pressure but the gradient structure. However, extreme high pressure (ridge) can also trap air masses and create persistent inversions.
Wind Impact Is Minimal
| Wind | 10G Avg | 24G Avg | 47G Avg |
|---|---|---|---|
| Calm (0-3 kts) | 216 | 84 | — |
| Light (3-7 kts) | 214 | 100 | 77 |
| Moderate (7-12 kts) | 220 | 113 | 77 |
Wind shows no meaningful penalty for distance achieved. Algo1's heavy wind penalty (18% weight) is not supported by the data. Wind may reduce inversion quality, but it also creates boundary-layer dynamics (e.g., marine layer advection, frontal mixing) that can enhance propagation.
Time of Day (10 GHz, Timed Contacts Only, CDT)
| Local Hour | N | Avg Dist | P90 Dist |
|---|---|---|---|
| 22-23 (late evening) | 771 | 235 | 416 |
| 0-1 (overnight) | 299 | 212 | 370 |
| 5-6 (dawn) | 1,252 | 211 | 474 |
| 7-8 (early AM) | 5,848 | 216 | 394 |
| 9-12 (morning) | 16,383 | 211 | 373 |
| 13-16 (afternoon) | 17,432 | 213 | 373 |
| 17-21 (evening) | 12,401 | 213 | 396 |
Dawn (5-6 CDT) shows the highest P90 distances (474 km) but not the highest average — this confirms the inversion timing theory: dawn is when the strongest inversions exist, but they're spotty (high variance). Late evening (22-23) shows the best average. The afternoon minimum is real but modest (~5% lower than peak).
Long-Range Contact Conditions
10 GHz > 600 km: Two primary mechanisms observed:
- Marine/coastal duct: Hot, very dry (87°F, 27% RH), moderate wind. California/Mexico coast paths.
- Radiation inversion: Cool, humid (64°F, 80% RH), calm. Northeast/Midwest overnight paths.
24 GHz > 200 km: Three mechanisms:
- Coastal duct in dry air: Hot, very dry (81°F, 39% RH). West Coast marine layer.
- Cold dry air: Cool, very dry (57°F, 33% RH). Winter enhanced refraction.
- Rain scatter: One 710 km contact explicitly labeled "CW (Rainscatter)". Unique mechanism.
47 GHz > 150 km: Primarily dry-air events:
- Cool/dry (64°F, 60% RH) for the longest (343 km).
- Very dry winter (57°F, 33% RH) for 246 km.
- Exception: some medium-distance contacts in moderately humid conditions, suggesting occasional ducting.
Frequency-Specific Physics
Atmospheric Absorption
Based on ITU-R P.676-13 approximate values at sea level (15°C, 1013 hPa):
| Band | f (GHz) | O₂ (dB/km) | H₂O Coeff (dB/km per g/m³) | Total @ 7.5 g/m³ | Absorption Line Proximity |
|---|---|---|---|---|---|
| 10G | 10.368 | 0.008 | 0.0005 | ~0.012 | Clear window |
| 24G | 24.192 | 0.015 | 0.012 | ~0.105 | 22.235 GHz H₂O shoulder |
| 47G | 47.088 | 0.045 | 0.003 | ~0.068 | Window (H₂O/O₂ gap) |
| 75G | 76.032 | 0.012 | 0.006 | ~0.057 | Window |
| 122G | 122.250 | 0.8 | 0.010 | ~0.875 | 118.75 GHz O₂ wing |
| 134G | 134.928 | 0.08 | 0.015 | ~0.193 | Between O₂ 118 & H₂O 183 |
| 142G | 142.000 | 0.05 | 0.025 | ~0.238 | Approaching H₂O 183 |
| 241G | 241.000 | 0.08 | 0.30 | ~2.33 | Between H₂O 183 & H₂O 325 |
Rain Attenuation (ITU-R P.838-3)
γ_R = k × R^α (dB/km), R = rain rate in mm/hr:
| Band | k_H | α_H | Light (4 mm/hr) | Moderate (10 mm/hr) | Heavy (25 mm/hr) |
|---|---|---|---|---|---|
| 10G | 0.010 | 1.28 | 0.05 dB/km | 0.19 dB/km | 0.56 dB/km |
| 24G | 0.070 | 1.07 | 0.31 dB/km | 0.81 dB/km | 2.04 dB/km |
| 47G | 0.187 | 0.93 | 0.68 dB/km | 1.58 dB/km | 3.69 dB/km |
| 75G | 0.345 | 0.84 | 1.07 dB/km | 2.40 dB/km | 5.18 dB/km |
| 122G | 0.498 | 0.77 | 1.32 dB/km | 2.93 dB/km | 5.91 dB/km |
| 241G | 0.550 | 0.70 | 1.30 dB/km | 2.76 dB/km | 5.20 dB/km |
Above 75 GHz, even light rain effectively kills the path. At 10 GHz, moderate rain is tolerable. At 24/47 GHz, rain is a serious concern for paths > 100 km.
Free-Space Path Loss (ITU-R P.525)
FSPL = 20·log₁₀(d_km) + 20·log₁₀(f_GHz) + 92.45 dB
| Band | FSPL @ 100 km | FSPL @ 200 km | FSPL @ 500 km |
|---|---|---|---|
| 10G | 152.8 dB | 158.8 dB | 166.4 dB |
| 24G | 160.1 dB | 166.1 dB | 173.4 dB |
| 47G | 165.9 dB | 171.9 dB | 179.4 dB |
| 75G | 170.0 dB | 176.0 dB | 183.5 dB |
| 122G | 174.2 dB | 180.2 dB | 187.6 dB |
| 241G | 180.1 dB | 186.1 dB | 193.5 dB |
Each doubling of frequency adds ~6 dB of path loss. Combined with higher atmospheric absorption, this explains why achievable distance falls rapidly with frequency.
Revised Scoring Algorithm: Per-Band
Design Principles
- Frequency-dependent humidity treatment — moisture is beneficial at 10 GHz (refractivity), harmful at 24+ GHz (absorption)
- Reduced wind penalty — data doesn't support the heavy wind penalty from algo1
- Pressure gradient over absolute — frontal activity matters more than pressure value
- Boundary layer depth — when sounding data available, use BL depth as a predictor
- Refractivity gradient — super-refraction events are the strongest predictor
Band Configuration
@band_configs %{
10_000 => %{
label: "10 GHz",
o2_db_km: 0.008,
h2o_coeff: 0.0005,
# Humidity INCREASES refractivity -> helps propagation
# Net effect at 10G: more moisture = better
humidity_effect: :beneficial,
humidity_penalty: 0.0, # No penalty
humidity_bonus: 0.3, # Bonus factor for moderate humidity
rain_k: 0.010, rain_alpha: 1.28,
seasonal_base: %{1 => 88, 2 => 84, 3 => 72, 4 => 62, 5 => 55,
6 => 42, 7 => 28, 8 => 28, 9 => 52, 10 => 68,
11 => 96, 12 => 88},
seasonal_adj: %{},
typical_range_km: 200,
extended_range_km: 500,
exceptional_range_km: 1000
},
24_000 => %{
label: "24 GHz",
o2_db_km: 0.015,
h2o_coeff: 0.012,
humidity_effect: :harmful,
humidity_penalty: 1.6, # Strong penalty near 22 GHz H2O line
humidity_bonus: 0.0,
rain_k: 0.070, rain_alpha: 1.07,
seasonal_base: %{1 => 88, 2 => 84, 3 => 72, 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 => %{
label: "47 GHz",
o2_db_km: 0.045,
h2o_coeff: 0.003,
humidity_effect: :harmful,
humidity_penalty: 1.0, # Moderate — in atmospheric window
humidity_bonus: 0.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
},
75_000 => %{
label: "75 GHz (4mm)",
o2_db_km: 0.012,
h2o_coeff: 0.006,
humidity_effect: :harmful,
humidity_penalty: 1.2,
humidity_bonus: 0.0,
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 => %{
label: "122 GHz (2.5mm)",
o2_db_km: 0.80,
h2o_coeff: 0.010,
humidity_effect: :harmful,
humidity_penalty: 1.0,
humidity_bonus: 0.0,
rain_k: 0.498, rain_alpha: 0.77,
# O2 absorption near 118.75 GHz is the primary constraint
# Winter months preferred (cold/dry = less O2 broadening)
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 => %{
label: "134 GHz",
o2_db_km: 0.08,
h2o_coeff: 0.015,
humidity_effect: :harmful,
humidity_penalty: 1.3,
humidity_bonus: 0.0,
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 => %{
label: "241 GHz (1.2mm)",
o2_db_km: 0.08,
h2o_coeff: 0.30,
humidity_effect: :harmful,
humidity_penalty: 3.0, # Extremely H2O sensitive (between 183 & 325 lines)
humidity_bonus: 0.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
}
}
Scoring Functions
1. Humidity Score (Revised — Frequency-Dependent)
def score_humidity(abs_humidity_gm3, band_config) do
case band_config.humidity_effect do
:beneficial ->
# 10 GHz: More moisture = more refractivity = better propagation
# But extreme humidity (tropical) can cause scintillation
cond do
abs_humidity_gm3 < 4 -> 55 # Very dry = reduced refractivity
abs_humidity_gm3 < 7 -> 70 # Dry
abs_humidity_gm3 < 10 -> 82 # Moderate
abs_humidity_gm3 < 14 -> 90 # Good refractivity
abs_humidity_gm3 < 18 -> 95 # Excellent refractivity
abs_humidity_gm3 < 22 -> 88 # High but starts to scatter
true -> 75 # Tropical — scintillation risk
end
:harmful ->
# 24+ GHz: H2O absorption dominates
r = abs_humidity_gm3 * band_config.humidity_penalty
cond do
r <= 6 -> 100
r <= 9 -> round(95 - (r - 6) / 3 * 20)
r <= 13 -> round(75 - (r - 9) / 4 * 30)
r <= 18 -> round(45 - (r - 13) / 5 * 35)
true -> max(0, round(10 - (r - 18) * 2))
end
end
end
2. Wind Score (Revised — Reduced Weight)
def score_wind(speed_kts) do
# Data shows wind has minimal impact on achieved distance
# Retain a mild penalty for very high winds (turbulent scintillation)
cond do
speed_kts < 5 -> 100 # Calm/light — ideal for inversion
speed_kts < 10 -> 90 # Light breeze — still good
speed_kts < 15 -> 75 # Moderate — some mixing
speed_kts < 20 -> 55 # Fresh — noticeable mixing
speed_kts < 25 -> 35 # Strong — turbulent scintillation
true -> 15 # Gale — path degraded
end
end
3. Sky Cover Score (Unchanged from algo1)
def score_sky(condition) do
case condition do
c when c in ["CLR", "SKC"] -> 100
"FEW" -> 88
"SCT" -> 60
"BKN" -> 25
"OVC" -> 5
"VV" -> 5
_ -> 50
end
end
4. Time of Day Score (Revised — Data-Calibrated)
def score_time_of_day(utc_hour, utc_minute, month) do
# CDT/CST offset based on month
is_dst = month >= 3 and month <= 10
offset = if is_dst, do: -5, else: -6
local = rem(utc_hour + utc_minute / 60 + offset + 24, 24)
# Monthly sunrise table (local decimal hours)
sunrise = [7.4, 7.3, 7.0, 6.7, 6.35, 6.25,
6.35, 6.65, 6.9, 7.1, 7.35, 7.45]
|> Enum.at(month - 1)
d = local - sunrise # Hours relative to sunrise
cond do
# Peak window: sunrise ±1.5 hours
d >= -1.5 and d <= 1.5 ->
{100, "Peak — inversion maximum"}
# Good morning window: +1.5 to +3 hours
d > 1.5 and d <= 3.0 ->
{78, "Good — inversion eroding"}
# Pre-dawn: -3 to -1.5 hours before sunrise
d > -3.0 and d < -1.5 ->
{82, "Pre-dawn — inversion building"}
# Marginal morning: +3 to +6 hours
d > 3.0 and d <= 6.0 ->
{38, "Marginal — boundary layer mixing"}
# Late evening: 20:00-00:00 local (data shows elevated P90)
local >= 20.0 or local <= 1.0 ->
{72, "Evening — cooling, inversion reforming"}
# Afternoon: worst period
d > 6.0 ->
{18, "Afternoon — full convective mixing"}
# Early night
true ->
{55, "Night — gradual cooling"}
end
end
5. Temperature-Dewpoint Depression Score (Revised — Frequency-Split)
def score_td_depression(temp_f, dewpoint_f, band_config) do
dep = temp_f - dewpoint_f
case band_config.humidity_effect do
:beneficial ->
# 10 GHz: Smaller depression (moist) can be BETTER due to refractivity
# But very small depression (near fog) is bad (scintillation/scattering)
cond do
dep < 3 -> 40 # Near saturation — fog risk
dep < 8 -> 75 # Moist — good refractivity
dep < 14 -> 85 # Moderate — balanced
dep < 22 -> 70 # Dry — reduced refractivity
true -> 55 # Very dry — poor refractivity
end
:harmful ->
# 24+ GHz: Larger depression = drier = less absorption = better
cond do
dep > 22 -> 96
dep > 14 -> 80
dep > 8 -> 60
dep > 4 -> 38
true -> 18
end
end
end
6. Season Score (Per-Band Lookup)
def score_season(month, band_config) do
base = Map.get(band_config.seasonal_base, month, 50)
adj = Map.get(band_config.seasonal_adj, month, 0)
max(0, min(100, base + adj))
end
7. Pressure Score (Revised — Gradient-Focused)
def score_pressure(current_mb, previous_mb) do
# Data shows frontal boundaries (low pressure, changing pressure) can
# produce ducting. Steady high pressure is not as favorable as assumed.
case previous_mb do
nil ->
# No trend data — score based on absolute (mild effect)
cond do
current_mb > 1025 -> 55 # Strong ridge — can trap but stagnant
current_mb > 1018 -> 65 # Mild high — stable
current_mb > 1010 -> 60 # Normal
current_mb > 1005 -> 55 # Low — frontal activity possible
true -> 40 # Very low — active weather
end
prev ->
delta = current_mb - prev
cond do
delta > 2.5 -> 80 # Rising rapidly — post-frontal clearing
delta > 0.8 -> 70 # Rising — stabilizing
delta > -0.5 -> 60 # Steady — neutral
delta > -2.0 -> 65 # Falling slowly — approaching front, duct possible
true -> 45 # Falling rapidly — active weather
end
end
end
8. Rain/Precipitation Score (NEW — Critical for 24 GHz+)
def score_rain(rain_rate_mmhr, band_config) do
# Calculate path attenuation penalty
if rain_rate_mmhr == nil or rain_rate_mmhr == 0 do
100
else
gamma = band_config.rain_k * :math.pow(rain_rate_mmhr, band_config.rain_alpha)
# Score based on additional dB/km penalty
cond do
gamma < 0.1 -> 95 # Negligible rain effect
gamma < 0.5 -> 75 # Minor rain effect
gamma < 1.0 -> 50 # Moderate rain — paths significantly degraded
gamma < 2.0 -> 25 # Heavy rain — short paths only
gamma < 5.0 -> 10 # Very heavy — marginal
true -> 0 # Extreme — path destroyed
end
end
end
9. Refractivity/Sounding Score (NEW — When Available)
def score_refractivity(sounding, band_config) do
# Use sounding-derived parameters when available
cond do
sounding == nil -> 50 # No data — neutral
sounding.min_refractivity_gradient < -500 ->
# Super-refraction detected
case band_config.humidity_effect do
:beneficial -> 98 # 10 GHz loves super-refraction
:harmful -> 85 # Higher bands benefit too but less
end
sounding.min_refractivity_gradient < -200 ->
# Enhanced refraction
80
sounding.boundary_layer_depth_m != nil and
sounding.boundary_layer_depth_m >= 500 and
sounding.boundary_layer_depth_m <= 1000 ->
# Sweet-spot BL depth
78
sounding.ducting_detected ->
# Duct detected but not super-refraction
70
true ->
50 # Normal conditions
end
end
Composite Score: Revised Weights
Weight Rationale Changes From algo1
| Factor | algo1 | algo2 | Rationale |
|---|---|---|---|
| Humidity | 26% | 22% | Still important but split role (helpful at 10G) |
| Wind | 18% | 8% | Data shows minimal impact |
| Sky | 15% | 10% | Modest effect, mainly at higher frequencies |
| Time of Day | 14% | 18% | Strongest diurnal predictor in data |
| Td Depression | 11% | 14% | Proxy for humidity aloft — strong signal |
| Season | 9% | 10% | Long-term baseline |
| Pressure | 7% | 5% | Weak standalone predictor |
| Rain | 0% (new) | 8% | Critical for 24+ GHz |
| Refractivity | 0% (new) | 5% | Best predictor when available |
Composite Function
def composite_score(factors) do
round(
factors.humidity * 0.22 +
factors.wind * 0.08 +
factors.sky * 0.10 +
factors.time_of_day * 0.18 +
factors.td_depression * 0.14 +
factors.season * 0.10 +
factors.pressure * 0.05 +
factors.rain * 0.08 +
factors.refractivity * 0.05
)
end
# Total: 100%
Score Interpretation (Per-Band Distance Estimates)
| Score | Label | 10G Range | 24G Range | 47G Range | 75G Range |
|---|---|---|---|---|---|
| 80-100 | EXCELLENT | 400-1000+ km | 200-500 km | 120-300 km | 80-200+ km |
| 65-79 | GOOD | 250-400 km | 120-200 km | 80-120 km | 50-80 km |
| 50-64 | MARGINAL | 150-250 km | 70-120 km | 50-80 km | 30-50 km |
| 33-49 | POOR | 80-150 km | 40-70 km | 25-50 km | 15-30 km |
| 0-32 | NEGLIGIBLE | <80 km | <40 km | <25 km | <15 km |
Short-Term Prediction Model
Concept
Current conditions are sampled every 5 minutes from ASOS. The prediction model extrapolates conditions forward 1-6 hours using:
- Diurnal temperature curve — predict T and Td based on time of day and current trajectory
- Inversion lifecycle — model inversion formation (evening), strengthening (overnight), peak (sunrise), and breakup (mid-morning)
- Pressure tendency — extrapolate from last 3 hours of observations
- Moisture trajectory — absolute humidity changes slowly; project forward from current trend
- Forecast data — when available, use NWS forecast (cloud cover, wind, precipitation probability)
Prediction Algorithm
def predict_score(current_obs, recent_obs_3hr, forecast, band_config) do
# Calculate current trajectory
temp_trend = (current_obs.temp_f - recent_obs_3hr.temp_f) / 3 # °F/hr
dp_trend = (current_obs.dewpoint_f - recent_obs_3hr.dewpoint_f) / 3
pressure_trend = (current_obs.slp - recent_obs_3hr.slp) / 3 # mb/hr
# Generate predictions for +1, +2, +3, +4, +5, +6 hours
for hours_ahead <- 1..6 do
future_time = DateTime.add(current_obs.observed_at, hours_ahead * 3600)
{future_hour, future_minute} = {future_time.hour, future_time.minute}
month = future_time.month
# Project temperature using diurnal model
projected_temp = project_temperature(current_obs.temp_f, temp_trend,
hours_ahead, future_hour, month)
projected_dp = project_dewpoint(current_obs.dewpoint_f, dp_trend,
hours_ahead)
projected_slp = current_obs.slp + pressure_trend * hours_ahead
# Use forecast data for sky/rain if available
projected_sky = forecast_sky(forecast, hours_ahead) || current_obs.sky_condition
projected_rain = forecast_rain(forecast, hours_ahead) || 0
projected_wind = project_wind(current_obs.wind_speed_kts, forecast,
hours_ahead)
# Calculate projected abs humidity
tc = (projected_temp - 32) * 5 / 9
es = 6.112 * :math.exp(17.67 * tc / (tc + 243.5))
td_c = (projected_dp - 32) * 5 / 9
ed = 6.112 * :math.exp(17.67 * td_c / (td_c + 243.5))
rh = min(100, ed / es * 100)
abs_hum = 217 * (rh / 100) * es / (tc + 273.15)
# Score each factor
factors = %{
humidity: score_humidity(abs_hum, band_config),
wind: score_wind(projected_wind),
sky: score_sky(projected_sky),
time_of_day: score_time_of_day(future_hour, future_minute, month) |> elem(0),
td_depression: score_td_depression(projected_temp, projected_dp, band_config),
season: score_season(month, band_config),
pressure: score_pressure(projected_slp, current_obs.slp),
rain: score_rain(projected_rain, band_config),
refractivity: 50 # Can't predict refractivity from surface obs
}
%{
hours_ahead: hours_ahead,
time: future_time,
score: composite_score(factors),
factors: factors,
confidence: prediction_confidence(hours_ahead)
}
end
end
Diurnal Temperature Projection
def project_temperature(current_temp, trend_per_hour, hours_ahead,
future_local_hour, month) do
# Diurnal temperature follows a sinusoidal pattern
# Max temp ~15:00 local, min temp ~06:00 local
sunrise = [7.4, 7.3, 7.0, 6.7, 6.35, 6.25,
6.35, 6.65, 6.9, 7.1, 7.35, 7.45]
|> Enum.at(month - 1)
# After sunset: cooling rate ~1-2°F/hr
# After sunrise: warming rate ~2-4°F/hr
# Near max/min: rate approaches 0
# Blend between current trend and diurnal model
diurnal_rate = cond do
future_local_hour < sunrise - 1 -> -0.5 # Pre-dawn: slow cooling
future_local_hour < sunrise + 2 -> 0.0 # Around sunrise: transition
future_local_hour < 15 -> 2.0 # Morning: warming
future_local_hour < 18 -> 0.5 # Late afternoon: slow warming
future_local_hour < 21 -> -1.5 # Evening: cooling
true -> -1.0 # Night: slow cooling
end
# Weight: current trend dominates short-term, diurnal dominates long-term
weight = min(1.0, hours_ahead / 4.0)
blended_rate = trend_per_hour * (1 - weight) + diurnal_rate * weight
current_temp + blended_rate * hours_ahead
end
Prediction Confidence
def prediction_confidence(hours_ahead) do
# Confidence decreases with time
case hours_ahead do
1 -> 0.90
2 -> 0.80
3 -> 0.65
4 -> 0.50
5 -> 0.40
6 -> 0.30
_ -> 0.20
end
end
Band-Specific Propagation Mechanisms
10 GHz (3cm)
Primary mechanisms: Tropospheric ducting, enhanced refraction Key factor: Refractivity profile (N-units), NOT humidity absorption Best conditions:
- Moderate to high humidity (12-20 g/m³) — increases surface N
- Temperature inversions — radiation (overnight) or advection (marine layer)
- Stable atmosphere — LI > 0, moderate BL depth 500-1000m
- Any time of day can work, but late evening through early morning peaks
Unique considerations:
- 10 GHz is largely insensitive to rain (< 0.2 dB/km at 10 mm/hr)
- Cloud/fog does NOT significantly attenuate — can propagate through cloud decks
- Marine ducting can produce 1000+ km paths along coastlines
- Frontal boundaries create strong refractive gradients
Prediction emphasis: Focus on inversion formation/destruction cycle, not moisture trends.
24 GHz (1.2cm)
Primary mechanisms: Tropospheric ducting (reduced by absorption), enhanced refraction Key factor: Absolute humidity — the 22.235 GHz H₂O line makes this THE most humidity-sensitive band Best conditions:
- Very dry air (< 8 g/m³ absolute humidity)
- Cold season (Nov-Mar) when humidity is naturally low
- Clear skies with strong radiation inversions
- Pre-dawn through early morning
Unique considerations:
- 24 GHz is 10x more sensitive to water vapor than 10 GHz
- Summer is terrible (Gulf moisture floods the boundary layer)
- Rain scatter is a viable mechanism at this frequency (one 710 km QSO documented)
- The humidity penalty effectively limits summer range to ~50% of winter potential
Prediction emphasis: Track humidity trajectory closely. A drying trend is the most actionable prediction.
47 GHz (6mm)
Primary mechanisms: Tropospheric ducting (in atmospheric window), line-of-sight with enhanced refraction Key factor: Balance of humidity (absorption) and refractivity (ducting) Best conditions:
- Dry air (< 8 g/m³) — moderate H₂O sensitivity
- Clear skies — no precipitation
- Strong inversions — required for anything beyond ~100 km
- Early morning, pre-dawn
Unique considerations:
- Sits in a window between 22 GHz H₂O and 60 GHz O₂ bands
- O₂ absorption is significant (~0.045 dB/km) — cannot be reduced by weather
- Very dry winter conditions can produce surprising distances (246 km documented)
- Ducting events are the ONLY way to exceed ~150 km
Prediction emphasis: Monitor for ducting conditions (inversions + dry air). Predict morning inversion peak.
75 GHz (4mm)
Primary mechanisms: Tropospheric ducting (rare), enhanced LOS Key factor: Dry air + path geometry Best conditions:
- Very dry air (< 5 g/m³)
- No precipitation whatsoever
- Strong elevated inversions
- Winter months
Unique considerations:
- 289 km record on this band (California marine duct)
- Only 81 contacts with distance data — sparse empirical basis
- Rain attenuation is severe (~1 dB/km at 4 mm/hr)
- Most contacts are during summer contests but winter records are longer
- Equipment is rare, so opportunities to collect data are limited
Prediction emphasis: Same as 47 GHz but stricter rain avoidance. Track precipitation probability.
122 GHz (2.5mm)
Primary mechanisms: Enhanced LOS, rare ducting Key factor: O₂ absorption from 118.75 GHz line Best conditions:
- Cold temperatures (reduce O₂ line broadening)
- Low altitude paths (O₂ absorption decreases with altitude... actually it doesn't scale simply)
- Very dry air
- No rain or precipitation
Unique considerations:
- The 118.75 GHz O₂ absorption line creates ~0.8 dB/km baseline absorption
- This CANNOT be improved by any weather conditions — O₂ is well-mixed
- Cold temperatures narrow the O₂ absorption lines slightly, reducing wing absorption
- 139 km record (California, February) — cold dry conditions
- Practically limited to ~50 km for reliable paths
Prediction emphasis: Temperature trajectory (cold = slightly better O₂ absorption), rain avoidance.
134 GHz
Primary mechanisms: Enhanced LOS Key factor: Between O₂ 118 and H₂O 183 lines — a mini window Best conditions:
- Cold, dry air
- No precipitation
- Clear LOS paths
Unique considerations:
- 157 km record (Germany, March) — cold European winter
- Better than 122 GHz because farther from O₂ 118.75 line
- H₂O 183 line begins to affect from this side
- Very few active stations worldwide
241 GHz (1.2mm)
Primary mechanisms: LOS only (atmospheric absorption too high for long paths) Key factor: H₂O absorption is dominant (~0.3 dB/km per g/m³) Best conditions:
- Extremely dry air (< 3 g/m³) — essentially winter-only in most US locations
- High altitude stations (thinner atmosphere)
- Short paths (< 20 km typical, < 50 km extended)
Unique considerations:
- 114 km record (Virginia, January) — WA1ZMS/W4WWQ path, likely elevated terrain + extreme cold/dry
- Between H₂O 183 and H₂O 325 lines — significant absorption from both wings
- Total path loss at 100 km: FSPL + gas ≈ 180 + 230 = 410 dB (impractical without ducting + dry air)
- Realistic prediction: display "Path viable" / "Path not viable" for specific planned paths
Implementation Notes
Data Sources for Current Conditions
- ASOS surface observations (every 5-20 min): temp, dewpoint, wind, pressure, sky, visibility
- RAOB soundings (00Z and 12Z): refractivity profile, BL depth, inversions, ducts
- NWS forecast (hourly): precipitation probability, wind, cloud cover (for predictions)
Computation Flow
Every 5 minutes:
1. Fetch latest ASOS observations for nearby stations
2. Compute absolute humidity from temp + dewpoint
3. Compute per-band scores using scoring functions
4. Generate composite score per band
5. If sounding data available (within last 12 hrs), incorporate refractivity score
6. Generate 6-hour forecast per band
7. Display current + forecast in UI
Every 12 hours (00Z, 12Z):
1. Fetch new sounding data
2. Recompute refractivity/ducting parameters
3. Update refractivity score component
Display: Band Conditions Panel
For each band, show:
- Current score (0-100, colored badge)
- Estimated range (km, based on score tier)
- Key limiting factor (e.g., "High humidity: 16 g/m³", "Strong inversion detected")
- Trend arrow (improving/stable/degrading based on last hour)
- 6-hour sparkline or simple timeline of predicted scores
Display: Prediction Timeline
Band Now +1h +2h +3h +4h +5h +6h
10 GHz 78 ▲ 82 85 88 90 88 82
24 GHz 45 ▼ 42 38 35 32 35 42
47 GHz 52 → 54 55 58 60 58 52
75 GHz 40 ▼ 38 35 32 30 32 38
Confidence indicators: bold for +1-2h (high confidence), normal for +3-4h, dim for +5-6h.
Validation Against Known Contacts
Calibration Points
| Contact | Band | Distance | Conditions | algo1 Score | algo2 Score |
|---|---|---|---|---|---|
| W5LUA-K0VXM Jun 2024 | 10G | 1,609 km | 87°F, 49% RH, wind 8 kts | ~50 | ~75 |
| WA5WCP-W5VY Sep 2022 | 10G | 1,265 km | 69°F, 75% RH, wind 5 kts | ~55 | ~72 |
| W4DEX-K1WHS Sep | 10G | 1,212 km | 64°F, 80% RH, wind 3 kts | ~48 | ~78 |
| WB6CWN-AD6FP Aug | 24G | 526 km | 81°F, 39% RH, wind 11 kts | ~55 | ~82 |
| W6QI-AD6FP Oct | 47G | 343 km | 64°F, 60% RH, wind 8 kts | ~62 | ~78 |
| W0EOM-KF6KVG Feb | 47G | 246 km | 57°F, 33% RH, wind 6 kts | ~68 | ~85 |
algo2 better captures the favorable conditions for each band's physics:
- 10 GHz long contacts in humid conditions now score well (algo1 penalized humidity)
- 24/47 GHz dry contacts score well (algo1 also scored these well, but algo2 adds rain penalty and better humidity model)
Future Improvements
- Machine learning model — With 50K+ 10 GHz QSOs, train a gradient-boosted model directly on weather features → distance outcome
- Path-specific prediction — Use actual terrain profiles and station locations instead of generic area scores
- Sounding interpolation — Temporal interpolation between 00Z and 12Z soundings using surface obs trends
- NWS model soundings — Use RAP/HRRR forecast soundings for refractivity predictions
- Rain radar integration — Real-time NEXRAD data for precipitation scoring
- Regional calibration — Different weights for coastal vs inland vs mountain regions
ITU-R References
- P.453-14: Radio refractivity
- P.525: Free-space path loss
- P.676-13: Gaseous absorption (O₂ + H₂O)
- P.838-3: Specific rain attenuation model
- P.526: Diffraction
- P.452: Prediction procedure for evaluation of interference between stations
Data References
- ARRL Microwave Contest QSO database: 58,282 contacts (2019-2024)
- IEM ASOS observations: 95 stations, ±2hr window match
- IEM RAOB soundings: 9 stations, ±6hr window match