# Microwave Propagation Algorithm — Unified ## Overview Propagation scoring and prediction for microwave amateur radio bands (10-241 GHz), calibrated against 57,492 QSOs with distance data, validated against 18,540 signal samples from 7 commercial terrestrial links at 11/24/68 GHz, and grounded in ITU-R atmospheric models. The algorithm has two operating regimes: 1. **Beyond-LOS** — Ham radio paths (50-1000+ km) where atmospheric ducting and refraction are essential. This is the primary use case. 2. **LOS** — Known fixed links or short paths with clear Fresnel clearance where gaseous absorption is the dominant variable. The regime distinction matters because refractivity effects are *inverted* between the two: enhanced refraction extends beyond-LOS range but causes multipath fading on short LOS paths. ### Calibration Dataset **QSO data:** 57,492 tropospheric contacts across 13 bands (ARRL Microwave Contest, 2019-2024), filtered to exclude EME/satellite. Weather-matched against 95 ASOS stations (surface) and 9 RAOB stations (soundings). **Link data:** 18,540 samples from 7 commercial links near DFW (March 14-29 2026), correlated with KTKI ASOS and HRRR model refractivity profiles. No rain during observation period. **Confirmed long-range contacts:** - 47 GHz: 116.0 km (Nov 2025), 98.8 km (Jun 2024) - 24 GHz: 542.1 km (Sep 2002, longest), 101.0 km (Nov 2025) - 10 GHz: 1,609 km (longest in dataset) --- ## Part 1: Atmospheric Physics ### Absolute Humidity The single most important weather variable. Temperature and relative humidity are proxies; absolute humidity (g/m^3) directly determines gaseous absorption. ``` rho = 217 * (RH/100) * e_s / T_kelvin e_s = 6.112 * exp(17.67 * T_c / (T_c + 243.5)) # Magnus formula (hPa) ``` ### Surface Refractivity (ITU-R P.453-14) ``` N = 77.6 * P / T + 3.73e5 * e / T^2 P: pressure (hPa) T: absolute temperature (K) e: water vapor pressure (hPa) = 6.112 * exp(17.67 * Td_c / (Td_c + 243.5)) ``` N is a compound variable: both dry air (pressure/temperature) and moisture contribute. At 10 GHz, higher N increases beam bending (beneficial for beyond-LOS). At 24+ GHz, higher N usually means more moisture = more absorption (harmful), though the refractivity benefit partially offsets this. ### Modified Refractivity (M-units) ``` M = N + 0.157 * (h_agl) h_agl: height above ground level (m) ``` Ducting occurs where dM/dh < 0 (M decreases with height). Duct strength = delta-M across the inversion layer. ### Gaseous Absorption (ITU-R P.676-13) Total absorption per km = O2 component (fixed) + H2O component (humidity-dependent): | Band | f (GHz) | O2 (dB/km) | H2O Coeff (dB/km per g/m^3) | Total @ 7.5 g/m^3 | Dominant Constraint | |------|---------|------------|---------------------------|-------------------|---------------------| | 10G | 10.368 | 0.008 | 0.0005 | 0.012 | Negligible absorption | | 24G | 24.192 | 0.015 | 0.012 | 0.105 | 22.235 GHz H2O line | | 47G | 47.088 | 0.045 | 0.003 | 0.068 | O2 wing + mild H2O | | 68G | 68.040 | 0.90 | 0.007 | 0.95 | 60 GHz O2 band wing | | 75G | 76.032 | 0.012 | 0.006 | 0.057 | Window band | | 122G | 122.250 | 0.80 | 0.010 | 0.875 | 118.75 GHz O2 wing | | 134G | 134.928 | 0.08 | 0.015 | 0.193 | Between O2 118 & H2O 183 | | 142G | 142.000 | 0.05 | 0.025 | 0.238 | Approaching H2O 183 | | 241G | 241.000 | 0.08 | 0.30 | 2.33 | Between H2O 183 & H2O 325 | The 11 GHz and 24 GHz coefficients are validated by commercial link measurements. The 68 GHz coefficient is directly measured (0.1 dB/km per g/m^3 increase on a 2.8 km path, consistent with ITU-R model when O2 wing contribution is included). ### Rain Attenuation (ITU-R P.838-3) gamma_R = k * R^alpha (dB/km), R = rain rate (mm/hr): | Band | k_H | alpha_H | Light 4mm/hr | Moderate 10mm/hr | Heavy 25mm/hr | |------|------|------|-------------|-----------------|--------------| | 10G | 0.010 | 1.28 | 0.05 | 0.19 | 0.56 | | 24G | 0.070 | 1.07 | 0.31 | 0.81 | 2.04 | | 47G | 0.187 | 0.93 | 0.68 | 1.58 | 3.69 | | 68G | 0.310 | 0.86 | 0.98 | 2.18 | 4.73 | | 75G | 0.345 | 0.84 | 1.07 | 2.40 | 5.18 | | 122G | 0.498 | 0.77 | 1.32 | 2.93 | 5.91 | | 241G | 0.550 | 0.70 | 1.30 | 2.76 | 5.20 | Rain model is NOT validated by measured data (no rain events in link dataset). Coefficients are from ITU-R P.838-3 and interpolation. ### Free-Space Path Loss (ITU-R P.525) ``` FSPL = 20 * log10(d_km) + 20 * log10(f_GHz) + 92.45 (dB) ``` ### Fresnel Zone Radius ``` r_fresnel = sqrt(lambda * d1 * d2 / (d1 + d2)) lambda = 0.3 / f_GHz (meters) ``` ### Earth Bulge ``` bulge = (d1 * d2) / (2 * K * 6371000) K: effective earth radius factor (standard = 4/3) ``` ### Effective K-Factor from Surface N ``` dN_est = -40 - (N_surface - 315) * 0.25 K = 1 / (1 + 6371 * dN_est * 1e-6) K clamped to [0.5, 5.0] ``` --- ## Part 2: Key Empirical Findings These findings drive the scoring model's design. Each contradicts or refines assumptions from simpler models. ### Finding 1: Humidity Effect Reverses by Frequency The most important discovery. At 10 GHz, more moisture = longer paths (refractivity dominates, absorption negligible). At 24+ GHz, more moisture = shorter paths (absorption dominates). **10 GHz — humidity helps (N=52,456 QSOs):** | Abs. Humidity | Avg Dist | P90 Dist | |---------------|----------|----------| | 5-8 g/m^3 | 193 km | 342 km | | 11-14 g/m^3 | 215 km | 383 km | | 17+ g/m^3 | 230 km | 519 km | **24 GHz — humidity hurts (N=3,439 QSOs):** | Abs. Humidity | Avg Dist | P90 Dist | |---------------|----------|----------| | 5-8 g/m^3 | 115 km | 154 km | | 11-14 g/m^3 | 105 km | 174 km | | 17+ g/m^3 | 53 km | 103 km | **47 GHz — humidity hurts, less severely (N=576 QSOs):** | Abs. Humidity | Avg Dist | P90 Dist | |---------------|----------|----------| | 0-5 g/m^3 | 191 km | 234 km | | 11-14 g/m^3 | 71 km | 114 km | ### Finding 2: Wind Penalty Is Overrated Data shows no meaningful penalty for wind on achieved distance: | 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 may reduce inversion quality but also creates boundary-layer dynamics that can enhance propagation. Weight reduced from 18% to 8%. ### Finding 3: Low Pressure Correlates with Longer Distances Contradicts the original model's high-pressure = good assumption: | Pressure | 10G Avg | 10G P90 | 24G Avg | 47G Avg | |----------|---------|---------|---------|---------| | <1010 | 262 | 506 | 119 | 111 | | 1015-1020 | 217 | 383 | 97 | 74 | | 1025+ | 196 | 354 | 122 | -- | Low pressure systems bring frontal boundaries with strong temperature/moisture gradients that create inversions and ducts. The key is gradient structure, not absolute pressure. ### Finding 4: Boundary Layer Depth Sweet Spot Moderate BL depth (500-1000m) produces the best results across all bands — indicating an elevated inversion high enough to trap signals but not so deep that full convective mixing has occurred. ### Finding 5: Binary Duct Detection Is Weak Simple "duct detected yes/no" from soundings shows negligible correlation with distance (avg dist: 214 vs 216 km at 10 GHz). This is because soundings are 12-hourly point samples while conditions evolve continuously, and duct characteristics (strength, height) matter more than binary presence. ### Finding 6: Diurnal Signal Variation Sets a Noise Floor Commercial link data shows 1-5 dB daily variation even on perfectly clear, stable days. The algorithm should convey that even an "EXCELLENT" score has +/-2-3 dB inherent uncertainty. ### Finding 7: LOS vs Beyond-LOS Regimes Are Inverted On short LOS paths (3-7 km), sub-refractive conditions (dN/dh > -40/km) produce the best signal — minimal multipath, clean beam coupling. On long beyond-LOS paths (50-500+ km), enhanced refraction/ducting is essential. The algorithm must handle both regimes. --- ## Part 3: Band Configuration ```elixir @band_configs %{ 10_000 => %{ label: "10 GHz", o2_db_km: 0.008, h2o_coeff: 0.0005, humidity_effect: :beneficial, # More moisture = more refractivity = longer paths humidity_penalty: 0.0, 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, # Validated by commercial link data humidity_effect: :harmful, # 22.235 GHz H2O line shoulder humidity_penalty: 1.6, 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, # Window band, moderate H2O sensitivity 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 => %{ label: "68 GHz", o2_db_km: 0.90, # 60 GHz O2 band wing — validated by link data h2o_coeff: 0.007, # Measured: ~0.1 dB/km per g/m^3 on 2.8 km path 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 => %{ 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 => %{ label: "122 GHz", o2_db_km: 0.80, # 118.75 GHz O2 wing — weather independent 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 => %{ 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 => %{ label: "241 GHz", o2_db_km: 0.08, h2o_coeff: 0.30, # Extreme H2O sensitivity (183/325 GHz lines) 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 } } ``` --- ## Part 4: Scoring Functions (Beyond-LOS Regime) All scores return 0-100. The beyond-LOS regime is the primary use case for ham radio propagation prediction. ### 1. Humidity Score — Frequency-Dependent The critical insight: moisture helps at 10 GHz (refractivity) and hurts at 24+ GHz (absorption). ```elixir def score_humidity(abs_humidity_gm3, band_config) do case band_config.humidity_effect do :beneficial -> # 10 GHz: more moisture = higher surface N = more beam bending # Extreme humidity risks scintillation cond do abs_humidity_gm3 < 4 -> 55 # Very dry — poor 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 — scintillation onset true -> 75 # Tropical — scintillation risk end :harmful -> # 24+ GHz: H2O absorption dominates # Penalty factor scales by band (1.0 for 47G window, 1.6 for 24G near line, 3.0 for 241G) 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. Time of Day Score — Inversion Lifecycle The strongest diurnal predictor in the data. Dawn shows the highest P90 distances; late evening shows elevated averages. ```elixir @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] def score_time_of_day(utc_hour, utc_minute, month) do offset = if month >= 3 and month <= 10, do: -5, else: -6 # CDT/CST local = rem(utc_hour + utc_minute / 60 + offset + 24, 24) sunrise = Enum.at(@sunrise_table, month - 1) d = local - sunrise # hours relative to sunrise cond do d >= -1.5 and d <= 1.5 -> {100, "Peak — inversion maximum"} d > 1.5 and d <= 3.0 -> {78, "Good — inversion eroding"} d > -3.0 and d < -1.5 -> {82, "Pre-dawn — inversion building"} d > 3.0 and d <= 6.0 -> {38, "Marginal — boundary layer mixing"} local >= 20.0 or local <= 1.0 -> {72, "Evening — cooling, inversion reforming"} d > 6.0 -> {18, "Afternoon — full convective mixing"} true -> {55, "Night — gradual cooling"} end end ``` ### 3. Temperature-Dewpoint Depression — Frequency-Split Large depression = dry aloft = favorable for 24+ GHz. Small depression = moist = favorable for 10 GHz refractivity (but near-saturation risks fog). ```elixir def score_td_depression(temp_f, dewpoint_f, band_config) do dep = temp_f - dewpoint_f case band_config.humidity_effect do :beneficial -> cond do dep < 3 -> 40 # Near saturation — fog/scattering 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 -> cond do dep > 22 -> 96 # Very dry aloft dep > 14 -> 80 # Good stability dep > 8 -> 60 # Moderate dep > 4 -> 38 # Marginal true -> 18 # Humid aloft end end end ``` ### 4. Sky Cover Score Data shows modest impact at 24/47 GHz, near-zero at 10 GHz. VV (vertical visibility / fog) is a moderate penalty due to near-surface moisture content. ```elixir 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 ``` ### 5. Season Score Per-band lookup with optional adjustments. 24 GHz gets additional summer penalties due to Gulf moisture. ```elixir 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 ``` ### 6. Wind Score — Reduced Weight Data shows minimal impact on achieved distance. Retain mild penalty only for very high winds (turbulent scintillation). ```elixir def score_wind(speed_kts) do cond do speed_kts < 5 -> 100 speed_kts < 10 -> 90 speed_kts < 15 -> 75 speed_kts < 20 -> 55 speed_kts < 25 -> 35 true -> 15 end end ``` ### 7. Rain Score Not validated by measured data. Based on ITU-R P.838-3 attenuation per km. At 10 GHz, moderate rain is tolerable. Above 75 GHz, even light rain effectively kills the path. ```elixir def score_rain(rain_rate_mmhr, band_config) do 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) cond do gamma < 0.1 -> 95 gamma < 0.5 -> 75 gamma < 1.0 -> 50 gamma < 2.0 -> 25 gamma < 5.0 -> 10 true -> 0 end end end ``` ### 8. Pressure Score — Gradient-Focused Data contradicts the original high-pressure-is-good model. Frontal boundaries (changing pressure) create the strongest refractive gradients. Rising post-frontal pressure and slowly falling pre-frontal pressure both score well. ```elixir def score_pressure(current_mb, previous_mb) do case previous_mb do nil -> # No trend — mild scoring on absolute value cond do current_mb > 1025 -> 55 # Strong ridge current_mb > 1018 -> 65 # Mild high current_mb > 1010 -> 60 # Normal current_mb > 1005 -> 55 # Low 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 ``` ### 9. Refractivity Score — When Sounding/HRRR Data Available Best predictor when available, but only available from 12-hourly soundings or HRRR model. Binary duct detection is weak; refractivity gradient and BL depth are stronger signals. ```elixir def score_refractivity(sounding_or_hrrr, band_config) do cond do sounding_or_hrrr == nil -> 50 # No data — neutral sounding_or_hrrr.min_dn_dh < -500 -> # Super-refraction — ducting probable case band_config.humidity_effect do :beneficial -> 98 # 10 GHz benefits most from ducting :harmful -> 85 # Higher bands benefit but absorption limits range end sounding_or_hrrr.min_dn_dh < -200 -> 80 # Enhanced refraction sounding_or_hrrr.bl_depth_m != nil and sounding_or_hrrr.bl_depth_m >= 500 and sounding_or_hrrr.bl_depth_m <= 1000 -> 78 # Sweet-spot BL depth — elevated inversion sounding_or_hrrr.min_dn_dh < -100 -> 65 # Mild enhancement true -> 50 # Standard conditions end end ``` --- ## Part 5: Composite Score ### Weights | Factor | Weight | Rationale | |--------|--------|-----------| | Humidity | 22% | Dominant variable, but split role by frequency | | Time of Day | 18% | Strongest diurnal predictor in QSO data | | Td Depression | 14% | Proxy for humidity aloft — strong signal | | Sky Cover | 10% | Modest effect, mainly at higher frequencies | | Season | 10% | Long-term baseline | | Wind | 8% | Data shows minimal impact; penalty only for extremes | | Rain | 8% | Critical for 24+ GHz paths | | Pressure | 5% | Weak standalone predictor | | Refractivity | 5% | Best predictor when available, but often unavailable | ```elixir def composite_score(factors) do round( factors.humidity * 0.22 + factors.time_of_day * 0.18 + factors.td_depression * 0.14 + factors.sky * 0.10 + factors.season * 0.10 + factors.wind * 0.08 + factors.rain * 0.08 + factors.pressure * 0.05 + factors.refractivity * 0.05 ) end ``` ### Score Tiers with Per-Band Range Estimates | Score | Label | 10G | 24G | 47G | 75G | |-------|-------|-----|-----|-----|-----| | 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 | | Color | Hex | |-------|-----| | EXCELLENT | #00ffa3 | | GOOD | #7dffd4 | | MARGINAL | #ffe566 | | POOR | #ff9044 | | NEGLIGIBLE | #ff4f4f | --- ## Part 6: LOS Regime Scoring For known fixed links or short paths with confirmed Fresnel clearance. Key difference: sub-refraction is neutral/beneficial (minimal multipath), and gaseous absorption is the primary variable. ### LOS Refractivity Score ```elixir def score_refractivity_los(dn_dh) do cond do dn_dh > 0 -> 60 # Strong sub-refraction — unusual but not harmful dn_dh > -30 -> 85 # Moderate sub-refraction — stable, clean signal dn_dh > -40 -> 75 # Near standard dn_dh > -80 -> 60 # Enhanced — multipath onset dn_dh > -157 -> 45 # Strong enhancement — multipath likely true -> 30 # Super-refraction — significant multipath fading end end ``` ### LOS Surface N Score Higher N often means more moisture = more absorption at 24+ GHz. Validated by link data: N < 310 gave best 68 GHz signal, N > 340 gave worst. ```elixir def score_surface_n(n_value, band_config) do case band_config.humidity_effect do :beneficial -> cond do n_value > 350 -> 90 n_value > 330 -> 80 n_value > 315 -> 65 n_value > 300 -> 50 true -> 35 end :harmful -> cond do n_value < 300 -> 90 n_value < 315 -> 80 n_value < 330 -> 65 n_value < 345 -> 50 true -> 35 end end end ``` ### LOS vs Beyond-LOS Selection ```elixir def compute_score(conditions, band_config, path_type \\ :beyond_los) do base_factors = %{ humidity: score_humidity(conditions.abs_humidity, band_config), wind: score_wind(conditions.wind_speed_kts), sky: score_sky(conditions.sky_condition), time_of_day: score_time_of_day(conditions.utc_hour, conditions.utc_minute, conditions.month) |> elem(0), td_depression: score_td_depression(conditions.temp_f, conditions.dewpoint_f, band_config), season: score_season(conditions.month, band_config), pressure: score_pressure(conditions.slp, conditions.prev_slp), rain: score_rain(conditions.rain_rate, band_config) } factors = case path_type do :beyond_los -> Map.put(base_factors, :refractivity, score_refractivity(conditions.sounding, band_config)) :los -> Map.put(base_factors, :refractivity, score_refractivity_los(conditions.dn_dh)) end %{score: composite_score(factors), factors: factors} end ``` --- ## Part 7: Link Budget For point-to-point path analysis with known station parameters. ### EIRP ``` eirp_dbm = tx_power_dbm + tx_antenna_dbi - feed_loss_db ``` ### Receiver Sensitivity ``` sensitivity_dbm = -174 + noise_figure_db + 10 * log10(bandwidth_hz) CW: bandwidth = 500 Hz SSB: bandwidth = 2700 Hz ``` ### Total Path Loss ``` total_loss = FSPL + gaseous_absorption + rain_attenuation + diffraction_loss - duct_enhancement gaseous_absorption = (o2_db_km + h2o_coeff * rho) * distance_km rain_attenuation = gamma_R * distance_km * rain_effective_fraction ``` ### Duct Enhancement (Beyond-LOS Only) Calibrated against confirmed contacts: ```elixir def duct_enhancement_db(prop_score) do cond do prop_score >= 80 -> -14 # 14 dB improvement prop_score >= 65 -> -10 prop_score >= 50 -> -6 prop_score >= 33 -> -2 true -> 0 end end ``` ### Knife-Edge Diffraction (ITU-R P.526) ```elixir def knife_edge_loss(v) do cond do v <= -0.7787 -> 0 # Clear v <= 0 -> -20 * :math.log10(0.5 - 0.62 * v) v <= 1 -> -20 * :math.log10(0.5 * :math.exp(-0.95 * v)) v <= 2.4 -> inner = max(0, 0.1184 - (0.38 - 0.1 * v) ** 2) -20 * :math.log10(0.4 - :math.sqrt(inner)) true -> 20 * :math.log10(v) + 13.0 # Asymptotic end end ``` ### Success Probability ```elixir def margin_to_success(margin_db, prop_score) do margin_pct = cond do margin_db <= 0 -> 0 margin_db <= 10 -> margin_db / 10 * 20 margin_db <= 15 -> 20 + (margin_db - 10) / 5 * 20 margin_db <= 20 -> 40 + (margin_db - 15) / 5 * 20 margin_db <= 25 -> 60 + (margin_db - 20) / 5 * 20 margin_db <= 30 -> 80 + (margin_db - 25) / 5 * 20 true -> 100 end # Propagation modulation: score 100 -> x1.30, score 50 -> x1.00, score 0 -> x0.70 prop_factor = 0.70 + (prop_score / 100) * 0.60 max(0, min(99, round(margin_pct * prop_factor))) end ``` ### Note: Antenna Height & Duct Coupling Geometry Antenna height and dish elevation angle affect how efficiently a station couples into an atmospheric duct. This is a real physical effect but is **second-order** to duct characteristics at the ranges this model targets (50-1000+ km). **Why it's not in the scoring model:** - At >300 km, the duct's own refractive gradient (k-factor) dominates over all antenna geometry. The required aim angle to graze a duct converges toward 0° regardless of antenna height. - Antenna height differences in the 15-21m range (typical amateur stations) shift beam geometry by ~0.001° at long range — well within the ±2-3 dB noise floor from diurnal variation. - The primary benefit of antenna height (50+ ft) is clearing local obstructions and ground clutter in the near field (0-20 km), not geometric coupling to the duct layer. - VE4MA (50 ft, flat prairie) and W5LUA (70 ft, suburban) achieve similar range classes, confirming duct geometry is the dominant term. **Where it matters — beamwidth vs frequency:** At 10 GHz a typical 60cm dish has ~3° beamwidth, making elevation angle errors forgiving. At 24 GHz beamwidth shrinks to ~1.5°, at 47 GHz to <1°. A 0.3° aim error that is irrelevant at 10 GHz becomes a contact killer at 47 GHz. If station profiles (antenna height, dish size, elevation setting) are added in the future, a frequency-dependent **beamwidth coupling penalty** in `margin_to_success` would be the right integration point — penalizing paths where the required aim angle to the detected duct layer exceeds the antenna's half-power beamwidth. --- ## Part 8: Short-Term Prediction Model ### Approach Extrapolate current conditions forward 1-6 hours using observed trends, diurnal models, and forecast data when available. ### Prediction Confidence Based on commercial link signal prediction accuracy: | Horizon | Observed Accuracy | Confidence | |---------|-------------------|------------| | Current | +/- 1 dB | 95% | | +30 min | +/- 1.5 dB | 90% | | +1 hr | +/- 2 dB | 85% | | +2 hr | +/- 3 dB | 75% | | +3 hr | +/- 4 dB | 60% | | +6 hr | +/- 5 dB | 40% | ### Diurnal Temperature Model ```elixir def project_temperature(current_temp_f, trend_per_hour, hours_ahead, future_local_hour, month) do sunrise = Enum.at(@sunrise_table, month - 1) diurnal_rate = cond do future_local_hour < sunrise - 1 -> -0.5 # Pre-dawn: slow cooling future_local_hour < sunrise + 2 -> 0.0 # Sunrise transition future_local_hour < 15 -> 2.0 # Morning: warming future_local_hour < 18 -> 0.5 # Late afternoon future_local_hour < 21 -> -1.5 # Evening: cooling true -> -1.0 # Night: slow cooling end # Blend: current trend dominates short-term, diurnal model dominates long-term weight = min(1.0, hours_ahead / 4.0) blended_rate = trend_per_hour * (1.0 - weight) + diurnal_rate * weight current_temp_f + blended_rate * hours_ahead end ``` ### Prediction Flow ```elixir def predict_scores(current_obs, obs_3hr_ago, forecast, band_config) do temp_trend = (current_obs.temp_f - obs_3hr_ago.temp_f) / 3 dp_trend = (current_obs.dewpoint_f - obs_3hr_ago.dewpoint_f) / 3 pressure_trend = (current_obs.slp - obs_3hr_ago.slp) / 3 for hours_ahead <- 1..6 do future_time = DateTime.add(current_obs.observed_at, hours_ahead * 3600) month = future_time.month projected_temp = project_temperature(current_obs.temp_f, temp_trend, hours_ahead, future_time.hour, month) projected_dp = current_obs.dewpoint_f + dp_trend * hours_ahead projected_slp = current_obs.slp + pressure_trend * hours_ahead projected_sky = forecast_value(forecast, :sky, hours_ahead) || current_obs.sky_condition projected_rain = forecast_value(forecast, :rain_rate, hours_ahead) || 0 projected_wind = forecast_value(forecast, :wind_kts, hours_ahead) || current_obs.wind_speed_kts # Compute absolute humidity from projected values tc = (projected_temp - 32) * 5 / 9 td_c = (projected_dp - 32) * 5 / 9 es = 6.112 * :math.exp(17.67 * tc / (tc + 243.5)) 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) 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_time.hour, future_time.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 # Cannot predict from surface obs alone } %{ hours_ahead: hours_ahead, time: future_time, score: composite_score(factors), factors: factors, confidence: prediction_confidence(hours_ahead) } end end def prediction_confidence(hours_ahead) do case hours_ahead do 1 -> 0.85 2 -> 0.75 3 -> 0.60 4 -> 0.50 5 -> 0.40 6 -> 0.30 _ -> 0.20 end end ``` --- ## Part 9: Sounding & Refractivity Analysis ### Refractivity Profile from Sounding ```elixir def compute_refractivity_profile(levels, sfc_height_m) do Enum.map(levels, fn level -> t_k = level.temp_c + 273.15 e = 6.1121 * :math.exp((18.678 - level.temp_c / 234.5) * (level.temp_c / (257.14 + level.temp_c))) e_actual = if level.dewpoint_c, do: 6.1121 * :math.exp((18.678 - level.dewpoint_c / 234.5) * (level.dewpoint_c / (257.14 + level.dewpoint_c))), else: 0 n = 77.6 * level.pressure_hpa / t_k + 3.73e5 * e_actual / (t_k * t_k) h_agl = level.height_m - sfc_height_m m = n + 0.157 * h_agl %{height_agl: h_agl, n: n, m: m, temp_c: level.temp_c, dewpoint_c: level.dewpoint_c} end) end ``` ### Duct Detection Duct exists where dM/dh < 0. Filter for strength > 2 M-units. ```elixir def detect_ducts(profile) do profile |> Enum.chunk_every(2, 1, :discard) |> Enum.reduce({[], nil}, fn [below, above], {ducts, duct_start} -> dm = above.m - below.m cond do dm < 0 and duct_start == nil -> {ducts, %{base: below.height_agl, base_m: below.m}} dm >= 0 and duct_start != nil -> strength = duct_start.base_m - below.m if strength > 2 do duct = %{base: duct_start.base, top: below.height_agl, strength: strength} {[duct | ducts], nil} else {ducts, nil} end true -> {ducts, duct_start} end end) |> elem(0) |> Enum.reverse() end ``` ### Inversion Detection Temperature increasing with height. Merge adjacent inversions within 200m gap. Filter: strength >= 0.5C, base < 5000m AGL. ### Stability Indices ``` K-Index = (T850 - T500) + Td850 - (T700 - Td700) Lifted Index = T500 - (Tsfc - (h500 - h_sfc) * 0.00976) LI < 0: Unstable (convection likely, inversion destroyed) LI > 0: Stable (inversion maintained) Precipitable Water = sum[(MR_i + MR_{i-1}) / 2 * dP / (9.81 * 10)] MR = 622 * e / (P - e) ``` ### Boundary Layer Depth Find height where potential temperature (theta = T + 9.8 * h/1000) exceeds surface theta by 2C. The 500-1000m sweet spot indicates an elevated inversion — high enough to trap signals, not so deep that full mixing has occurred. --- ## Part 10: Band-Specific Propagation Mechanisms ### Coupling Sensitivity by Frequency Duct coupling geometry becomes increasingly critical at higher frequencies due to narrower antenna beamwidths. A dish aimed 0.3° away from the optimal duct grazing angle: - **10 GHz** (~3° beamwidth): Still within half-power beam — negligible loss - **24 GHz** (~1.5° beamwidth): Approaching beam edge — moderate coupling loss - **47 GHz** (<1° beamwidth): Outside half-power beam — potential contact killer - **75+ GHz** (<0.5° beamwidth): Precision aim required — elevation error dominates For surface ducts, the beam must arrive at <0.5° grazing incidence to be trapped. For elevated ducts (500-1500m AGL), the optimal elevation angle is path-distance dependent: slightly positive at close range, near-zero at the "sweet spot" distance, and slightly negative at extreme range due to Earth curvature. ### 10 GHz (3cm) — Tropospheric Ducting Band **Primary mechanisms:** Ducting, enhanced refraction **Key variable:** Refractivity profile, NOT humidity absorption (0.012 dB/km total is negligible) **Best conditions:** Moderate-high humidity (12-20 g/m^3), temperature inversions, stable atmosphere, late evening through early morning **Unique:** Largely insensitive to rain. Can propagate through cloud decks. Marine ducting produces 1000+ km coastal paths. Frontal boundaries create strong refractive gradients. ### 24 GHz (1.2cm) — Water Vapor Line Band **Primary mechanisms:** Ducting (reduced by absorption), enhanced refraction **Key variable:** Absolute humidity (22.235 GHz H2O line makes this THE most humidity-sensitive band) **Best conditions:** Very dry air (<8 g/m^3), cold season (Nov-Mar), clear skies, pre-dawn through early morning **Unique:** 10x more sensitive to water vapor than 10 GHz. Summer Gulf moisture devastates range. Rain scatter is a viable alternative mechanism (710 km QSO documented). ### 47 GHz (6mm) — Atmospheric Window **Primary mechanisms:** Ducting (in atmospheric window), enhanced LOS **Key variable:** Balance of humidity and refractivity; very dry air dramatically helps **Best conditions:** Dry air (<8 g/m^3), clear skies, strong inversions, early morning **Unique:** Window between 22 GHz H2O and 60 GHz O2. O2 absorption ~0.045 dB/km is fixed. Ducting is the ONLY way beyond ~150 km. ### 68 GHz — V-Band Edge **Primary mechanisms:** LOS only (O2 absorption limits range) **Key variable:** O2 wing absorption (~0.9 dB/km, weather-independent) + humidity **Best conditions:** Cold/dry air, no precipitation, short paths **Unique:** Validated by link data showing 3-5 dB diurnal fades on 2.8 km path. O2 absorption caps practical range regardless of conditions. Viable for short links (<5 km), very challenging for beyond-LOS. ### 75 GHz (4mm) — Window Band **Primary mechanisms:** Rare ducting, enhanced LOS **Key variable:** Dry air + no precipitation **Best conditions:** Very dry (<5 g/m^3), no rain, strong inversions, winter **Unique:** 289 km record (California marine duct). Only 81 QSOs with distance data. Rain attenuation severe (~1 dB/km at 4 mm/hr). ### 122 GHz (2.5mm) — O2 Line Wing **Primary mechanisms:** Enhanced LOS, rare ducting **Key variable:** O2 absorption from 118.75 GHz line (~0.8 dB/km, cannot be improved by weather) **Best conditions:** Cold temperatures (reduce O2 line broadening), very dry, no rain **Unique:** 139 km record (California, February). Practically limited to ~50 km reliable paths. ### 134 GHz — Mini Window **Primary mechanisms:** Enhanced LOS **Key variable:** Between O2 118 and H2O 183 lines **Best conditions:** Cold, dry, no precipitation **Unique:** 157 km record (Germany, March). Better than 122 GHz due to distance from O2 line. ### 241 GHz (1.2mm) — Submillimeter **Primary mechanisms:** LOS only **Key variable:** H2O absorption dominates (~0.3 dB/km per g/m^3) **Best conditions:** Extremely dry (<3 g/m^3), winter-only in most US locations, high altitude stations **Unique:** 114 km record (Virginia, January). Total path loss at 100 km is ~410 dB without ducting. Realistic to display "viable / not viable" rather than a score. --- ## Part 11: Data Flow & Implementation ``` Surface Observations (ASOS, every 5-20 min) -> temp, dewpoint, wind, pressure, sky, visibility -> compute: abs_humidity, Td depression -> per-band scoring functions -> composite score per band -> 6-hour prediction timeline Sounding Data (RAOB 00Z/12Z) + HRRR Model (hourly) -> refractivity profile, dN/dh gradient, ducts, BL depth -> refractivity score component -> regime classification (LOS vs beyond-LOS for specific paths) Terrain Data (SRTM) -> path profile, Fresnel clearance, earth bulge -> determines LOS vs beyond-LOS regime -> diffraction loss calculation Link Budget (point-to-point) -> FSPL + gaseous + rain + diffraction - duct enhancement -> margin = RX power - sensitivity -> success % = margin_to_success(margin, prop_score) ``` ### Display: Band Conditions Panel For each band: - Current score (0-100, colored badge) - Estimated range (km, from score tier table) - Key limiting factor ("High humidity: 16 g/m^3", "Strong inversion detected") - Trend arrow (improving/stable/degrading from last hour) - 6-hour prediction timeline with confidence shading --- ## Constants Reference | Constant | Value | Source | |----------|-------|--------| | Earth radius | 6371 km | WGS-84 mean | | Standard K-factor | 4/3 | Standard atmosphere | | Standard surface N | 315 | ITU-R P.453 | | Standard dN/dh | -40 /km | ITU-R P.453 | | Humidity penalty 24 GHz | 1.6 | Near 22.235 GHz H2O peak | | Humidity penalty 47 GHz | 1.0 | Atmospheric window | | Humidity penalty 68 GHz | 1.4 | 60 GHz O2 wing + H2O | | Humidity penalty 241 GHz | 3.0 | Between H2O 183 & 325 | | Duct M-unit threshold | 2 | Noise filter | | Inversion min strength | 0.5C | Below is noise | | Inversion height limit | 5000m AGL | Above irrelevant | | BL depth sweet spot | 500-1000m | Empirical from QSO data | | Signal prediction floor | +/- 2-3 dB | Measured from link data | ## ITU-R References - P.453-14: Radio refractivity - P.525: Free-space path loss - P.676-13: Gaseous absorption (O2 + H2O) - P.838-3: Specific rain attenuation - P.526: Diffraction - P.452: Interference between stations ## Data References - ARRL Microwave Contest QSO database: 57,492 contacts with distance (2019-2024) - IEM ASOS observations: 95 stations, +/-2hr window match - IEM RAOB soundings: 9 stations, +/-6hr window match - Commercial link data: 18,540 samples, 7 links at 11/24/68 GHz (March 2026) - HRRR model: hourly refractivity profiles (March 2026 validation period)