prop/docs/algo_updated_with_data.md
Graham McIntire a283ad9c66
Refactor HRRR fetch to batch points per hour
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.
2026-03-30 17:21:47 -05:00

37 KiB
Raw Permalink Blame History

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:

  1. Current conditions display — Real-time band-by-band propagation scores
  2. 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:

  1. Marine/coastal duct: Hot, very dry (87°F, 27% RH), moderate wind. California/Mexico coast paths.
  2. Radiation inversion: Cool, humid (64°F, 80% RH), calm. Northeast/Midwest overnight paths.

24 GHz > 200 km: Three mechanisms:

  1. Coastal duct in dry air: Hot, very dry (81°F, 39% RH). West Coast marine layer.
  2. Cold dry air: Cool, very dry (57°F, 33% RH). Winter enhanced refraction.
  3. 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

  1. Frequency-dependent humidity treatment — moisture is beneficial at 10 GHz (refractivity), harmful at 24+ GHz (absorption)
  2. Reduced wind penalty — data doesn't support the heavy wind penalty from algo1
  3. Pressure gradient over absolute — frontal activity matters more than pressure value
  4. Boundary layer depth — when sounding data available, use BL depth as a predictor
  5. 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:

  1. Diurnal temperature curve — predict T and Td based on time of day and current trajectory
  2. Inversion lifecycle — model inversion formation (evening), strengthening (overnight), peak (sunrise), and breakup (mid-morning)
  3. Pressure tendency — extrapolate from last 3 hours of observations
  4. Moisture trajectory — absolute humidity changes slowly; project forward from current trend
  5. 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

  1. ASOS surface observations (every 5-20 min): temp, dewpoint, wind, pressure, sky, visibility
  2. RAOB soundings (00Z and 12Z): refractivity profile, BL depth, inversions, ducts
  3. 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

  1. Machine learning model — With 50K+ 10 GHz QSOs, train a gradient-boosted model directly on weather features → distance outcome
  2. Path-specific prediction — Use actual terrain profiles and station locations instead of generic area scores
  3. Sounding interpolation — Temporal interpolation between 00Z and 12Z soundings using surface obs trends
  4. NWS model soundings — Use RAP/HRRR forecast soundings for refractivity predictions
  5. Rain radar integration — Real-time NEXRAD data for precipitation scoring
  6. 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