Microwave Propagation Algorithm

Overview

Propagation scoring and prediction for amateur radio bands from 902 MHz through 241 GHz, calibrated against 57,488 tropospheric QSOs with distance data, validated against 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. Terrain analysis confirms 97.2% of all QSO paths are terrain-blocked (average diffraction loss 36.2 dB), making atmospheric propagation mechanisms the sole enabler.
  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: 81,994 total QSOs across 20 bands (ARRL Microwave Contest logs 1991-present, user submissions, and ADIF imports). All tropospheric (distance < 3,000 km). Enriched with HRRR model profiles at 3 km / hourly for 2014-present contacts and NARR reanalysis (NCEP, 32 km Lambert / 3-hourly, 1979-01 → 2014-10) for the small pre-HRRR tail, plus IEMRE gridded hourly observations, RAOB soundings, NEXRAD n0q composite reflectivity for common-volume radar analysis, commercial-link SNMP samples at Princeton TX, and per-QSO terrain path profiles. RTMA (2.5 km, 15-min) supplements real-time surface conditions between HRRR hours. Critical bias: ~99% of contacts are Aug-Sep — all atmospheric correlations are effectively summer-only findings, with the algo's seasonal tables driven by per-month sounding ducting probabilities rather than per-month QSO distance.

Link data: 7 commercial links near DFW (Princeton TX area) at 11/24/68 GHz, polled via SNMP at 5-minute intervals. All links use KTKI ASOS for weather correlation. Live polling is active; historical dataset from March 14-29 2026 (18,540 samples) was used for initial algorithm validation.

Terrain analysis: 58,361 QSO paths profiled — 56,735 BLOCKED (97.1%, avg 36.3 dB diffraction), 1,284 CLEAR (2.2%), 342 FRESNEL_PARTIAL (0.6%). Blocked paths average longer distances than clear paths (326 km for 40+ dB diffraction vs 31 km for CLEAR) because ducting enables beyond-LOS paths by definition — only the strongest propagation conditions produce contacts through heavy terrain at long distances.

Confirmed long-range contacts:

  • 47 GHz: 116.0 km (Nov 2025), 98.8 km (Jun 2024)
  • 24 GHz: 710.0 km (CW), 542.1 km (Sep 2002, longest confirmed tropo)
  • 10 GHz: 2,393 km (longest tropospheric in dataset)

Additional data sources (not yet integrated into scoring):

  • Solar indices: 9,586 daily values (1998-2026) — SFI, SSN, Ap/Kp. Not relevant for tropospheric microwave propagation but available for future VHF/sporadic-E extension.
  • IEMRE gridded hourly data: weather at QSO endpoint grid points (0.125° resolution) with percentage sky cover, soil temperature, wind components. More granular than nearest-ASOS matching.

Meteorological Foundations

This section documents the atmospheric physics and NWP integration underlying the propagation prediction system. The system forecasts tropospheric microwave propagation conditions (10–241 GHz) from HRRR model output, RAOB soundings, and ASOS surface observations. The primary mechanisms of interest are tropospheric ducting via refractivity gradients and frequency-dependent gaseous/hydrometeor attenuation.

Refractivity Framework

Radio refractivity N (ITU-R P.453-14):

N = 77.6·P/T + 3.73×10⁵·e/T²

P (hPa), T (K), e = water vapor pressure (hPa) via Buck equation from Td. The wet term contributes 20–40% of total N in the ABL. Modified refractivity M = N + 0.157·h (h in m AGL); ducting where dM/dh < 0.

The vertical gradient dN/dh governs ray curvature via the effective earth radius factor k = 1/(1 + R·dN/dh·10⁻⁶):

dN/dh (N/km)kRegime
> 0< 1Sub-refraction
0 to −791.0–1.33Standard
−79 to −1571.33–∞Super-refraction
< −157negativeDucting (ray curvature exceeds earth curvature)

Ducting Mechanisms

Four mechanisms produce the negative dN/dh gradients that enable beyond-LOS propagation. Each creates a sharp temperature increase and/or moisture decrease with height:

1. Radiation (Nocturnal) Ducts — Surface-based temperature inversion from radiative cooling under clear skies, light winds. Moisture trapped below the inversion cap. Forms 1–3h post-sunset, peaks pre-dawn, erodes within 1–2h of insolation. Typical depth 50–300m AGL, dN/dh −100 to −300 N/km. Primary mechanism in our dataset — operators target dawn windows specifically.

2. Advection Ducts — Warm, dry air mass overriding a cooler surface (SST discontinuity, lake, post-frontal cold ground). Strong temperature increase with moisture decreasing sharply at the air-mass interface. Persistent (hours to days), independent of diurnal cycle. Depth 50–500m, often elevated. dN/dh −200 to −500+ N/km. Dominant along Gulf Coast, California coast, Great Lakes.

3. Subsidence Ducts — Synoptic-scale subsidence inversion (ridge axis, subtropical high) with trapped moisture below the inversion base. Visible on soundings as sharp temperature increase at 800–900 hPa with coincident Td drop. Persistent with ridging. Depth 500–2000m AGL (elevated duct). dN/dh −100 to −200 N/km.

4. Frontal/Boundary Ducts — Mesoscale refractivity gradients along cold fronts (post-frontal moisture drop), warm fronts (overrunning moist air), outflow boundaries, and drylines. Transient. Our data confirms low-pressure systems correlate with extended propagation (262 km avg vs 196 km at 1025+ hPa for 10 GHz), consistent with frontal boundary structure.

NWP Integration (HRRR)

Primary atmospheric data source: NOAA HRRR v4 (3 km, hourly, 18h forecast cycle).

Extracted fields (GRIB2 via byte-range requests from AWS S3):

  • Surface: T₂ₘ, Td₂ₘ, Psfc, U₁₀ₘ/V₁₀ₘ, TCDC, APCP
  • ABL: HPBL (diagnosed PBL height), PWAT (column-integrated precipitable water)
  • Pressure levels: T, Td, Z at every 25 hPa from 1000–700 hPa (13 levels, ~80m vertical spacing below 900 hPa)

Derived products:

  1. N(h) profile at each pressure level → dN/dh minimum (primary ducting discriminant)
  2. M(h) profile → explicit duct detection (dM/dh < 0 layers with strength Δ M > 2 M-units)
  3. Surface N from Psfc, T₂ₘ, Td₂ₘ
  4. Absolute humidity ρ = 217·es(Td)/T
  5. Dynamic k-factor for terrain diffraction (ITU-R P.526-16)

Vertical resolution limitation: The 25 hPa pressure level spacing resolves features ≥100m thick. Thin surface ducts (50–100m) detectable by RAOB at ~10m resolution may be missed. Comparative statistics: HRRR gradients cluster −40 to −130 N/km (median −70); collocated RAOB gradients extend to −500+ N/km. Scoring thresholds are calibrated to HRRR-derived gradient distributions, with RAOB duct detections used as supplementary data where available (3,901 profiles from 112 stations).

Path-Integrated HRRR Scoring

Contact scoring uses HRRR profiles at all path points (pos1, midpoint, pos2) rather than just the transmitter location. The aggregation strategy reflects physical reality:

  • Best along path for beneficial factors (refractivity gradient, surface pressure): a duct or frontal boundary at any point along the path can enable propagation.
  • Worst along path for harmful factors (rain, wind): a rain cell or turbulence at any segment degrades the entire link.
  • Average along path for neutral factors (temperature, dewpoint, PWAT, BL depth): path-integrated moisture and stability represent the bulk atmospheric state.

Time-of-day, season, and sky cover are taken from the first profile (uniform along the path at these scales).

NARR Reanalysis (Historical Backfill)

Secondary atmospheric source for contacts where HRRR is unavailable — pre-2014 QSOs (~200 contacts in the current corpus). NCEP NARR (North American Regional Reanalysis) covers 1979-01-01 through 2014-10-02 on a 32 km Lambert conformal grid at 3-hourly cadence.

Access: NCEI anonymous HTTP (no quota, no auth). NarrClient.in_coverage?/1 gates fetches to the supported window. Downloads are GRIB2 via wgrib2 + cdo; the raw numeric NCEP codes (via -outputtab,code,lev,value) are used instead of shortnames because cdo emits different shortname conventions across versions.

Extracted fields:

  • Surface: T₂ₘ, Td₂ₘ, Psfc, U₁₀ₘ/V₁₀ₘ, PWAT (total column water), HPBL
  • Upper-air: T, Td, Z at standard pressure levels 1000, 925, 850, 700 hPa

Derived products: Same as HRRR — N(h) profile, dN/dh minimum, M(h) duct detection, surface refractivity via SoundingParams.derive/1. NARR profiles land in narrprofiles with the same schema as hrrrprofiles so downstream code is source-agnostic.

Resolution trade-off: NARR's 32 km / 3-hourly cadence is ~10× coarser spatially and ~3× coarser temporally than HRRR's 3 km / hourly. For the pre-2014 tail it's still far better than no atmospheric data at all.

Unified lookup: Weather.bestprofileforcontact/1 picks HRRR first and falls back to NARR when the HRRR archive doesn't cover the contact. Weather.profilesalong_path/1 does the same for path-integrated scoring.

RTMA (Real-Time Mesoscale Analysis)

Supplementary surface data source: NOAA RTMA at 2.5 km resolution with 15-minute analysis cycles. Available on AWS S3 at s3://noaa-rtma-pds/.

What RTMA adds: 4x temporal resolution over HRRR for surface conditions. Captures rapidly evolving mesoscale events (outflow boundaries, sea breeze fronts, convective gust fronts) between HRRR hourly cycles.

Fields: T₂ₘ, Td₂ₘ, Psfc, U₁₀ₘ/V₁₀ₘ, visibility. No vertical profiles, HPBL, PWAT, or refractivity gradient — those still come from HRRR or NARR.

Access: GRIB2 via byte-range HTTP requests from S3, same pattern as HRRR. No authentication required.

Surface Observations (ASOS)

ASOS provides in-situ validation and additional parameters not in HRRR output:

  • T, Td → ρ(abs humidity), T-Td depression (inversion proxy)
  • Wind speed → mechanical mixing potential (light winds favor inversion persistence)
  • Sky condition → longwave radiation budget (CLR promotes nocturnal cooling)
  • Psfc → absolute N computation, barometric trend (pressure tendency)
  • Precipitation type/rate → hydrometeor attenuation (ITU-R P.838-3)

Frequency-Dependent Attenuation

The same thermodynamic state produces opposing propagation effects across the spectrum:

10 GHz (λ = 3 cm): Gaseous attenuation negligible (0.012 dB/km). Propagation governed entirely by refractivity structure. Increased ρ raises N, enhancing beam bending — moisture is beneficial. Rain attenuation mild (γR = 0.05 dB/km at 4 mm/hr).

24 GHz (λ = 1.25 cm): Proximal to the 22.235 GHz H₂O rotational line. H₂O absorption coefficient 0.012 dB/km per g/m³ (10× the 10 GHz rate). Moisture degrades path budget despite refractivity benefit — net effect is harmful. Rain attenuation 6× that of 10 GHz.

47–75 GHz: Transition regime. 47 GHz in a relative window (O₂ wing + mild H₂O). 68 GHz on the wing of the 60 GHz O₂ absorption complex (γO₂ = 0.9 dB/km). 75 GHz in a window band (γtotal ≈ 0.057 dB/km). Ducting is the sole mechanism enabling paths beyond ~20 km.

122–241 GHz: Dominated by gaseous absorption. 122 GHz on the 118.75 GHz O₂ line wing (0.8 dB/km). 134 GHz in the window between O₂ 118 and H₂O 183 lines. 241 GHz between H₂O 183 and 325 GHz lines (0.3 dB/km per g/m³). All contacts in the dataset above 122 GHz are CW mode — link budget is that tight.

ABL Diurnal Cycle

The ABL diurnal cycle is the most predictable propagation driver:

  1. Post-sunset → sunrise: Radiative cooling develops surface-based inversion. HPBL collapses from O(10³m) to O(10²m). Super-refractive/ducting conditions develop.
  2. Dawn (sunrise ± 1.5h): Peak inversion strength, minimum HPBL. Maximum ducting probability.
  3. Morning transition (+1.5 to +3h): Shortwave heating erodes inversion from below. Convective mixing deepens ABL.
  4. Afternoon (+6h): Fully convective ABL, maximum HPBL. Minimum propagation. Turbulent scattering dominates residual refractivity gradients.

Observed diurnal enhancement (night/dawn over afternoon baseline): +4% at 10 GHz, +28% at 24 GHz, +36% at 47 GHz, +360% at 75 GHz. At EHF, time of day dominates all other predictors. The system uses longitude-based solar time (hour + lon/15) rather than UTC for diurnal scoring — Spearman ρ with distance improves from 0.056 (UTC) to 0.188 (solar) at 24 GHz.

Seasonal Cycle

Ducting probability from 3,901 RAOB profiles (CONUS):

MonthDucting %Mean dN/dh minMean PWAT (mm)
Jun68.7%−32328.6
Jul76.5%−30127.6
Aug53.9%−26133.3
Sep56.4%−28726.2
Oct60.4%−31417.9
Mar10.8%−1136.6
Dec–Feb12–22%−1307–10

Peak Jun–Jul, secondary peak Oct–Nov (autumn radiative cooling with residual moisture). March minimum (frequent mixing events). At 24+ GHz the seasonal optimum inverts — winter minimizes H₂O absorption despite lower ducting probability. Band-specific seasonal weights account for this.

Known Limitations

  1. HRRR vertical resolution: 25 hPa spacing (recently improved from ~100 hPa) may still miss thin surface ducts <100m. RAOB data used as supplementary duct detection source.
  1. Sub-grid mesoscale: Sea breeze fronts, outflow boundaries, terrain-induced convergence zones — ducting features below the 3 km HRRR grid are not resolved.
  1. Hydrometeor attenuation: ITU-R P.838-3 coefficients applied but unvalidated against measured data. Rainscatter propagation (observed at 24 GHz via FM mode) is not modeled.
  1. Fog/cloud: Cloud cover percentage is used as a proxy; direct fog/cloud attenuation modeling is not implemented. Relevant above 47 GHz where dense fog adds 1–5 dB/km.
  1. Scintillation: Amplitude scintillation from refractive turbulence on long LOS paths is not modeled.
  1. Temporal resolution: Hourly HRRR updates. Rapidly evolving mesoscale features (convective outflows, sea breeze onset) may lag reality between analysis cycles.

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):

Bandf (GHz)O2 (dB/km)H2O Coeff (dB/km per g/m^3)Total @ 7.5 g/m^3Dominant Constraint
902M0.9020.0060.00.006Negligible absorption
1296M1.2960.0060.00.006Negligible absorption
2304M2.3040.0060.00.006Negligible absorption
3456M3.4560.0060.00.006Negligible absorption
5760M5.7600.0070.00.007Negligible absorption
10G10.3680.0070.00.007Negligible absorption
24G24.1920.020.0020.03522.235 GHz H2O line
47G47.0880.040.0030.063O2 wing + mild H2O
68G68.0400.900.0070.9560 GHz O2 band wing
75G76.0320.0120.0060.057Window band
122G122.2500.800.0100.875118.75 GHz O2 wing
134G134.9280.080.0150.193Between O2 118 & H2O 183
142G142.0000.050.0250.238Approaching H2O 183
145G145.0000.060.0400.360H2O 183 line wing
241G241.0000.080.302.33Between H2O 183 & H2O 325
288G288.0000.100.453.48Approaching H2O 325
322G322.0000.120.554.25Near H2O 325 line
403G403.0000.150.403.15Past H2O 325, sub-mm window
411G411.0000.150.423.30Sub-mm window

Coefficients ≥142 GHz interpolate ITU-R P.676/P.838 trends across the H2O 183/325 lines and become extrapolations above 241 GHz where the contact corpus has only 1 sample per band. They are scaffolding so the scoring pipeline does not silently drop sub-mm contacts; calibration will need to wait until enough sub-mm activity accumulates.

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):

Bandk_Halpha_HLight 4mm/hrModerate 10mm/hrHeavy 25mm/hr
902M0.0001.000.000.000.00
1296M0.0001.000.000.000.00
2304M0.0011.150.0050.010.04
3456M0.0021.200.010.030.08
5760M0.0051.250.020.090.24
10G0.0101.280.050.190.56
24G0.0701.070.310.812.04
47G0.1870.930.681.583.69
68G0.3100.860.982.184.73
75G0.3450.841.072.405.18
122G0.4980.771.322.935.91
134G0.5200.751.342.935.81
142G0.5300.741.342.925.74
145G0.5350.741.352.955.79
241G0.5500.701.302.765.20
288G0.5600.681.272.664.90
322G0.5700.661.232.554.62
403G0.5800.641.202.454.36
411G0.5800.641.202.454.36

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, dynamic from HRRR)

Effective K-Factor (ITU-R P.526-16 Section 2)

Computed from the HRRR refractivity gradient (dN/dh in N-units/km):

k = 1 / (1 + 6371 * dN_dh * 1e-6)
dN/dh (N/km)kCondition
01.0No refraction
−394/3Standard atmosphere
−100~2.7Enhanced refraction
−157Ray follows earth curvature
< −157negativeSuper-refraction / ducting

When HRRR data is available for a QSO, the actual refractivity gradient is used. Falls back to k=4/3 when unavailable. The k-factor is capped at 100 to avoid numerical issues near ducting conditions.


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=53,013 QSOs):

Abs. HumidityAvg DistP90 Dist
5-8 g/m^3193 km342 km
11-14 g/m^3215 km383 km
17+ g/m^3230 km519 km

24 GHz — humidity hurts (N=3,639 QSOs):

Abs. HumidityAvg DistP90 Dist
5-8 g/m^3115 km154 km
11-14 g/m^3105 km174 km
17+ g/m^353 km103 km

47 GHz — humidity hurts, less severely (N=689 QSOs):

Abs. HumidityAvg DistP90 Dist
0-5 g/m^3191 km234 km
11-14 g/m^371 km114 km

Finding 2: Wind Penalty Is Overrated

Data shows no meaningful penalty for wind on achieved distance:

Wind10G Avg24G Avg47G Avg
Calm (0-3 kts)21684--
Light (3-7 kts)21410077
Moderate (7-12 kts)22011377

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 common assumption that high pressure = good propagation:

Pressure10G Avg10G P9024G Avg47G Avg
<1010262506119111
1015-10202173839774
1025+196354122--

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 — Retired (no usable signal)

> Status as of 2026-04-25: The HPBL multiplier is removed from > Scorer.score_refractivity/{3,4,5} and the Rust port. See > docs/algo-reports/2026-04-25-algo-revisions.md Recommendation 2.

The original sweet-spot finding ("shallow BL → longer distances") was fitted on n≈680 10 GHz HRRR-matched contacts in April 2026. On the n=47,418 matched corpus the effect disappears: rho_hpbl = +0.004 at 10 GHz, never exceeds |0.092| at any band ≥222 MHz, and the binned distance distribution is flat to within ±5 % across 200–2,000 m HPBL.

HPBL binnavg kmp50 km
< 200 m8,764211.4180.0
200–500 m12,799205.3178.5
500–1,000 m14,739207.8186.5
1,000–1,500 m7,240209.0190.5
1,500–2,000 m2,713199.2176.6
≥ 2,000 m1,160230.3197.4

The previously reported 2.3× distance ratio between shallow and deep HPBL bins was a small-corpus artefact. HPBL stays in the schema and diagnostics so we can revisit if a signal emerges in a different context (e.g. paired with k-factor stratification), but it does not modify the score.

Sounding-mechanism context — kept for documentation, not scoring — shows ducting is supported at both extremes of the diurnal HPBL cycle: shallow nocturnal radiation ducts at 12Z and elevated ducts inside deep residual boundary layers at 00Z. A single HPBL threshold was never going to capture both regimes, which is consistent with the zero overall correlation we now measure on a large corpus.

Finding 5: Binary Duct Detection Is Weak — Use Continuous Gradient (24 GHz only)

Ducting is the majority case in soundings: 2,099 ducting (53.8%) vs 1,800 non-ducting (46.2%). Binary detection has near-zero discriminating power.

DuctingCountAvg NAvg Min GradientAvg BL DepthAvg K-IndexAvg LI
No1,800327.4-123.4986m16.722.8
Yes2,099340.2-388.7738m12.725.3

The continuous gradient (-389 vs -123) is the real signal — a 3x magnitude difference. HRRR data shows 79% of profiles in "Enhanced" regime (gradient -40 to -100), so the scoring must discriminate within the enhanced category, not just between standard and enhanced.

> Tightened 2026-04-25: the gradient signal is only load-bearing at > ~24 GHz. From the n=68,062 HRRR↔contact correlation table: > > | Band | rho_grad | > |---|---:| > | 222 MHz | +0.031 | > | 432 MHz | +0.003 | > | 902 MHz | −0.062 | > | 1.296 GHz | −0.079 | > | 2.304 GHz | −0.178 | > | 5.76 GHz | −0.110 | > | 10 GHz | +0.027 | > | 24 GHz | +0.017 | > | 47 GHz | −0.008 | > | 75 GHz | +0.474 (n=83 — contest-cluster artefact) | > > Below 10 GHz the gradient never clears the 0.05 noise floor; the > dewpoint and PWAT terms already capture whatever moisture-driven > ducting these bands respond to. The 75 GHz row is dominated by Aug–Sep > contest weeks and is not strong evidence for a per-band gradient term. > Per-band gradient weight is therefore set to 0 outside [10 GHz, > 47 GHz]; the refractivity slot in the band-weight matrix carries the > 24 GHz signal alone.

Stability indices and ducting:

  • K-index is lower for ducting (12.7 vs 16.7) — stable atmosphere favors ducting, not convection
  • Lifted Index is higher for ducting (25.3 vs 22.8) — confirms stability correlation
  • Precipitable water is identical (28.0 mm) for both ducting and non-ducting — PWAT is NOT a useful ducting discriminator. It plateaus as a gradient predictor above ~15mm.

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.

Finding 8: Time-of-Day Effect Scales with Frequency (Solar Time)

Night/dawn (22Z-10Z) enhancement vs afternoon baseline, from QSO distance data:

BandAfternoon AvgNight/Dawn AvgEnhancementP90 Boost
10 GHz209.7 km218.6 km+4%+8%
24 GHz93.8 km119.7 km+28%+16%
47 GHz63.5 km86.6 km+36%+42%
75 GHz38.1 km175.4 km+360%+237%

At 10 GHz the effect is modest. At 47+ GHz it is the dominant variable, more important than most weather parameters. The 75 GHz result is from only 20 night/dawn QSOs but the 4.6x multiplier is consistent with strong ducting being the only path at that frequency.

Update (April 2026): Switching from fixed CDT/CST timezone to longitude-based solar time (longitude / 15) dramatically improves the time-of-day correlation at higher frequencies. Spearman correlation with distance: UTC hour rho=0.056 vs solar hour rho=0.188 at 24 GHz (3.4x improvement). At 75 GHz the UTC correlation was confounded by geographic longitude — solar time corrects this from rho=-0.39 to rho=+0.24.

Refresh (2026-04-25, n=82k corpus, hours with ≥30 contacts each): the "+4 / +28 / +36 / +360 %" enhancement table above is the single afternoon-vs-night cut. A robust hour-by-hour amplitude index (max p50 − min p50, divided by band p50) tells a more measured story:

Bandhours w/datalo p50 (km)hi p50 (km)amplitude %
902 MHz1594.5178.057.7
1.296 GHz1780.0176.071.2
2.304 GHz13108.0171.548.2
10 GHz23141.2217.140.4
24 GHz1757.3129.882.0
47 GHz1431.891.3101.1

So 10 → 24 → 47 GHz amplitude does double at each step (40 → 82 → 101 %), but it never reaches the 360 % figure quoted from the small 75 GHz subsample. The frequency-scaling direction in the time-of-day weights stands; the magnitude projection toward 75 GHz+ is unsupported on present data and will be revisited if/when the 75/122 GHz corpus breaks past n=200 with hour coverage.

> Selection-bias warning (added 2026-04-25). Diurnal amplitude > derived from QSO timestamps reflects both propagation diurnal cycles > and operator-scheduling diurnal cycles. At VHF/UHF the second term > dominates: the same robust amplitude table run for 222/432 MHz lands at > 129 % / 148 %, well above any microwave band — physics doesn't predict > that, contest scheduling does (VHF contests run evenings/weekends, > microwave contests run mornings as rovers chase grids). Time-of-day > weights for 222/432 MHz must therefore not be fitted to the QSO > diurnal curve; they keep the global default until a clean atmospheric > signal exists (e.g. continuous beacon monitoring).

Finding 9: Ducting Peaks June-July, Not August

Monthly ducting probability from the 27,058-sounding corpus (2026-04-25 refresh):

MonthSoundingsDucting %Avg dN/dhAvg PWAT (mm)
Jan9528.4 %−17110.9
Feb13129.0 %−1637.9
Mar8117.3 %−1489.4
Apr20025.5 %−16112.7
May31151.8 %−28625.6
Jun38069.5 %−35632.0
Jul30267.5 %−29429.7
Aug5,01355.2 %−25535.1
Sep2,34356.3 %−28027.2
Oct15557.4 %−30719.3
Nov15850.0 %−25712.0
Dec14025.7 %−18510.8

March is still the worst month (17.3 %, up from 10.8 % on the 3.9k corpus) and June–July still the peak (67–70 %). With 6.9× more soundings the shoulder-month numbers stabilised but the seasonal shape is unchanged. Aug/Sep dominate the row counts because the sounding backfill is QSO-driven during contest months; that's a sampling artefact, not a meteorological one.

Finding 10: Mode Matters — CW Advantage Scales with Frequency

Raw statistics show CW averaging 29% longer distances at 10 GHz, but this understates the true advantage due to contest strategy bias. The Great Lakes region generates 3.2x more PH contacts than CW via "firing squad" cross-lake SSB exchanges, inflating PH averages at every band. With cluster activity (EN, CM/DM grids) removed:

BandCW Advantage (corrected)Explanation
10 GHz+35%Ducting, moderate absorption
24 GHz+16%Ducting, high H2O absorption
47 GHz+48%Ducting, window band
75 GHz+221%Every dB counts at high absorption

CW advantage is monotonically increasing with frequency. The raw 24 GHz data shows PH winning (-8%) but this is entirely the Great Lakes firing squad — with manufactured contacts removed, CW leads by 16%.

SSB is not possible on rainscatter. FM is the mode used for rainscatter on 24 GHz.

At 75+ GHz, SSB is only viable for short-range contacts (median 13 km vs CW's 57 km). Above 122 GHz, 100% of contacts in the dataset are CW.

See docs/findings10.md for full regional breakdown and statistical analysis.

Finding 11: Regional Performance Varies but Is Not Algorithm-Correctable

10 GHz QSO distances by Maidenhead field (N≥20):

FieldRegionQSOsAvg kmP90 kmMax km
FMMid-Atlantic (DC/VA/MD)6643216061041
ELFlorida/Gulf Coast322314601609
CMN. California3,9352244111460
ENUpper Midwest (WI/MN/IL)18,7452173431223
FNNortheast (NY/NE)20,9572154181212
DMSoCal/Southwest6,3142033651448
EMSouth-Central (TX/OK)2,2041462721609
CNPacific Northwest11984192468

At 24 GHz, the ranking shifts — dry regions outperform because absorption dominates:

FieldRegionQSOsAvg kmP90 km
DMSoCal/Southwest279157227
CMN. California318129221
ENUpper Midwest1,13598178
FNNortheast1,64490144
EMSouth-Central2514383

Sounding data by region shows ducting frequency is broadly similar (52-62%), suggesting the atmosphere is not the primary driver of the 4x regional spread in QSO distances:

RegionStationsSoundingsDucting %Avg BL Depth
West Coast1763461.7%1043m
Southeast/Gulf291,35755.6%741m
Central371,23553.6%990m
Northeast/Mid-Atl1693052.3%558m

Why regional adjustments are NOT in the scoring model:

  1. Station density and operator skill dominate. The Mid-Atlantic's top ranking correlates with a dense cluster of experienced mountaintop operators, not unique atmospheric physics. Contest results reflect who showed up and where, not just propagation.
  2. Terrain is the confound. PNW (CN) underperforms due to Cascades blocking paths, not worse atmosphere. Terrain profiles handle this separately.
  3. The weather inputs already capture the physics. Coastal inversions (high humidity + refractivity), dry air (low absorption at 24G), and boundary layer depth are all in the scoring factors. If the Mid-Atlantic has better ducting conditions on a given day, it scores higher naturally.
  4. Overfitting risk. The QSO dataset is 97% Aug-Sep contests. Regional weights calibrated to contest patterns would break for non-contest conditions (winter, spring, nighttime).

The correct approach is to let the physics-based factors (humidity, refractivity gradient, BL depth, time of day) produce regional variation organically rather than applying static multipliers.


Part 2b: Data-Driven Refinements

This section documents findings from a systematic correlation analysis matching QSOs to HRRR atmospheric conditions at both endpoints. Each QSO joins to the nearest HRRR grid point (0.125° snap) at both station positions, using the profile valid at the hour of the contact. Spearman rank correlation (rho) measures monotonic association between each atmospheric variable and achieved distance — a nonparametric measure robust to outliers and non-linear relationships.

Correlation Rankings by Band

10 GHz (n=52,846):

Variablerhon_valid
Pressure (mb)-0.18052,846
Month0.10552,846
Dewpoint (°C)-0.05952,846
HPBL (m)0.04552,846
PWAT (mm)-0.03952,846
Refractivity Gradient-0.03452,347
Temperature (°C)0.03152,846
Surface Refractivity-0.02452,347
UTC Hour0.00752,846

24 GHz (n=3,621):

Variablerhon_valid
Dewpoint (°C)-0.3713,621
PWAT (mm)-0.3303,621
Surface Refractivity-0.3173,582
Month0.2723,621
Temperature (°C)-0.1793,621
Pressure (mb)-0.1723,621
Refractivity Gradient-0.0753,582
UTC Hour0.0563,621
HPBL (m)-0.0493,621

47 GHz (n=680):

Variablerhon_valid
Pressure (mb)-0.231680
PWAT (mm)-0.227680
Dewpoint (°C)-0.181680
Refractivity Gradient-0.139678
Month0.111680
Surface Refractivity-0.109678
Temperature (°C)0.037680
UTC Hour-0.024680
HPBL (m)0.004680

75 GHz (n=94):

Variablerhon_valid
Dewpoint (°C)-0.70394
PWAT (mm)-0.60894
Temperature (°C)-0.58994
Pressure (mb)-0.57094
Surface Refractivity-0.52694
UTC Hour-0.39294
HPBL (m)0.15094
Month0.14494
Refractivity Gradient-0.08294

Key Insights

1. Pressure is the #1 correlator at 10 GHz. rho=-0.180. The binned analysis is unambiguous:

Pressure BinnMedian kmp25p75
<1005 mb47,669197.1121.2285.1
1005-1013 mb3,447151.3100.7289.2
1013-1020 mb1,254130.877.9239.2
>1020 mb476103.476.8191.3

Low pressure (<1005 mb) gives 197 km median vs 103 km for >1020 mb — a nearly 2× difference. Low pressure systems bring frontal boundaries, moisture gradients, and boundary-layer structures that create ducting conditions, and the pressure scoring function in Scorer.score_pressure/2 reflects this with its lowest tier (<980 mb) scored highest.

2. Time of day is a weak predictor at 10 GHz. UTC hour correlates at rho=0.007 — barely above zero. The binned data shows modest variation (181-210 km across 3-hour blocks) with no clear diurnal signal at 10 GHz. Time of day matters more at 24+ GHz (rho=0.056 at 24 GHz, -0.392 at 75 GHz), consistent with Finding 8 in Part 2, so the per-band weights in Part 2d scale time-of-day multipliers with frequency (0.6× at 50 MHz → 5× at 122 GHz+).

3. Refractivity gradient signal is modest. Correlation ranges from rho=-0.034 at 10 GHz to -0.139 at 47 GHz. The HRRR pressure-level product's vertical resolution is too coarse to resolve the thin ducting layers that produce the strongest gradients. The binned analysis at 10 GHz shows it: gradient <-300 gives 214 km median vs 192 km for >-100 — only an 11% improvement. hrrrnativeprofiles.bestductband_ghz from the hybrid sigma levels picks up what the pressure-level product misses (see Part 2c).

4. PWAT is a strong independent predictor not captured by any existing factor. Correlations range from rho=-0.039 at 10 GHz to -0.608 at 75 GHz. At 10 GHz, the relationship is non-monotonic with a sweet spot:

PWAT BinnMedian kmp25p75
<10 mm1,834160.696.8265.0
10-20 mm15,041193.8125.1280.7
20-30 mm17,788218.8129.9295.6
30-40 mm13,999173.9106.5289.6
>40 mm4,184155.284.5256.1

At 24 GHz, the relationship is monotonic — lower is always better: <10 mm gives 126 km median vs 45 km for >40 mm. PWAT integrates the full moisture column and captures information beyond surface-level humidity and Td depression.

5. Ducting detection is a non-discriminator at 10 GHz. Ducting YES: n=7,979, median 189 km. Ducting NO: n=44,867, median 192 km. The non-ducting group actually achieves slightly longer median distances. Binary ducting detection from HRRR profiles is useless for scoring — consistent with Finding 5 in Part 2, now confirmed with 10x the sample size using HRRR data rather than soundings.

6. Data is almost entirely Aug/Sep. Of 52,846 QSOs at 10 GHz, 26,813 are August and 26,024 are September. Only 9 QSOs fall outside these months. This limits seasonal conclusions but does not invalidate the atmospheric correlations within those months — pressure, PWAT, and temperature-dewpoint vary substantially within Aug-Sep due to synoptic weather patterns.

Interaction Effects (10 GHz)

The analysis tested whether atmospheric variables interact (i.e., does the effect of one variable depend on the value of another):

Refractivity Gradient x Time of Day: Strong gradients (avg < -100 N/km) improve night/morning distances by 20-30 km but have negligible or negative effect in the afternoon. At night, strong gradient gives 204 km vs 168 km for weak gradient. In the afternoon, the relationship inverts: weak gradient gives 198 km vs 190 km for strong. This suggests afternoon convective mixing disrupts duct structures regardless of gradient strength.

HPBL x Season: In summer, deeper BL correlates with longer distances (shallow 180 km, mid 207 km, deep 231 km). In fall, the relationship flattens (shallow 184 km, mid 194 km, deep 188 km). Summer deep-BL paths may reflect residual elevated ducts from the previous night's inversion within a deep mixed layer.

Binned-distance validation

Binned distance analysis of the full HRRR-matched contact set confirms and refines the correlation findings above.

Shallow BL bonus applied as HPBL multiplier. HPBL binned against contact distance is monotonic: <200 m gives 230 km avg, ≥2000 m gives 100 km. Shallow BL is how surface inversions create steep gradients, so Scorer.score_refractivity/4 folds this in as a multiplier on the gradient score — 1.10× at <200 m down to 0.78× at ≥2000 m.

Pressure tiers. Scorer.score_pressure/2 awards score 88 at <980 mb, capturing the strongest low-pressure signal: contacts at <970 mb average 242.7 km vs 184.1 km at 990-1000 mb (32% longer). The <980 to >1020 gradient is the strongest single-factor predictor in the dataset.

Refractivity gradient flat in bulk range. Gradient bins from -150 to -75 N/km all produce ~212-216 km avg distance. Only the weakest bin (≥ -55 N/km, 176 km) shows meaningful degradation.

Mode distance advantage. CW: 247 km avg, SSB: 191 km avg, FM: 141 km avg at 10 GHz. CW's 29% advantage over SSB is consistent with the ~7 dB bandwidth difference theoretical prediction.

RAOB gradients are 2.5× stronger than HRRR pressure-level. RAOB soundings average -265 N/km (median -200) at contact points vs HRRR's -107 average. RAOB resolves thin surface ducts (50-100 m) that HRRR's pressure-level vertical resolution misses; the hrrrnativeprofiles product closes the gap by running duct analysis on HRRR's 50 hybrid sigma levels instead (Part 2c).

Seasonal tables track RAOB ducting probability. October has the strongest mean gradient of any month (-307 N/km) and 57.4% ducting probability (3rd highest behind Jun 69.5% and Jul 67.5%); the 10 GHz seasonal table scores it at 88. February sits at 29.0% ducting, scored 40.

Commercial link diurnal patterns. 68 GHz (2.82 km LOS) shows 3.9 dB diurnal swing — morning best (-50.7 dBm at 09 UTC), afternoon worst (-54.6 at 13 UTC). 11 GHz (5.66 km) shows the inverted pattern: 1.7 dB swing with more multipath variability at night. 24 GHz (4.36 km) is remarkably stable at 0.9 dB swing. Diurnal sensitivity is non-monotonic with frequency: 24 GHz is more stable than 11 GHz on LOS paths because H₂O absorption is a constant floor rather than a fluctuating variable.


Part 2c: Native-Resolution Duct Analysis

Beyond the pressure-level HRRR product, a second table hrrrnativeprofiles carries native-vertical-resolution duct analysis from HRRR's 50 hybrid sigma levels. This resolves thin trapping layers (50-100 m) that the pressure-level product cannot see.

Historical coverage

Pre-2014 contacts fall back to NARR; 2014-10 onward uses HRRR. Per-band HRRR match counts from the current corpus (post-2014 contacts joined at ±0.07° / ±1h):

BandHRRR-matched contacts
222 MHz5,392
432 MHz6,583
902 MHz1,317
1,296 MHz2,146
2,304 MHz564
3,400 MHz280
5,760 MHz246
10,000 MHz6,675
24,000 MHz613
47,000 MHz53

Bands with ≥200 matched contacts get per-band weight overrides (Part 2d); 47+ GHz and bands with no contacts (50, 144 MHz) inherit the default vector.

Sounding ducting probability

Monthly ducting probability from the sounding corpus (n=9,574 soundings with refractivity gradient data):

MonthSoundingsDucting %Avg dN/dhAvg PWAT (mm)
Jan9528.4%−17110.9
Feb13129.0%−1637.9
Mar8117.3%−1489.4
Apr13431.3%−17613.5
May31151.8%−28625.6
Jun38069.5%−35632.0
Jul30267.5%−29429.7
Aug5,00755.2%−25535.2
Sep2,33556.2%−28027.2
Oct15557.4%−30719.3
Nov15850.0%−25712.0
Dec14025.7%−18510.8

March is the worst month (17.3% ducting) with December close behind (25.7%). June-July peak (67-70%). These probabilities drive the per-band seasonal_base tables in BandConfig.

The continuous-vs-binary signal is sharp: ducting soundings have avg gradient −391 N/km (n=3,672) vs non-ducting −124 N/km (n=3,085) — a 3.1× ratio. K-index is lower for ducting (13.9 vs 16.5) and Lifted Index is higher (24.5 vs 22.4), confirming the stable-atmosphere correlation.

NEW: Native-resolution HRRR duct analysis

hrrrnativeprofiles (11,472 rows) extracts native hybrid sigma levels from HRRR (50 levels vs the 13 pressure levels used elsewhere), then runs Microwaveprop.Propagation.Duct.analyze/1 to find every dM/dh < 0 layer and its supportable frequency.

Best supportable bandProfiles%Avg inversion top (m)Avg θₑ jump (K)Avg Bulk Richardson
<5 GHz1,43212.5%2,0975.418.4
5–15 GHz980.85%4,40825.712.6
15–30 GHz100.087%2,0915.98.7
30–75 GHz40.035%3,8605.012.1
None9,92886.5%11,49780.538.3

Only 13.5% of native-resolution profiles contain a duct at all, and of those, 92.7% support only sub-5 GHz frequencies. Microwave-supporting ducts (15+ GHz) are 14 / 11,472 = 0.12% of the population. This quantifies why microwave tropospheric ducting is so much rarer than the VHF-tropo experience suggests — the sounding-derived "ducting %" includes a lot of weak ducts that don't support 10+ GHz at all. bestductband_ghz is the per-cell upper bound the refractivity scorer consults for the native-duct boost (below).

Bulk Richardson number is systematically lower in duct cells (8.7–18.4) than non-duct cells (38.3), reflecting the dynamic-stability requirement for thin trapping layers. Scorer.scorerefractivity/5 uses this as a gating condition on the native-duct boost: a bestductbandghz reading with a high Richardson number is likely noise (a duct that would get shredded by mechanical mixing), so the 1.15× boost only applies when Richardson is in the stable regime (< 25). A nil Richardson is treated as "no information" rather than "turbulent", so older profiles without the column still receive the unconditional boost.

Per-band signal highlights

The full correlation matrix across every band with ≥200 matched contacts is in Part 2d. Three signals are worth flagging at the physics level:

  • HPBL — retired (no usable signal). The earlier "−0.20 to −0.38" HPBL correlations were small-corpus artefacts; the n=47,418 10 GHz match shows rho_hpbl = +0.004 and the bin tables are flat to within ±5 % across 200–2,000 m. Multiplier removed from the scorer 2026-04-25. See Finding 4 for details.
  • Surface refractivity is consistently positive (+0.08 to +0.18 across VHF/UHF bands). Higher N bends rays further under the same gradient; independent of dN/dh. Contributes to the refractivity factor via the additive surface-N term in score_refractivity/4.
  • Pressure is negative at every band (r −0.04 at 902 MHz up to −0.42 at 47 GHz in magnitude). Low pressure brings the frontal boundaries and moisture gradients that build ducts — Scorer.score_pressure/2 scores <980 mb highest and >1020 mb lowest.

2026-04-25 caveat — bimodality at 10 GHz. The pressure-bin distribution at 10 GHz is U-shaped, not monotonic:

| Pressure bin (mb) | n | avg km | p50 km | |---|---:|---:|---:| | < 990 | 23,536 | 219.4 | 206.0 | | 990–1000 | 12,221 | 182.7 | 159.6 | | 1000–1010 | 6,246 | 204.5 | 170.6 | | 1010–1020 | 3,996 | 214.7 | 173.6 | | ≥ 1020 | 1,419 | 235.7 | 208.9 |

Two physically distinct regimes are stacked into one signal: low pressure → frontal lift / advection ducts; high pressure → subsidence inversion ducts. The current linear scoring captures the < 990 mb effect but under-weights the ≥ 1020 mb ridge. Listed as a next- iteration target — current weights stay linear pending a piecewise rewrite of Scorer.score_pressure/2.

Sub-mm band coverage

The contact corpus contains sub-mm contacts at 142, 145, 241, 288, 322, 403, and 411 GHz. BandConfig has entries for all of them; ITU-R P.676/838 coefficients are interpolated from the 134/241 GHz entries, and ranges are scaled from observed contact distances.

Recalibration tooling

scripts/recalibratealgo.py produces the full correlation matrix + binned distributions as a dated Markdown report under docs/algo-reports/. scripts/deriveband_weights.py converts those correlations into per-band weight override maps ready to paste into BandConfig. Re-run both whenever the contact or HRRR corpus grows materially.

NEXRAD composite reflectivity → rain-attenuation score

HRRR hourly precipitation accumulation lags fast-moving convective cells by up to 59 minutes. NEXRAD n0q composite reflectivity runs at 5-minute cadence and catches those cells at the moment they pass over a grid cell. The scoring pipeline now takes the max of the HRRR-derived rain rate (from precipmm) and the NEXRAD-derived rain rate (from maxreflectivity_dbz via Marshall-Palmer) so either source can trigger the rain penalty without double-counting.

Scorer.dbztorainratemmhr/1 implements:

R (mm/hr) = (Z / 200)^(1/1.6)   where Z = 10^(dBZ/10)

with a 5 dBZ noise floor (ground clutter / clear air) and a 150 mm/hr ceiling (hail contamination). Only active for forecast_hour == 0 — the worker skips NEXRAD merge on f01+ because we have no future radar image.

Native-profile duct boost — retired 2026-04-25

The 1.15× boost on Scorer.score_refractivity/{4,5} for cells where hrrrnativeprofiles.bestductband_ghz ≥ target frequency was removed on 2026-04-25 after the n=56,837 native-profile join falsified its premise. At 10 GHz, contacts where the native duct supports the band ran shorter than no-duct contacts:

Bandn_totalno duct (km)duct supports band (km)duct below band (km)
10 GHz52,341211.3 (n=44 658)197.9 (n=173)199.8 (n=7,510)
24 GHz3,70094.8 (n=3,182)131.5 (n=6)98.3 (n=512)
47 GHz68964.3 (n=552)– (n=0)58.7 (n=137)

The 10 GHz cell with 173 supports-band samples is large enough to say the boost was not just absent — it was opposite the truth in our matched corpus. At 24/47 GHz the supports-band cell is too small to draw any conclusion either way. The Bulk Richardson gate that existed only to suppress this boost is also retired; the function arity stays put so existing call sites compile unchanged. Refractivity scoring now relies on minrefractivitygradient alone.

hrrrnativeprofiles continues to be ingested — the schema, worker, and the per-cell duct-band column are useful diagnostically and may yet earn back a scoring role under a different statistical model. We just don't multiply the refractivity score by it.

Commercial-link inverse sensor

Seven af11x / af60 commercial microwave links around Princeton TX (33.2°N, 96.5°W) are polled every 5 minutes via SNMP and stored in commercialsamples. These are short (2–6 km) LOS paths where the same refractivity anomaly that helps beyond-LOS amateur propagation degrades the link's rxpower through multipath fading.

Commercial.linkdegradationat/3 computes the average of (7-day baseline rxpower − current rxpower) across all healthy (linkstate == 1) links within a configurable radius of a target point (default 75 km). The worker calls this for every grid cell on forecasthour == 0 runs; commercial links only exist near DFW so almost every cell gets nil at near-zero cost.

Scorer.commerciallinkboost/2 then adds a terminal boost to the composite score:

degradation_dbBonusInterpretation
<3 dB0Noise floor
3–8 dB+2 to +10Mild multipath — weak enhancement
≥8 dB+10 to +25 (clamped ≤100)Deep fade — strong ducting signature

This is the first measured signal in the algorithm — every other factor is a model-derived proxy. It only helps a ~150 km radius around DFW, but in that zone it's the strongest single indicator we have of actual refractivity anomalies happening right now. Out-of-zone cells see no change.

2026-04-25 calibration update. commercial_samples now has 38,443 rows over 22 days (2026-03-30 → 2026-04-20) across the seven links. Per-link rxpower0 standard deviation is 1.0–1.7 dB on the af11x links and 2.5–2.9 dB on the af60 links — the 60 GHz pair is intermittent (only 150–200 up-samples each in the window) which is itself the strongest signal for "60 GHz weather sensitivity" the project has ever recorded.

QSO overlap with the sample window was zero (DFW microwave activity is contest-driven, Aug/Sep), so direct contact-distance vs fade correlation is still pending. But against the 37-hour HRRR overlap that does exist in the window, hourly mean rx-degradation correlates with HRRR fields at magnitudes 4–5× anything the QSO-vs-HRRR table produces at 24 GHz:

corr(fade_db, …)value
minrefractivitygradient−0.161
surfacedewpointc+0.487
pwat_mm+0.609
t-td depression+0.147
surfacepressuremb−0.605
HPBL+0.033

Direction matches the QSO-derived 24 GHz weights exactly: wet column → more fade, low pressure → more fade. The sensor is wired and producing usable signal; QSO co-occurrence is the missing piece, not the sensor design.

Morning fade window. Binning the 27,820 af11x up-samples by local hour, the rate of fades ≥ 3 dB below per-link mean is two-peaked:

Local hournn_fade≥3dBpct
031,13970.61
041,135161.41
051,135181.59
061,13570.62
091,130131.15
101,134252.20
111,140161.40
121,117191.70
13–22flat3–80.18–0.65
00–02quiet0–20.00–0.18

Two physical mechanisms: 04–05 local is the breakdown of overnight radiation-fog / nocturnal-inversion ducting; 10–12 local is the onset of boundary-layer mixing. This is the qualitative "morning fade window" 11 GHz operators have always experienced — quantified for the first time. Suggests the diurnal time-of-day curve at 10–24 GHz should be biased toward morning hours rather than the symmetric "night peak" that sub-mm-band physics alone would suggest.


Part 2d: Full-Corpus Per-Band Recalibration

Per-band composite weights are derived from a full-corpus correlation run (scripts/recalibratealgo.py + scripts/deriveband_weights.py). The latest dated report lives in docs/algo-reports/.

Matched corpus sizes

Each contact is joined to its nearest HRRR grid point at ±0.07° / ±1h (±0.25° / ±2h for the pre-2014 NARR fallback). DISTINCT ON (c.id) keeps a single nearest match per contact. Pre-2014 coverage is intentionally thin: of 207 pre-2014 contacts with pos1, 103 matched a NARR profile — the NARR backfill has only fired on the contacts the enqueuer has seen.

BandContacts in DBHRRR-matchedNARR-matched
222 MHz7,5955,3920
432 MHz9,1776,5830
902 MHz1,7941,3170
1,296 MHz2,9962,1460
2,304 MHz7495640
3,400 MHz3512800
5,760 MHz3282460
10 GHz54,2646,675103
24 GHz3,8066130
47 GHz762530

The per-band HRRR-match rate is 12–75%; the rest are contacts whose timestamps predate the HRRR archive start (2014-10) or whose locations don't have a nearest grid point written. The NARR join only lights up at 10 GHz because pre-2014 QSOs happen to concentrate there in the corpus.

Per-band Pearson correlations vs contact distance

Joining each contact to its nearest HRRR profile (±0.07° / ±1h) and computing Pearson r between distance_km and the HRRR field at the origin station:

Band (n)TempDewpointPressurePWATSurface NdN/dhHPBL
222 (5,392)−0.086+0.063−0.038+0.077+0.077+0.057−0.033
432 (6,583)−0.089+0.137+0.058+0.124+0.136+0.041−0.066
902 (1,317)−0.060+0.096+0.044+0.049+0.079−0.014−0.010
1,296 (2,146)−0.032+0.085+0.013+0.049+0.085−0.031−0.041
2,304 (564)−0.114+0.130−0.006+0.086+0.154−0.110−0.074
3,400 (280)−0.103+0.150−0.025+0.131+0.176+0.046−0.023
5,760 (246)−0.082+0.087+0.040+0.137+0.105+0.021−0.083
10,000 (6,675)+0.043−0.028−0.066+0.017+0.031−0.024−0.025
24,000 (613)−0.007−0.208−0.178−0.172−0.103−0.251−0.201
47,000 (53)+0.222−0.518−0.423−0.149+0.510−0.154−0.465

Signs tell the story. From 222 MHz through 5.76 GHz moisture (dewpoint, PWAT, surface refractivity) is consistently positive — higher column water → longer contacts, because water-vapor pressure lifts surface-layer refractivity without triggering the absorption penalty that kicks in above ~15 GHz. At 10 GHz the signs flip or collapse into noise. At 24 GHz all seven fields go negative and the magnitudes roughly triple: the same synoptic ridge that bends 222 MHz rays 250 km is also the ridge that loads 24 GHz air with 30+ mm of H₂O absorption. This is the algorithm's humidity-direction reversal made visible in the data.

47 GHz (n=53) is printed for interest only; the correlation magnitudes are eye-watering because the sample is tiny and Aug–Sep contest clusters. We do not use the 47 GHz row to fit weights.

Per-band weight derivation

Rule (encoded in scripts/derivebandweights.py):

  1. Start from the global default weight vector (BandConfig.weights/0, the 2026-04-11 gradient-descent fit).
  2. For each of the five correlation-backed factors (humidity↔dewpoint, td_depression↔temp, refractivity↔dN/dh, pressure↔pressure, pwat↔PWAT) compute a multiplier:

``` sband,factor = (|rband| + 0.05) / (|r_10GHz| + 0.05) ```

then clamp to [0.5, 2.0]. The 0.05 floor is the noise level at which we can reliably distinguish a correlation from zero given typical n, and the clamp keeps a single noisy correlation from moving a weight more than 2× the prior.

  1. For the five physics-only factors (rain, season, time-of-day, sky, wind) use predetermined per-band multipliers:

- Rain scales as √(raink / 10 GHz raink). Linear scaling (rain_k ratio 7× at 24 GHz) would swamp everything; sqrt keeps the weight jump proportionate to the fraction of conditions where rain actually drives the outcome. - Season scales up at VHF/UHF (Es-like physics amplifies monthly variation that we don't model directly) and at mm-wave (summer humidity hurts). - Time-of-day scales with frequency per Finding 8: 0.6× at 50 MHz, 1.0× at 10 GHz, 5.0× at 122 GHz+. - Sky, wind held flat — no per-band evidence.

  1. Multiply default weights by the multipliers, normalize to sum to 1.0, round to 4 decimals.

Bands with fewer than 200 matched contacts (47+ GHz) and bands with zero contacts (50, 144 MHz) inherit the default vector via BandConfig.weights/1 fallback.

Resulting per-band weight matrix

Bandhumidtodtdrefrskyseasonwindrainpwatpress
default0.12430.04960.09780.10490.08000.11120.08000.13620.11280.1032
222 MHz0.15930.03500.12500.14010.07060.12760.07060.01200.19160.0681
432 MHz0.20610.03290.12000.11860.06630.11060.06630.01130.18700.0809
902 MHz0.22010.04370.11020.08880.07830.11970.07830.01330.16350.0839
1,296 MHz0.21310.04940.08980.13920.07970.11080.07970.01360.16780.0568
2,304 MHz0.19410.03870.13850.16380.06250.08680.06250.03360.17620.0432
3,400 MHz0.19960.03980.12510.13100.06420.08930.06420.04890.18110.0568
5,760 MHz0.18290.04230.12050.08810.06830.09490.06830.08220.19250.0602
10 GHzdefaults (reference band)
24 GHz0.14810.05320.03600.12500.04770.07290.04770.21470.13440.1203

Cells in bold are the factors that moved meaningfully from default — roughly, what the data says is different at this band. The overall pattern:

  • VHF/UHF (222–1296 MHz): PWAT and humidity up, rain and pressure down. These bands need moisture to duct; rain doesn't attenuate them; surface pressure is a weak synoptic proxy that cleaner moisture fields already capture.
  • Low microwave (2,304–5,760 MHz): Moisture still dominant; refractivity gradient picks up weight as HRRR's vertical resolution starts to matter; rain gradually climbs.
  • 10 GHz: Reference band, weights unchanged.
  • 24 GHz: Rain doubles-plus (0.215), td_depression collapses to 0.036 because the temperature signal alone is essentially zero (humidity via dewpoint already carries the moisture story). Refractivity holds because dN/dh shows the strongest correlation of any band at 24 GHz (−0.25).

Soundings refresh (n=26,933 total, 9,574 with refractivity gradient)

Monthly ducting probability from the expanded sounding corpus. Compared to the Part 2c snapshot (n=6,757), August's sample nearly doubled (2,572 → 5,007) as soundings backfill caught up with the QSO calendar. The headline findings hold: March is still the worst month (17.3% ducting), June–July the peak (67–70%).

MonthSoundingsDucting %Avg dN/dhAvg PWAT (mm)
Jan9528.4%−17110.9
Feb13129.0%−1637.9
Mar8117.3%−1489.4
Apr13431.3%−17613.5
May31151.8%−28625.6
Jun38069.5%−35632.0
Jul30267.5%−29429.7
Aug5,00755.2%−25535.2
Sep2,33556.2%−28027.2
Oct15557.4%−30719.3
Nov15850.0%−25712.0
Dec14025.7%−18510.8

What stayed, what left

  • Humidity direction flip at ~15 GHz — preserved. Data shows it clearly (signs flip going from 5.76 GHz to 10 GHz and again from 10 GHz to 24 GHz).
  • Pressure is the strongest 10 GHz correlator — preserved, but magnitude is now −0.066 (vs −0.180 reported in Part 2b); the newer matched corpus has cleaner spatial joins and less contest-seasonality bias. The bin distribution at 10 GHz is U-shaped — see Part 2c "Pressure" for the bimodality and the next-iteration target.
  • HPBL boundary-layer multiplier — retired 2026-04-25. rhohpbl ≈ 0 at every band ≥ 222 MHz on the n=68k matched corpus; the previously reported "shallow-BL → longer-distance" effect was a small-corpus artefact. Multiplier removed from Scorer.scorerefractivity/{3,4,5} and the Rust port; the column stays in the schema and diagnostics.
  • Native-profile 1.15× duct boost — retired 2026-04-25. At 10 GHz on n=52,341 matched contacts, cells where the native duct supports the band ran shorter than no-duct cells (198 vs 211 km, n=173 vs 44,658). Removed from both the Elixir scorer and the Rust port; arity preserved so callers don't need a coordinated rewrite. hrrrnativeprofiles continues to be ingested for diagnostics.
  • Per-band recalibration is statistically defensible — the Part 2c moratorium lifts. 9 bands have n ≥ 200 matched contacts.
  • Commercial-link inverse sensor — promoted from deferred. 22-day, 38k-sample SNMP corpus now exists. 37-hour HRRR overlap shows rho(fade, PWAT) = +0.61, rho(fade, P) = −0.61. Sensor is detecting the right physics; QSO co-occurrence is the missing piece, not the sensor design. See Part 2c "Commercial-link inverse sensor."
  • NARR historical calibration — still deferred. 103 pre-2014 matches at 10 GHz isn't enough to validate the weights against the earlier atmosphere; the NARR backfill is still at 358 rows. Re-run when narr_profiles crosses ~10K rows.

Open items

  1. 50 MHz and 144 MHz are unmodeled (0 contacts in corpus). Both bands carry default weights but ranges and seasonal tables are pure physics priors. Any VHF calibration needs an import of 6 m and 2 m contacts from external logs before we can say anything.
  2. 47+ GHz inherit defaults. 47 GHz has n=53, below the 200-contact floor; 75 GHz has n=106 in the DB but only a handful HRRR-match. Re-fit when contest-season 47/75 GHz logs accumulate.
  3. Refractivity gradient signal is load-bearing only at 24 GHz. At every other band the gradient correlation rides the noise floor. Per-band gradient weight set to 0 outside [10 GHz, 47 GHz] (Recommendation 7 in docs/algo-reports/2026-04-25-algo-revisions.md). This will be reflected in the band-weight matrix on the next derivebandweights.py run.
  4. Pressure scoring is linear, distance is U-shaped at 10 GHz. The < 990 mb tail and the ≥ 1020 mb tail both run longer than the 990–1010 mb middle. Scorer.score_pressure/2 should switch from linear to a piecewise score with a peak near the 990–1010 mb minimum and rising to both tails.
  5. VHF/UHF time-of-day weights are contest-schedule-contaminated. Running the diurnal-amplitude fit on 222/432 MHz lands at 129 % / 148 %, which physics doesn't predict; that's evening-contest scheduling, not the atmosphere. VHF/UHF time-of-day weights stay at the global default until a clean atmospheric signal exists.

Part 3: Band Configuration

@band_configs %{
  902 => %{
    label: "902 MHz",
    o2_db_km: 0.006,
    h2o_coeff: 0.0,
    humidity_effect: :beneficial,  # Sub-GHz: refractivity dominates, absorption negligible
    humidity_penalty: 0.0,
    rain_k: 0.000, rain_alpha: 1.0,  # Rain attenuation negligible at 900 MHz
    seasonal_base: %{1 => 38, 2 => 40, 3 => 22, 4 => 55, 5 => 68,
                     6 => 90, 7 => 95, 8 => 75, 9 => 78, 10 => 88,
                     11 => 78, 12 => 25},
    seasonal_adj: %{},
    typical_range_km: 400,
    extended_range_km: 800,
    exceptional_range_km: 1500
  },
  1_296 => %{
    label: "1296 MHz",
    o2_db_km: 0.006,
    h2o_coeff: 0.0,
    humidity_effect: :beneficial,
    humidity_penalty: 0.0,
    rain_k: 0.000, rain_alpha: 1.0,
    seasonal_base: %{1 => 38, 2 => 40, 3 => 22, 4 => 55, 5 => 68,
                     6 => 90, 7 => 95, 8 => 75, 9 => 78, 10 => 88,
                     11 => 78, 12 => 25},
    seasonal_adj: %{},
    typical_range_km: 350,
    extended_range_km: 700,
    exceptional_range_km: 1200
  },
  2_304 => %{
    label: "2304 MHz",
    o2_db_km: 0.006,
    h2o_coeff: 0.0,
    humidity_effect: :beneficial,
    humidity_penalty: 0.0,
    rain_k: 0.001, rain_alpha: 1.15,  # Onset of rain sensitivity
    seasonal_base: %{1 => 38, 2 => 40, 3 => 22, 4 => 55, 5 => 68,
                     6 => 90, 7 => 95, 8 => 75, 9 => 78, 10 => 88,
                     11 => 78, 12 => 25},
    seasonal_adj: %{},
    typical_range_km: 300,
    extended_range_km: 600,
    exceptional_range_km: 1000
  },
  3_456 => %{
    label: "3456 MHz",
    o2_db_km: 0.006,
    h2o_coeff: 0.0,
    humidity_effect: :beneficial,
    humidity_penalty: 0.0,
    rain_k: 0.002, rain_alpha: 1.20,
    seasonal_base: %{1 => 38, 2 => 40, 3 => 22, 4 => 55, 5 => 68,
                     6 => 90, 7 => 95, 8 => 75, 9 => 78, 10 => 88,
                     11 => 78, 12 => 25},
    seasonal_adj: %{},
    typical_range_km: 250,
    extended_range_km: 550,
    exceptional_range_km: 900
  },
  5_760 => %{
    label: "5760 MHz",
    o2_db_km: 0.007,
    h2o_coeff: 0.0,
    humidity_effect: :beneficial,
    humidity_penalty: 0.0,
    rain_k: 0.005, rain_alpha: 1.25,
    seasonal_base: %{1 => 38, 2 => 40, 3 => 22, 4 => 55, 5 => 68,
                     6 => 90, 7 => 95, 8 => 75, 9 => 78, 10 => 88,
                     11 => 78, 12 => 25},
    seasonal_adj: %{},
    typical_range_km: 220,
    extended_range_km: 500,
    exceptional_range_km: 1000
  },
  10_000 => %{
    label: "10 GHz",
    o2_db_km: 0.007,
    h2o_coeff: 0.0,
    humidity_effect: :beneficial,  # More moisture = more refractivity = longer paths
    humidity_penalty: 0.0,
    rain_k: 0.010, rain_alpha: 1.28,
    # Seasonal pattern INVERTED from 24+ GHz: ducting + humidity both help 10G
    # Ducting peaks Jun-Jul (69-77%), March worst (10.8%), Dec-Feb ~12-22%
    # Score tracks ducting probability scaled to 0-100
    seasonal_base: %{1 => 38, 2 => 40, 3 => 22, 4 => 55, 5 => 68,
                     6 => 90, 7 => 95, 8 => 75, 9 => 78, 10 => 88,
                     11 => 78, 12 => 25},
    seasonal_adj: %{},
    typical_range_km: 200,
    extended_range_km: 500,
    exceptional_range_km: 1000
  },
  24_000 => %{
    label: "24 GHz",
    o2_db_km: 0.02,
    h2o_coeff: 0.002,           # 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,
    # 24G: humidity hurts, so best in dry months. But March is worst for ducting.
    # Balance: winter dry + some ducting > spring dry + no ducting
    seasonal_base: %{1 => 88, 2 => 84, 3 => 68, 4 => 62, 5 => 51,
                     6 => 34, 7 => 18, 8 => 18, 9 => 48, 10 => 68,
                     11 => 96, 12 => 88},
    seasonal_adj: %{5 => -4, 6 => -8, 7 => -10, 8 => -10, 9 => -4},
    typical_range_km: 100,
    extended_range_km: 250,
    exceptional_range_km: 500
  },
  47_000 => %{
    label: "47 GHz",
    o2_db_km: 0.04,
    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).

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 — Solar Time, Inversion Lifecycle

Uses longitude-based solar time (longitude / 15 offset) instead of a fixed timezone offset. This produces physically correct local time at every grid point across CONUS and dramatically improves correlation with QSO distance:

BandUTC Hour rhoSolar Hour rhoImprovement
10 GHz0.0070.0162.4x (still weak — 10G ducts form at all times)
24 GHz0.0560.1883.4x (now #5 predictor)
47 GHz-0.0240.152Sign corrected (UTC was confounded by longitude)
75 GHz-0.3920.239Sign corrected (western US at lower UTC ≠ better propagation)

The UTC hour correlation at 75 GHz was spuriously negative because western US stations (lower UTC hours) happened to have longer paths — a geographic confound, not physics. Solar time corrects this.

At 24 GHz, the solar hour bins show a clear physical pattern: 03-05 solar (pre-dawn) has worst distances (57 km median), evening/night (18-23 solar) has best (107-140 km median) — consistent with nocturnal inversion formation.

@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, longitude) do
  offset = longitude / 15  # solar time offset from longitude
  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).

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. Supports both METAR categories (from ASOS) and percentage sky cover (from IEMRE gridded data).

def score_sky(condition) when is_binary(condition) do
  # METAR category from ASOS
  case condition do
    c when c in ["CLR", "SKC"] -> 100
    "FEW" -> 88
    "SCT" -> 60
    "BKN" -> 25
    "OVC" -> 5
    "VV"  -> 5
    _     -> 50
  end
end

def score_sky(pct) when is_number(pct) do
  # Percentage sky cover from IEMRE (0-100%)
  cond do
    pct <= 6   -> 100  # CLR equivalent
    pct <= 25  -> 88   # FEW
    pct <= 50  -> 60   # SCT
    pct <= 87  -> 25   # BKN
    true       -> 5    # OVC
  end
end

5. Season Score

Per-band lookup with optional adjustments. 24 GHz gets additional summer penalties due to Gulf moisture.

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).

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.

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 — Low Pressure Favors Beyond-LOS

Data from 57,248 QSO-HRRR matches shows pressure is the #1 correlator at 10 GHz (rho=-0.180). The relationship is monotonic and strong: <1005 mb gives 197 km median vs 103 km for >1020 mb. Low pressure systems bring frontal boundaries, moisture gradients, and boundary-layer structures that create ducting conditions. The previous function scored high pressure higher — completely backwards for beyond-LOS propagation.

When trend data is available, falling pressure (approaching front) scores highest because pre-frontal dynamics create the strongest refractive gradients.

def score_pressure(current_mb, previous_mb) do
  case previous_mb do
    nil ->
      # No trend — score on absolute value, low pressure = better
      cond do
        current_mb < 980  -> 88   # Deep low — strong frontal dynamics
        current_mb < 990  -> 82   # Low — frontal activity, boundary ducts
        current_mb < 1000 -> 70   # Moderate low
        current_mb < 1010 -> 55   # Normal
        current_mb < 1020 -> 40   # Mild high — stable, less ducting
        true              -> 30   # Strong ridge — inversions cap at wrong altitude
      end

    prev ->
      delta = current_mb - prev
      cond do
        delta > 2.5  -> 80   # Rising rapidly — post-frontal clearing, residual ducts
        delta > 0.8  -> 70   # Rising — stabilizing
        delta > -0.5 -> 60   # Steady — neutral
        delta > -2.0 -> 65   # Falling slowly — approaching front
        true         -> 45   # Falling rapidly — active frontal dynamics, mixing
      end
  end
end

9. Refractivity Score — When Sounding/HRRR Data Available

Best predictor when available. Binary duct detection is useless (54% baseline rate). Use continuous gradient magnitude and BL depth instead.

Thresholds calibrated for HRRR-derived gradients which are coarser than radiosonde soundings. HRRR gradient distribution: p1=-230, p5=-162, p10=-130, p25=-94, p50=-70, p75=-53, p95=-40 N/km. Previous thresholds (-500 to -60) placed nearly all HRRR profiles in the default bucket.

Gradient (N/km)Beneficial ScoreHarmful ScoreCondition
< -2009885Strong ducting (HRRR p1)
< -1509280Enhanced refraction (HRRR p5)
< -1008272Above-average refraction (HRRR p25)
< -756862Near-median gradient (HRRR p50)
< -555555Below-median (HRRR p75)
< -404848Weak gradient (HRRR p95)
≥ -404242Standard/sub-refractive

Shallow BL fallback: when gradient is unavailable but BL depth < 300m, score 82 (strong inversion cap).

10. PWAT Score — Precipitable Water (NEW)

PWAT (precipitable water, total column integrated moisture in mm) is a strong independent predictor. Correlation with distance ranges from rho=-0.039 at 10 GHz to -0.608 at 75 GHz. Unlike surface humidity and Td depression which measure conditions at ground level, PWAT integrates the full moisture column and captures elevated moisture layers relevant to duct formation and path absorption.

At 10 GHz (beneficial humidity), the relationship is non-monotonic: 20-30 mm PWAT gives the best median distances (219 km), with both very dry (<10 mm, 161 km) and very wet (>40 mm, 155 km) conditions performing worse. Moderate PWAT indicates sufficient moisture for refractivity enhancement without the atmospheric instability that accompanies very high moisture content.

At 24+ GHz (harmful humidity), lower PWAT is universally better. At 24 GHz: <10 mm gives 126 km median, >40 mm gives 45 km — a 2.8x difference.

def score_pwat(pwat_mm, band_config) do
  case band_config.humidity_effect do
    :beneficial ->
      # 10 GHz: sweet spot at moderate PWAT (20-30 mm)
      cond do
        pwat_mm < 10 -> 55   # Very dry — poor refractivity
        pwat_mm < 20 -> 75   # Moderate dry
        pwat_mm < 30 -> 90   # Optimal — best median distances
        pwat_mm < 40 -> 70   # High — beginning absorption penalty
        true         -> 50   # Very high — absorption dominates
      end

    :harmful ->
      # 24+ GHz: lower is better, scales by frequency via humidity_penalty
      cond do
        pwat_mm < 10 -> 95   # Very dry — minimal absorption
        pwat_mm < 20 -> 80   # Low — good conditions
        pwat_mm < 30 -> 60   # Moderate — noticeable absorption
        pwat_mm < 40 -> 35   # High — significant absorption
        true         -> 15   # Very high — severe absorption
      end
  end
end

Upper-Air Factors (Pending Native-Profile Backfill)

The 10 factors above are all surface or column-integrated quantities. None of them see the mid-to-upper troposphere, because the legacy HRRR ingestion capped at 700 mb (~3 km). With the native hybrid-sigma profile (Part 12) now storing all 50 levels up to ~19 km, the scorer can consume synoptic-scale signals that discriminate ridge-vs-trough regimes — the single strongest predictor of tropo propagation at microwave frequencies.

Status: feature plumbing is in place (hrrrnativeprofiles schema already stores the full-atmosphere arrays). Calibration is blocked on the backfill finishing — the top-N hours by contact count must be ingested before gradient descent can assign weights. Once backfill completes, re-run the calibration pipeline (scripts/recalibrate_algo.py) with these features included.

The five proposed factors below are derived from the native profile at the QSO's path midpoint, not the endpoints, since the synoptic pattern is spatially smooth over a 300 km path.

1. 500 mb dewpoint depression — mid-level dryness

Dry air at 500 mb above moist lower levels is the textbook signature of synoptic subsidence: a ridge aloft pushes dry stratospheric air down, warming the mid-troposphere and capping the boundary layer. High depression (> 30 °C) correlates with anticyclonic regimes and subsidence-driven trapping; small depression (< 5 °C) indicates deep convective moisture, mixing, and poor tropo.

Derived from native profile: interpolate T and Td to 500 mb, return T500c - Td500c. Td comes from SPFH via the Magnus inverse (same path as HrrrNativeProfile.toskewt_profile/1).

2. 300 mb wind speed — jet-level flow

The 300 mb wind is the standard proxy for jet-stream position and intensity. Strong jet (> 50 m/s) means active storm track, frontal passage, vertical wind shear, and convective mixing — all bad for tropo. Weak jet (< 15 m/s) indicates zonal/blocked flow aloft, which is a necessary (not sufficient) condition for stable ducting patterns to persist beyond a single diurnal cycle.

Derived from native profile: interpolate sqrt(u² + v²) to 300 mb.

3. 850→500 mb potential-temperature gradient — deep subsidence metric

The mid-troposphere lapse rate, converted to potential temperature so it's mixing-invariant:

dθ/dz = (θ_500 - θ_850) / (z_500 - z_850)

where θ = T * (1000/p)^0.2854. A strongly positive gradient (> 4 K/km) means the column is stably stratified through a deep layer — subsidence is warming the mid-troposphere faster than the surface cools, creating the deep capping inversion that supports elevated ducts and keeps the boundary-layer moisture trapped. Near-zero or negative means the column is mixing through its full depth (cumulus convection, post-frontal), which destroys tropo.

This factor is expected to correlate more strongly with long-path 10 GHz distances than any existing factor except refractivity gradient itself, because it captures the synoptic reason the gradient is there.

4. Tropopause height — airmass proxy

A high tropopause (> 13 km) means warm, deep troposphere — subtropical airmass under a ridge, the classic beyond-LOS regime. A low tropopause (< 10 km) means cold polar airmass, active frontal zone, dynamic weather. This is a slowly varying but very clean indicator of the regime at the timescale of a contact.

Derived from native profile: walk levels upward from the surface, find the first level where dT/dz transitions from negative (troposphere) to ≥ -2 K/km sustained for > 2 km (WMO definition). Height of that transition is the tropopause.

5. 500 mb geopotential-height anomaly — synoptic regime indicator

The single best synoptic-scale discriminator between ridging (beneficial) and troughing (harmful). Requires a climatology baseline — monthly/daily 500 mb height normals per grid point. Ridge anomaly (> +60 m above climo) is beyond-LOS-favorable; trough (< -60 m) is harmful.

Dependency: 500 mb climatology must be computed from the native backfill as a by-product (or pulled from NARR for the pre-2014 window). This is the one upper-air factor that needs infrastructure beyond the native profile schema.

Weight placeholders

| Factor                              | Weight | Source                                                         |
| 500 mb dewpoint depression          |  TBD   | Native HRRR profile, interpolated to 500 mb                    |
| 300 mb wind speed                   |  TBD   | Native HRRR profile, interpolated to 300 mb                    |
| 850-500 mb dθ/dz (subsidence)       |  TBD   | Native HRRR profile, potential-temp gradient                   |
| Tropopause height                   |  TBD   | Native HRRR profile, WMO lapse-rate definition                 |
| 500 mb height anomaly               |  TBD   | Native HRRR or NARR, climatology-baseline (dependency)         |

Calibration plan once backfill is complete:

  1. Extract the five features for every QSO with a matching native profile (~20-40k expected coverage).
  2. Compute per-band correlations with both distancekm and compositescore residuals.
  3. Feed features into the existing gradient-descent recalibration alongside the current 10 factors; current weights are re-fit simultaneously to avoid overclaiming the upper-air contribution.
  4. Redistribute weight out of whichever current factors are partially redundant with upper-air signals (early suspects: pressure, season, refractivity — all indirect proxies for the same synoptic regime).

Part 5: Composite Score

Weights — per-band

The scoring weight vector is now band-specific. BandConfig.weights(band_config) returns either the override map stored on the band (for the nine bands with ≥200 matched HRRR samples in the 2026-04-18 full-corpus analysis) or the default vector shown below for everything else.

Default vector (the April-11 gradient-descent fit, applied at 10 GHz as the reference band and to any band without enough data to fit its own vector):

FactorWeightSource
Rain13.6%ITU-R P.838-3 specific attenuation
Humidity12.4%Absolute humidity from surface T/Td
PWAT11.3%HRRR precipitable water (column-integrated)
Season11.1%Per-band monthly lookup tables
Refractivity10.5%Native HRRR dM/dh (10–50m), fallback to pressure-level (250m)
Pressure10.3%Surface pressure (frontal activity proxy)
Td Depression9.8%Surface T minus Td (stability indicator)
Sky Cover8.0%HRRR total cloud cover
Wind8.0%HRRR 10m wind speed
Time of Day5.0%Solar-time adjusted diurnal cycle

Per-band overrides (see Part 2d for the derivation rule and full matrix):

Bandhumiditytodtdrefrskyseasonwindrainpwatpress
222 MHz0.15930.03500.12500.14010.07060.12760.07060.01200.19160.0681
432 MHz0.20610.03290.12000.11860.06630.11060.06630.01130.18700.0809
902 MHz0.22010.04370.11020.08880.07830.11970.07830.01330.16350.0839
1.296 GHz0.21310.04940.08980.13920.07970.11080.07970.01360.16780.0568
2.304 GHz0.19410.03870.13850.16380.06250.08680.06250.03360.17620.0432
3.4 GHz0.19960.03980.12510.13100.06420.08930.06420.04890.18110.0568
5.76 GHz0.18290.04230.12050.08810.06830.09490.06830.08220.19250.0602
10 GHzdefaults (reference)
24 GHz0.14810.05320.03600.12500.04770.07290.04770.21470.13440.1203

Bands inheriting defaults: 50, 144 (0 contacts), 47+ GHz (<200 matched contacts). The Scorer composite is now:

def composite_score(conditions, band_config) do
  factors = compute_factors(conditions, band_config)
  weights = BandConfig.weights(band_config)

  round(
    factors.rain          * weights.rain +
    factors.humidity      * weights.humidity +
    factors.pwat          * weights.pwat +
    factors.season        * weights.season +
    factors.refractivity  * weights.refractivity +
    factors.pressure      * weights.pressure +
    factors.td_depression * weights.td_depression +
    factors.sky           * weights.sky +
    factors.wind          * weights.wind +
    factors.time_of_day   * weights.time_of_day
  )
end

All weight vectors sum to 1.0 within round-off (verified by BandConfigTest). Re-run scripts/recalibratealgo.py + scripts/derivebandweights.py whenever the contact or HRRR corpus grows materially — the defaults in this section are a snapshot of the 2026-04-18 local propdev state.

Score Tiers with Per-Band Range Estimates

Range estimates are for CW mode. For SSB/phone, reduce by ~25% at 10 GHz, ~15% at 24 GHz, ~50% at 47 GHz, ~70% at 75+ GHz. For FM, reduce by ~40%.

Database stats for reference: 10G avg=213 km, P90=383 km, max=2,393 km. 24G avg=98 km, P90=179 km, max=710 km. 47G avg=66 km, P90=122 km, max=343 km. 75G avg=64 km, P90=177 km, max=289 km.

ScoreLabel10G24G47G75G
80-100EXCELLENT400-2000+ km200-500 km120-300 km80-200+ km
65-79GOOD250-400 km120-200 km80-120 km50-80 km
50-64MARGINAL150-250 km70-120 km50-80 km30-50 km
33-49POOR80-150 km40-70 km25-50 km15-30 km
0-32NEGLIGIBLE<80 km<40 km<25 km<15 km
ColorHex
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

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.

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

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, conditions.longitude) |> 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),
    pwat: score_pwat(conditions.pwat_mm, 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:

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-16 Eq. 31)

Single clean formula replacing the previous piecewise approximation:

J(ν) = 6.9 + 20·log10(√((ν−0.1)² + 1) + ν − 0.1)    for ν > −0.78
J(ν) = 0                                                 for ν ≤ −0.78

Diffraction parameter ν (P.526-16):

ν = h · √(2·(d1+d2) / (λ·d1·d2))

where h is the obstacle height above the direct ray (positive = blocked, negative = clear). At grazing (ν = 0), loss is ~6 dB. At 0.6× Fresnel clearance (ν ≈ −0.85), loss is negligible.

Deygout Multi-Edge Method (ITU-R P.526-16 Section 6)

For paths with multiple terrain obstacles, the Deygout 3-edge method is used instead of single-worst-obstacle:

  1. Find the principal edge — the point with the highest ν on the full T→R path
  2. Find subsidiary edge on the T→principal sub-path (highest ν)
  3. Find subsidiary edge on the principal→R sub-path (highest ν)
  4. Total diffraction loss = J(νmain) + J(νsub1) + J(ν_sub2)

This produces higher (more realistic) diffraction estimates for paths crossing multiple ridgelines. The frequency dependence is significant: the same physical obstacle produces ~10 dB at 10 GHz but ~27 dB at 241 GHz.

Success Probability

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 margintosuccess 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:

HorizonObserved AccuracyConfidence
Current+/- 1 dB95%
+30 min+/- 1.5 dB90%
+1 hr+/- 2 dB85%
+2 hr+/- 3 dB75%
+3 hr+/- 4 dB60%
+6 hr+/- 5 dB40%

Diurnal Temperature Model

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

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, longitude) |> 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),
      pwat: score_pwat(forecast_value(forecast, :pwat_mm, hours_ahead) || 50, 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

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.

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.

902 MHz (33cm) — UHF Troposcatter/Ducting Band

Primary mechanisms: Tropospheric ducting, troposcatter, enhanced refraction Key variable: Refractivity profile; gaseous absorption negligible (O₂ 0.006 dB/km, H₂O 0.0) Best conditions: Same as 10 GHz — moderate-high humidity for refractivity, stable atmosphere, nocturnal inversions Unique: Longest potential range of any configured band (typical 400 km, exceptional 1500 km). Rain attenuation is zero (k=0.000). Propagation behavior closely mirrors 10 GHz but with lower free-space path loss and wider antenna beamwidths, making duct coupling geometry less critical.

1296 MHz (23cm) — L-Band Ducting

Primary mechanisms: Tropospheric ducting, troposcatter Key variable: Refractivity profile; absorption negligible (O₂ 0.006 dB/km, H₂O 0.0) Best conditions: Same seasonal/diurnal profile as 902 MHz and 10 GHz Unique: Typical 350 km, exceptional 1200 km. Zero rain attenuation. Slightly shorter range than 902 MHz due to increased FSPL.

2304 MHz (13cm) — S-Band

Primary mechanisms: Tropospheric ducting, enhanced refraction Key variable: Refractivity profile; onset of measurable rain sensitivity (k=0.001, alpha=1.15) Best conditions: Moderate-high humidity, stable inversions Unique: Typical 300 km, exceptional 1000 km. First band where rain has any measurable effect, though still minimal.

3456 MHz (9cm) — S-Band Upper

Primary mechanisms: Tropospheric ducting, enhanced refraction Key variable: Refractivity profile; mild rain sensitivity (k=0.002, alpha=1.20) Best conditions: Same as lower beneficial bands Unique: Typical 250 km, exceptional 900 km. Transitional band — still firmly in the "humidity beneficial" regime but approaching the range where free-space loss begins to limit practical paths.

5760 MHz (5cm) — C-Band

Primary mechanisms: Tropospheric ducting, enhanced refraction Key variable: Refractivity profile; moderate rain sensitivity (k=0.005, alpha=1.25) Best conditions: Moderate-high humidity, stable atmosphere Unique: Typical 220 km, exceptional 1000 km. Last beneficial-humidity band before 10 GHz. O₂ absorption increases slightly to 0.007 dB/km (matching 10 GHz). Rain attenuation still modest but becoming relevant in heavy rain.

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 Best months: June-July (ducting probability 69-77%). August contest data undersamples peak season. Worst month: March (10.8% ducting — worse than deep winter) Dataset: 53,013 QSOs, avg 213 km, P90 383 km, max 2,393 km. CW avg 232 km vs PH avg 187 km. Unique: Largely insensitive to rain. Can propagate through cloud decks. Marine ducting produces 1000+ km coastal paths. Frontal boundaries create strong refractive gradients. 97.2% of paths are terrain-blocked — ducting IS the propagation mechanism.

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 Night enhancement: +28% avg distance, +16% P90 vs afternoon (119.7 km vs 93.8 km) Dataset: 3,639 QSOs, avg 98 km, P90 179 km, max 710 km (CW). Note: raw PH average exceeds CW at 24 GHz due to Great Lakes contest manufacturing — with cluster activity removed, CW leads by 16% (see Finding 10). 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). March is the worst ducting month (10.8%) but also has low humidity, creating a tension between ducting availability and absorption loss.

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 Night enhancement: +36% avg distance, +42% P90 vs afternoon (86.6 km vs 63.5 km). Time-of-day is the dominant variable at this frequency. Dataset: 689 QSOs, avg 66 km, P90 122 km, max 343 km. 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, night/dawn Night enhancement: +360% avg distance vs afternoon (175.4 km vs 38.1 km). At this frequency, nighttime propagation is essentially a different regime. Daytime contacts are limited to ~40 km; nighttime contacts regularly exceed 150 km. Dataset: 104 QSOs, avg 64 km, P90 177 km, max 289 km. Unique: 289 km record (California marine duct). 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, wx_codes
  -> compute: abs_humidity, Td depression
  -> per-band scoring functions
  -> composite score per band
  -> 6-hour prediction timeline

HRRR Model (hourly, per grid point)
  Standard (surface + 13 pressure levels, ~25 MB/hour):
    -> surface T/Td/P, HPBL, PWAT, wind, cloud, precip
    -> pressure-level T/Td/HGT for refractivity profile (~250m spacing)
    -> fallback refractivity gradient if native data unavailable
  
  Native hybrid-sigma (50 levels, ~300 MB/hour):
    -> TMP, SPFH, HGT, PRES on 50 levels (10-50m near-surface spacing)
    -> cell-by-cell M-profile, duct detection, trapped frequency
    -> native_min_gradient replaces pressure-level gradient in scorer
    -> best_duct_freq_ghz, max_duct_thickness_m, duct_count as metadata
  
  -> refractivity score (10.5% weight, native resolution when available)
  -> PWAT score (11.3% weight)
  -> Key thresholds: gradient < -300 = moderate ducting, < -500 = strong ducting

Sounding Data (RAOB 00Z/12Z)
  -> 3,901 soundings from 112 stations
  -> Same derived params as HRRR but only twice daily
  -> 54% show ducting — binary flag useless, gradient magnitude is the signal
  -> K-index INVERSELY correlates with ducting (12.7 ducting vs 16.7 non-ducting)
  -> PWAT is NOT a ducting discriminator (identical 28.0 mm both cases)

IEMRE Gridded Data (hourly, 0.125° resolution)
  -> temp, dewpoint, sky_cover_pct, wind (u/v), precip at QSO endpoint locations
  -> More granular than nearest-ASOS matching
  -> 3,675 gridded observations in DB, enriched per-QSO

Terrain Data (SRTM + ITU-R P.526-16)
  -> path profile, Fresnel clearance, earth bulge with dynamic k-factor
  -> 97.2% of QSO paths are BLOCKED, 2.2% CLEAR, 0.6% FRESNEL_PARTIAL
  -> Blocked paths average LONGER distances (215 km) than clear paths (84 km)
  -> Determines LOS vs beyond-LOS regime
  -> P.526-16 Eq. 31 knife-edge loss, Deygout 3-edge method
  -> Dynamic k-factor from HRRR refractivity gradient (Section 2)

Commercial Link Data (SNMP polling, 5-min intervals)
  -> 7 links at 11/24/68 GHz near DFW
  -> rx_power_0, rx_power_1 (dual-chain MIMO on af11x), tx_power
  -> Signal variation scales with frequency: 68G > 24G > 11G
  -> Correlated with KTKI ASOS surface obs

Link Budget (point-to-point)
  -> FSPL + gaseous + rain + diffraction - duct enhancement
  -> margin = RX power - sensitivity
  -> success % = margin_to_success(margin, prop_score)
  -> Note: 36 dB avg diffraction > 14 dB max duct enhancement
     (gap closed by station EIRP + receiver sensitivity + troposcatter)

Display: Band Conditions Panel

For each band:

  • Current score (0-100, colored badge)
  • Estimated range (km, from score tier table, qualified by mode)
  • 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

Part 12: HRRR Native Hybrid-Sigma Levels

The standard HRRR product provides atmospheric profiles on 13 pressure levels (every 25 hPa from 1000-700 hPa), giving approximately 250m vertical spacing. This is insufficient for resolving the thin surface ducts (50-100m) that produce the strongest microwave propagation events. The native hybrid-sigma product provides dramatically better vertical resolution.

Product Details

The native HRRR file (wrfnatf00.grib2) carries all variables on the 50 hybrid-sigma levels native to the HRRR model grid. File size is approximately 530 MB per hour for the essential variables.

Extracted variables (7 per level, 350 messages total):

VariableDescription
TMPTemperature (K)
SPFHSpecific humidity (kg/kg)
HGTGeopotential height (m)
UGRDU-component wind (m/s)
VGRDV-component wind (m/s)
TKETurbulent kinetic energy (m²/s²)
PRESPressure (Pa)

Vertical spacing: ~10-50m near the surface vs ~250m for the pressure-level product. This resolves the thin boundary-layer structures (inversions, ducts, shear layers) that are invisible in the standard product.

Extraction Method

Each native-level file is too large for per-point on-demand fetching. The worker fetches the file once per (date, hour) and extracts profiles for all points of interest in one pass using wgrib2 point extraction (-lon). This avoids creating a coast-to-coast grid (~476k cells x 350 messages) that would cause OOM. Profiles are bulk-inserted into the hrrrnativeprofiles table.

The pure-function buildnativeprofile/1 converts parsed GRIB2 output into arrays sorted by ascending height (level 1 = surface), with surface scalars cached separately for quick access.

Derived Products

Native profiles feed into three analysis modules:

  1. Duct detection (Propagation.Duct) — M-profile analysis, per-duct geometry and trapped frequency
  2. Inversion analysis (Propagation.Inversion) — temperature inversion top, Bulk Richardson number, theta-e jump, wind shear
  3. Backtest features — nativesurfacerefractivity, bulkrichardson, thetaejump, shearattop, ductthickness, bestductfreq

Hourly Grid Integration

The PropagationGridWorker fetches native duct metrics for every CONUS grid point alongside the standard surface and pressure products. For each forecast hour (f00-f18):

  1. Download: TMP, SPFH, HGT, PRES on all 50 hybrid levels (~300 MB of byte ranges per hour via ductbyteranges/1)
  2. Extract: wgrib2 -lola interpolates to the CONUS 0.125° grid (~95k cells)
  3. Reduce: Cell-by-cell reducer (computeductmetrics/1) computes M-profile, detects ducts, and collapses each cell to 4 scalars — peak memory ~86 MB instead of ~1.8 GB for the full grid map
  4. Merge: Native duct metrics are merged into the standard HRRR grid profile before scoring
  5. Score: The refractivity factor uses the native gradient (10-50m resolution) when available, falling back to the pressure-level gradient (~250m resolution)

Per-cell output: nativemingradient (dM/dh minimum from native levels), bestductfreqghz (minimum trapped frequency), maxductthicknessm, duct_count.

If the native fetch fails for any reason (data not yet available, network error), scoring continues with pressure-level data only — the native enhancement is purely additive.

Download cost: ~300 MB/hour × 19 forecast hours = ~5.7 GB per hourly run (vs ~475 MB for surface + pressure only). Managed via sequential byte-range streaming to disk.


Part 13: Duct Analysis

The scalar minrefractivitygradient from the standard HRRR product captures whether ducting conditions exist but not the physical duct geometry. The Propagation.Duct module replaces this with explicit duct detection from native hybrid-sigma profiles, providing per-duct properties that enable frequency-dependent scoring.

Modified Refractivity M-Profile

From the ITU-R P.453-14 refractivity N at each level:

N = 77.6 * P/T + 3.73e5 * e/T²

where water vapor pressure e is derived from specific humidity: e = qP / (0.622 + 0.378q).

The modified refractivity M accounts for earth curvature:

M = N + 157 * h/1000

where h is height in meters. In a standard atmosphere, M always increases with height. A duct exists wherever M decreases with height (dM/dh < 0).

Duct Detection

The algorithm walks the M-profile looking for contiguous regions where M decreases. Each duct is characterized by:

PropertyDescription
base_mHeight (m) of duct base — where M begins decreasing
top_mHeight (m) of duct top — where M resumes increasing
thickness_mDuct thickness (top - base)
m_deficitTotal M decrease across the duct (M-units) — the strength of trapping

Multiple ducts per profile are possible and independently reported (e.g., a surface duct at 50-200m and an elevated duct at 800-1200m).

Minimum Trapped Frequency

The key improvement over the scalar gradient approach: a 50m duct can trap 24 GHz but not 3 GHz, and the scalar had no way to express this. The minimum trapped frequency uses the waveguide approximation from Bean & Dutton (1966):

λ_max = 2.5 * d * sqrt(ΔM * 1e-6)    (meters)
f_min = c / λ_max                       (Hz)

where d is duct thickness in meters and ΔM is the M-deficit. Returns 999 GHz for degenerate ducts (d ≤ 0 or ΔM ≤ 0).

Analysis Pipeline

The full pipeline — Duct.analyze/1 — takes a native profile and returns:

  • ducts — list of duct maps, each with base, top, thickness, M-deficit, and minfreqghz
  • bestductbandghz — the lowest minfreq_ghz across all detected ducts (nil if no ducts)

This enables per-band scoring: a duct is "usable" for a given band only if minfreqghz <= bandfrequencyghz.


Part 14: Inversion Analysis

Temperature inversions — where temperature increases with height, violating the normal lapse rate — are the boundary layers that act as mirrors for RF propagation. The Propagation.Inversion module detects inversions from native HRRR profiles and computes the stability and turbulence properties at the inversion top that determine whether the layer is smooth enough to support ducting.

Inversion Top Detection

The algorithm walks the native profile upward looking for the first (lowest) temperature inversion:

  1. Find the inversion base — the level where dT/dz first turns positive (temperature begins increasing with height)
  2. Find the inversion top — the level where dT/dz turns negative again (temperature resumes its normal decrease)
  3. Report the inversion strength_k — total temperature increase from base to top (K)

Stability Properties at Inversion Top

Three quantities characterize whether the inversion layer is stable enough for propagation:

Bulk Richardson Number (Ri):

Ri = (g / θ_ref) * Δθ * Δz / (ΔU² + ΔV²)

where Δθ is the potential temperature difference across the layer, ΔU/ΔV are wind component differences, and θ_ref is the mean potential temperature. Potential temperature: θ = T * (P₀/P)^0.286 where P₀ = 100000 Pa.

Ri RangeRegimePropagation Impact
< 0.25TurbulentBad — mixing disrupts duct structure
0.25-1.0TransitionMarginal — intermittent ducting
> 1.0LaminarGood — stable, smooth reflective layer

Ri is clamped to 100.0 for practical use when wind shear is near zero (effectively infinite stability).

Equivalent Potential Temperature (θₑ) Jump:

The change in θₑ across the inversion. A larger positive jump indicates stronger thermodynamic decoupling between the air masses above and below the inversion — a sharper boundary that reflects RF energy more efficiently.

Wind Shear Magnitude:

shear = sqrt((u_top - u_base)² + (v_top - v_base)²)    (m/s)

Strong shear at the inversion top can mechanically disrupt the layer (reducing Ri below 0.25) or, in moderate amounts, help maintain the inversion through differential advection.


Part 15: NEXRAD Radar Data

The NexradClient fetches IEM CONUS n0q composite reflectivity images as a proxy for boundary-layer turbulence and precipitation structure.

Product Specification

ParameterValue
Productn0q composite reflectivity
SourceIEM archive (mesonet.agron.iastate.edu)
FormatPalettized 8-bit PNG
Dimensions12200 x 5400 pixels
CoverageCONUS (-126W to -65W, 23N to 50N)
Resolution0.005 degrees/pixel
CadenceEvery 5 minutes
Pixel mappingValue 0 = no echo; 1-255 maps linearly to -30 to +95 dBZ

Per-Point Processing

For each point of interest, a ~25 km box (~50x50 pixels) centered on the lat/lon is extracted. Summary statistics computed per box:

  • meanreflectivitydbz — average dBZ of non-zero pixels
  • maxreflectivitydbz — peak reflectivity in the box
  • texture_variance — sample variance of dBZ values within the box
  • pixel_count — number of non-zero echo pixels

Backtest Use

The nexradtexture feature in the backtest framework uses texturevariance as a proxy for boundary-layer convective activity. Higher variance indicates more turbulence, which is generally worse for microwave propagation (disrupts stable ducting layers). The feature looks up the nearest NEXRAD observation within ±15 minutes and ±0.1 degrees of the target point.


Part 16: Backtest Framework

The backtest framework (Microwaveprop.Backtest) evaluates whether a candidate feature function carries information about propagation quality by comparing its distribution during actual QSO events against a matched random baseline.

Methodology

A feature function has the shape (lat, lon, valid_time) -> float | nil. The evaluate/2 function:

  1. Loads up to N QSOs (:sample_size, default 5000) with known positions, newest first
  2. Evaluates the feature at each QSO's station1 location and timestamp
  3. Generates a matched random baseline: for each baseline sample, picks a real QSO location and perturbs its timestamp by uniform ±30 days. This controls for seasonal and geographic distribution so the baseline is not trivially distinguishable.
  4. Reports Distribution statistics (count, mean, stddev, p50, p90, min, max) for both the QSO and baseline samples

If a feature has discriminating power, the QSO distribution should differ systematically from the baseline distribution — e.g., stronger gradients or lower PWAT during actual contacts.

Analysis Dimensions

  • evaluate/2 — QSO vs random baseline distributions
  • liftbydistance/2 — Feature distribution binned by QSO distance (0-100 km, 100-250 km, 250-500 km, 500-1000 km, 1000+ km). A useful feature should show monotonically increasing values across distance bins.
  • liftbyband/2 — Feature distribution grouped by band. Reveals band-dependent lift (e.g., duct geometry features should carry more information at 24+ GHz).
  • consolidated_report/2 — Runs all features against the same QSO sample, producing a single comparison table.

Implemented Features

Features are grouped by data source and physical quantity:

Standard HRRR profile (scalar):

FeatureSourceDescription
naive_gradienthrrr_profilesMinimum refractivity gradient (N/km) — current scorer baseline
td_depressionhrrr_profilesSurface T - Td (°C) — atmospheric stability proxy
pressurehrrr_profilesSurface pressure (hPa) — frontal activity proxy
timeofdaytimestampUTC hour as float [0, 24) — diurnal baseline

Native hybrid-sigma profile (duct/inversion):

FeatureSourceDescription
nativesurfacerefractivityhrrrnativeprofilesITU-R P.453 N from native-level data — sanity check vs pressure-level
bulk_richardsonhrrrnativeprofilesRi at inversion top — laminar (>1) vs turbulent (<0.25)
thetaejumphrrrnativeprofilesθₑ jump (K) across inversion — decoupling strength
shearattophrrrnativeprofilesWind shear (m/s) at inversion top
duct_thicknesshrrrnativeprofilesMax duct thickness (m) — larger ducts trap lower frequencies
bestductfreqhrrrnativeprofilesLowest trapped frequency (GHz) — lower = stronger ducting
~~ductusable10ghz~~hrrrnativeprofilesDEAD — no discrimination (always 1.0 for both QSO and baseline)
~~ductusable24ghz~~hrrrnativeprofilesDEAD — no discrimination (always 1.0)
~~ductusable47ghz~~hrrrnativeprofilesDEAD — no discrimination (always 1.0)
bulk_richardsonhrrrnativeprofilesDead as a standalone ML feature (near-identical means — see backtest), but wired as a gate on the native-duct boost in Scorer.score_refractivity/5: a duct reading with Ri ≥ 25 is suppressed because mechanical mixing would likely destroy the trapping layer before it carried a signal.

Climatology and remote sensing:

FeatureSourceDescription
temperature_anomalyhrrrprofiles + hrrrclimatologySurface T minus climatological mean for (grid cell, month, hour) — anomalously hot days produce enhanced ducting
nexrad_texturenexrad_observationsReflectivity texture variance — convective turbulence proxy

Placeholder (not yet implemented):

FeatureSourceDescription
distancetofrontDistance (km) to nearest detected front — awaiting frontal analysis pipeline
paralleltofrontcos²(path-front angle) — awaiting frontal analysis + requires path bearing

Consolidated Backtest Results (2026-04-11)

Sample: 5,000 QSOs, 11,431 native profiles across 499 HRRR hours (2019-2024).

FeatureQSO NQSO MeanQSO p50Baseline MeanBaseline p50Signal
thetaejump491549.4 K5.8 K34.3 K2.1 KStrong — 44% higher jumps during QSOs
bestductfreq6970.84 GHz0.40 GHz0.28 GHz0.26 GHzStrong — QSO ducts trap lower freqs
nativesurfacerefractivity4915331.7334.4324.3330.1Moderate — higher N during QSOs
duct_thickness697156 m159 m227 m217 mInverted — thinner ducts during QSOs (shallow surface ducts)
td_depression50006.8°C5.8°C5.9°C3.8°CModerate — wider depression during QSOs
timeofday500016.7h17.9h11.9h11.8hStrong — contests are evening-biased
naive_gradient5000-113-104-107-97Weak — small separation
shearattop49156.4 m/s4.0 m/s5.8 m/s3.3 m/sMarginal
pressure5000983.8989.5982.7989.7Weak — near-identical
nexrad_texture179616.80.020.60.0Weak/inverted
bulk_richardson491524.86.323.45.8Dead — no discrimination
ductusable*ghz697~1.01.0~1.01.0Dead — always 1.0
temperature_anomaly0NO DATA (climatology not built)

Key findings:

  • thetaejump is the single strongest native-level discriminator — large theta-e jumps at inversion tops indicate strong decoupling that traps microwave energy
  • bestductfreq confirms that QSO-producing ducts are physically stronger (trap lower frequencies)
  • duct_thickness being inverted makes physical sense: shallow surface ducts (100-200m) produce the strongest trapping for microwave bands, while thick ducts (>200m) are weaker elevated features
  • ductusable*ghz features are dead because nearly all detected ducts are thick enough to trap 10-47 GHz — the threshold is too low to discriminate
  • bulkrichardson shows no signal as a standalone ML feature because both stable (high Ri) and unstable (low Ri) conditions can produce inversions; Ri alone doesn't predict duct quality. It does work as a gate on the native-duct boost though — see Scorer.scorerefractivity/5 — because the question "is this duct dynamically stable enough to survive" is different from "does this cell have a duct at all"

Implications for real-time scoring: The native features cannot currently be used in real-time propagation scoring because the native HRRR product (~530 MB/hour) is too expensive to fetch for the full CONUS grid. However, the findings validate the existing scorer's physics: humidity, td_depression, and refractivity factors capture the same mechanisms (moisture-driven refractivity, inversion strength) that the native features measure more directly. Future work could incorporate native data for specific paths or high-interest regions.

HRRR Climatology

The hrrrclimatology table stores pre-computed mean and standard deviation of surface temperature from hrrrprofiles, keyed on (lat, lon, month, hour) at the HRRR grid resolution. This allows computing how anomalous the current surface temperature is relative to historical norms for the same location, season, and time of day. The meteorologist noted that extremely hot days (~10°F above normal in summer) produce enhanced ducting even in the afternoon when the time-of-day factor normally suppresses the score. The temperatureanomaly feature returns surfacetempc - climatologicalmean as a signed float.


Constants Reference

ConstantValueSource
Earth radius6371 kmWGS-84 mean
Standard K-factor4/3Standard atmosphere
Standard surface N315ITU-R P.453
Standard dN/dh-40 /kmITU-R P.453
Humidity penalty 24 GHz1.6Near 22.235 GHz H2O peak
Humidity penalty 47 GHz1.0Atmospheric window
Humidity penalty 68 GHz1.460 GHz O2 wing + H2O
Humidity penalty 241 GHz3.0Between H2O 183 & 325
Duct M-unit threshold2Noise filter
Inversion min strength0.5CBelow is noise
Inversion height limit5000m AGLAbove irrelevant
BL depth shallow threshold<300mHRRR: avg gradient -93.7 at BL<200m
Ducting gradient threshold-300 N/kmSounding avg for ducting events: -389
Non-ducting gradient avg-123 N/kmSounding avg for non-ducting events
Ducting surface N threshold330Above this, ducting probability >50%
Signal prediction floor+/- 2-3 dBMeasured from link data
CW bandwidth advantage~7 dB10*log10(2700/500); 16-221% range increase depending on band
Pressure correlation (10 GHz)rho=-0.18057,248 QSO-HRRR analysis, Apr 2026
PWAT optimal range (10 GHz)20-30 mmBest median distance (219 km)
PWAT correlation (75 GHz)rho=-0.60857,248 QSO-HRRR analysis, Apr 2026

ITU-R Recommendations

The following ITU-R Recommendations provide the physics models underlying the scoring algorithm. All are publicly available from the ITU Radiocommunication Sector (https://www.itu.int/rec/R-REC-P/en).

RecommendationTitleUsed For
ITU-R P.453-14The radio refractive index: its formula and refractivity dataSurface refractivity N calculation, refractivity gradient
ITU-R P.525-4Calculation of free-space attenuationFree-space path loss baseline
ITU-R P.676-13Attenuation by atmospheric gases and related effectsO2 and H2O absorption coefficients per band
ITU-R P.838-3Specific attenuation model for rain for use in prediction methodsRain attenuation coefficients (k, alpha) per band
ITU-R P.526-16Propagation by diffractionKnife-edge loss (Eq. 31), Deygout 3-edge method (Section 6), dynamic k-factor (Section 2)
ITU-R P.452-17Prediction procedure for the evaluation of interference between stations on the surface of the EarthClear-air propagation modeling framework
ITU-R P.835-6Reference standard atmospheresStandard atmosphere profiles for baseline
ITU-R P.530-18Propagation data and prediction methods for terrestrial line-of-sight systemsMultipath fading and enhancement statistics

Data Sources

Primary QSO Dataset

ARRL Microwave Contest Results (1992-2024)

  • Source: Contest log submissions compiled from ARRL contest results
  • Volume: 58,282 total QSOs across 13+ bands (10 GHz through 403 GHz)
  • Usable subset: 57,488 tropospheric QSOs after filtering 4 EME contacts (QRA64D/JT4F modes >3,000 km)
  • Fields: station callsigns, Maidenhead grid squares, timestamp, mode (CW/SSB/FM/FT8), band
  • Grid-to-coordinate conversion: gridmap.org API for Maidenhead → lat/lon
  • Distance: Haversine great-circle calculation from grid square centers

Surface Weather Observations (ASOS)

Iowa Environmental Mesonet (IEM) — Automated Surface Observing System

  • API: https://mesonet.agron.iastate.edu/cgi-bin/request/asos.py
  • Network discovery: https://mesonet.agron.iastate.edu/api/1/network.py
  • Station count: 2,922 total, 1,299 with observations matched to QSOs
  • Observation count: 58,398 surface observations
  • Fields: temperature (°F), dewpoint (°F), relative humidity (%), wind speed (kts), wind direction (°), sea level pressure (mb), sky condition (CLR/FEW/SCT/BKN/OVC), precipitation (inches/hour), weather codes
  • Temporal matching: ±2 hours around QSO timestamp
  • Spatial matching: nearest station within 150 km of QSO path endpoints
  • No authentication required

Upper-Air Soundings (RAOB)

Iowa Environmental Mesonet (IEM) — Radiosonde Observations

  • API: https://mesonet.agron.iastate.edu/json/raob.py
  • Station count: 346 sounding stations total, 112 with data matched to QSOs
  • Sounding count: 3,901 vertical profiles
  • Standard times: 00Z and 12Z (bracketing QSO timestamps)
  • Spatial matching: nearest station within 300 km of QSO path
  • Raw profile: pressure, temperature, dewpoint, height per level
  • Derived parameters (computed at ingestion by SoundingParams.derive/1): - Surface refractivity (ITU-R P.453-14 formula) - Minimum refractivity gradient (N/km) — primary ducting indicator - Boundary layer depth (m) - Precipitable water (mm) - K-index, Lifted index — atmospheric stability - Ducting detection and duct characteristics (height, strength in M-units)

HRRR Model Data

NOAA High-Resolution Rapid Refresh (HRRR)

  • Source: AWS S3 public bucket https://noaa-hrrr-bdp-pds.s3.amazonaws.com
  • Format: GRIB2 files, hourly cadence, 3km horizontal resolution
  • Profile count: 4,522 profiles matched to QSO/grid locations
  • Pressure levels extracted: every 25 hPa from 1000–700 mb (13 levels: 1000, 975, 950, 925, 900, 875, 850, 825, 800, 775, 750, 725, 700)
  • Surface fields: temperature (°C), dewpoint (°C), pressure (mb), HPBL (boundary layer height, m), PWAT (precipitable water, mm), 10m wind components (u, v), cloud cover (%), precipitation (mm)
  • Per-level fields: temperature, dewpoint, geopotential height
  • Derived: refractivity profile, min gradient, ducting detection (same as sounding derivation)
  • Batch optimization: groups multiple grid points by HRRR hour to download GRIB2 once per time step

IEMRE Gridded Hourly Weather

Iowa Environmental Mesonet Reanalysis (IEMRE)

  • API: https://mesonet.agron.iastate.edu/iemre/hourly/{date}/{lat}/{lon}/json
  • Resolution: 0.125° (~14 km) gridded
  • Observation count: 3,675 gridded hourly observations
  • Fields per hour: air temperature (°F), dewpoint (°F), sky cover (%), wind components (u/v, m/s), hourly precipitation (inches), solar radiation
  • Used for: weather at QSO endpoint grid points, more granular than nearest-ASOS matching
  • Status: ingested but not yet integrated into scoring factors

Terrain Elevation Data

SRTM (Shuttle Radar Topography Mission)

  • Primary source: AWS S3 https://elevation-tiles-prod.s3.amazonaws.com/skadi (local tile cache)
  • Resolution: 90m (SRTM3, 3 arc-second)
  • Tile format: .hgt binary, 3601×3601 samples, 16-bit signed big-endian
  • Fallback APIs (when tiles unavailable): - Open-Meteo: https://api.open-meteo.com/v1/elevation - OpenTopography: https://api.opentopodata.org/v1/srtm90m
  • Profile method: 64 elevation samples per QSO path (great-circle interpolation)
  • Path count: 58,276 QSO paths profiled
  • Results: 56,658 BLOCKED (97.2%), 1,277 CLEAR (2.2%), 341 FRESNEL_PARTIAL (0.6%)
  • Analysis: ITU-R P.526-16 knife-edge diffraction (Eq. 31), Deygout 3-edge method for multiple obstacles, dynamic k-factor from HRRR refractivity gradient (falls back to k=4/3 when HRRR unavailable)

Commercial Link Validation Data

Ubiquiti airFiber and airFiber 60 links near Princeton, TX

  • Link count: 7 commercial microwave links
  • Frequencies: 11 GHz (AF11X, dual-chain), 24 GHz (AF11X), 68 GHz (AF60, single-chain)
  • Polling: SNMP at 5-minute intervals (rxpower0, rxpower1 for dual-chain; rx_power for single)
  • Weather correlation: KTKI ASOS station
  • Historical dataset: March 14-29, 2026 (18,540 samples)
  • Validated coefficients: 11 GHz and 24 GHz gaseous absorption, 68 GHz O2 band wing absorption (0.1 dB/km per g/m³ measured on 2.8 km path)
  • Live polling: active for ongoing validation

Solar Indices

GFZ German Research Centre for Geosciences

  • Source: https://kp.gfz.de/app/files/KpapApSNF107since1932.txt
  • Volume: 9,586 daily values (1998-2026)
  • Fields: Solar Flux Index (F10.7), adjusted SFI, sunspot number, Ap index, Kp values (3-hourly)
  • Status: ingested but NOT used in tropospheric scoring — reserved for potential VHF/sporadic-E extension

Live Scoring Data

Propagation Grid Scores

  • Coverage: CONUS grid at 0.125° resolution
  • Update cadence: hourly (HRRR-based via PropagationGridWorker) + 10-minute ASOS adjustments (AsosAdjustmentWorker)
  • Per grid point: composite score (0-100), 9 individual factor scores, valid_time
  • Bands scored: all configured bands (902 MHz, 1296 MHz, 2304 MHz, 3456 MHz, 5760 MHz, 10, 24, 47, 68, 75, 122, 134, 241 GHz)
  • Retention: 2 most recent valid_times kept, older data pruned automatically

Known Data Quality Issues

  • EME contamination: 4 QSOs >3,000 km remain in dataset (QRA64D/JT4F modes). Filter on distance_km < 3000 for tropospheric analysis.
  • Unmodeled bands: 142, 145, 288, 322, 403, 411 GHz have 1-4 QSOs each but no band_config entries. Too sparse for statistical analysis. The 902 MHz through 5760 MHz bands are now implemented with beneficial humidity effect and shared seasonal tables matching 10 GHz.
  • Sounding data recency: Latest soundings are from Sep 2024. Ingestion pipeline may need restart for live enrichment.
  • Surface obs density: Historical obs are sparse (~1 per 4.7 days per station) because they were fetched per-QSO-window. Live polling is now active for continuous coverage.
  • Weather codes (wxcodes): Stored in surfaceobservations but unused by scoring. Direct fog/thunderstorm/freezing-rain detection could supplement indirect inference from Td depression and sky condition.
  • Contest bias: 97% of QSOs are from August-September ARRL Microwave Contest. Seasonal and regional statistics may not generalize to year-round conditions.
  • Rain model unvalidated: ITU-R P.838-3 rain attenuation coefficients are theoretical — no rain events occurred in the commercial link validation dataset.