# Microwave Propagation Algorithm — Unified

## 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) | k | Regime |
|---|---|---|
| > 0 | < 1 | Sub-refraction |
| 0 to −79 | 1.0–1.33 | Standard |
| −79 to −157 | 1.33–∞ | Super-refraction |
| < −157 | negative | Ducting (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 `narr_profiles` with the same schema as `hrrr_profiles` 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.best_profile_for_contact/1` picks HRRR first and falls back to NARR when the HRRR archive doesn't cover the contact. `Weather.profiles_along_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):

| Month | Ducting % | Mean dN/dh min | Mean PWAT (mm) |
|-------|-----------|----------------|----------------|
| Jun | 68.7% | −323 | 28.6 |
| Jul | 76.5% | −301 | 27.6 |
| Aug | 53.9% | −261 | 33.3 |
| Sep | 56.4% | −287 | 26.2 |
| Oct | 60.4% | −314 | 17.9 |
| Mar | 10.8% | −113 | 6.6 |
| Dec–Feb | 12–22% | −130 | 7–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.

2. **Sub-grid mesoscale:** Sea breeze fronts, outflow boundaries, terrain-induced convergence zones — ducting features below the 3 km HRRR grid are not resolved.

3. **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.

4. **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.

5. **Scintillation:** Amplitude scintillation from refractive turbulence on long LOS paths is not modeled.

6. **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):

| Band | f (GHz) | O2 (dB/km) | H2O Coeff (dB/km per g/m^3) | Total @ 7.5 g/m^3 | Dominant Constraint |
|------|---------|------------|---------------------------|-------------------|---------------------|
| 902M | 0.902 | 0.006 | 0.0 | 0.006 | Negligible absorption |
| 1296M | 1.296 | 0.006 | 0.0 | 0.006 | Negligible absorption |
| 2304M | 2.304 | 0.006 | 0.0 | 0.006 | Negligible absorption |
| 3456M | 3.456 | 0.006 | 0.0 | 0.006 | Negligible absorption |
| 5760M | 5.760 | 0.007 | 0.0 | 0.007 | Negligible absorption |
| 10G | 10.368 | 0.007 | 0.0 | 0.007 | Negligible absorption |
| 24G | 24.192 | 0.02 | 0.002 | 0.035 | 22.235 GHz H2O line |
| 47G | 47.088 | 0.04 | 0.003 | 0.063 | O2 wing + mild H2O |
| 68G | 68.040 | 0.90 | 0.007 | 0.95 | 60 GHz O2 band wing |
| 75G | 76.032 | 0.012 | 0.006 | 0.057 | Window band |
| 122G | 122.250 | 0.80 | 0.010 | 0.875 | 118.75 GHz O2 wing |
| 134G | 134.928 | 0.08 | 0.015 | 0.193 | Between O2 118 & H2O 183 |
| 142G | 142.000 | 0.05 | 0.025 | 0.238 | Approaching H2O 183 |
| 145G | 145.000 | 0.06 | 0.040 | 0.360 | H2O 183 line wing |
| 241G | 241.000 | 0.08 | 0.30 | 2.33 | Between H2O 183 & H2O 325 |
| 288G | 288.000 | 0.10 | 0.45 | 3.48 | Approaching H2O 325 |
| 322G | 322.000 | 0.12 | 0.55 | 4.25 | Near H2O 325 line |
| 403G | 403.000 | 0.15 | 0.40 | 3.15 | Past H2O 325, sub-mm window |
| 411G | 411.000 | 0.15 | 0.42 | 3.30 | Sub-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):

| Band | k_H | alpha_H | Light 4mm/hr | Moderate 10mm/hr | Heavy 25mm/hr |
|------|------|------|-------------|-----------------|--------------|
| 902M | 0.000 | 1.00 | 0.00 | 0.00 | 0.00 |
| 1296M | 0.000 | 1.00 | 0.00 | 0.00 | 0.00 |
| 2304M | 0.001 | 1.15 | 0.005 | 0.01 | 0.04 |
| 3456M | 0.002 | 1.20 | 0.01 | 0.03 | 0.08 |
| 5760M | 0.005 | 1.25 | 0.02 | 0.09 | 0.24 |
| 10G | 0.010 | 1.28 | 0.05 | 0.19 | 0.56 |
| 24G | 0.070 | 1.07 | 0.31 | 0.81 | 2.04 |
| 47G | 0.187 | 0.93 | 0.68 | 1.58 | 3.69 |
| 68G | 0.310 | 0.86 | 0.98 | 2.18 | 4.73 |
| 75G | 0.345 | 0.84 | 1.07 | 2.40 | 5.18 |
| 122G | 0.498 | 0.77 | 1.32 | 2.93 | 5.91 |
| 134G | 0.520 | 0.75 | 1.34 | 2.93 | 5.81 |
| 142G | 0.530 | 0.74 | 1.34 | 2.92 | 5.74 |
| 145G | 0.535 | 0.74 | 1.35 | 2.95 | 5.79 |
| 241G | 0.550 | 0.70 | 1.30 | 2.76 | 5.20 |
| 288G | 0.560 | 0.68 | 1.27 | 2.66 | 4.90 |
| 322G | 0.570 | 0.66 | 1.23 | 2.55 | 4.62 |
| 403G | 0.580 | 0.64 | 1.20 | 2.45 | 4.36 |
| 411G | 0.580 | 0.64 | 1.20 | 2.45 | 4.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) | k | Condition |
|---|---|---|
| 0 | 1.0 | No refraction |
| −39 | 4/3 | Standard atmosphere |
| −100 | ~2.7 | Enhanced refraction |
| −157 | ∞ | Ray follows earth curvature |
| < −157 | negative | Super-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. Humidity | Avg Dist | P90 Dist |
|---------------|----------|----------|
| 5-8 g/m^3 | 193 km | 342 km |
| 11-14 g/m^3 | 215 km | 383 km |
| 17+ g/m^3 | 230 km | 519 km |

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

| Abs. Humidity | Avg Dist | P90 Dist |
|---------------|----------|----------|
| 5-8 g/m^3 | 115 km | 154 km |
| 11-14 g/m^3 | 105 km | 174 km |
| 17+ g/m^3 | 53 km | 103 km |

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

| Abs. Humidity | Avg Dist | P90 Dist |
|---------------|----------|----------|
| 0-5 g/m^3 | 191 km | 234 km |
| 11-14 g/m^3 | 71 km | 114 km |

### Finding 2: Wind Penalty Is Overrated

Data shows no meaningful penalty for wind on achieved distance:

| Wind | 10G Avg | 24G Avg | 47G Avg |
|------|---------|---------|---------|
| Calm (0-3 kts) | 216 | 84 | -- |
| Light (3-7 kts) | 214 | 100 | 77 |
| Moderate (7-12 kts) | 220 | 113 | 77 |

Wind may reduce inversion quality but also creates boundary-layer dynamics that can enhance propagation. Weight reduced from 18% to 8%.

### Finding 3: Low Pressure Correlates with Longer Distances

Contradicts the common assumption that high pressure = good propagation:

| Pressure | 10G Avg | 10G P90 | 24G Avg | 47G Avg |
|----------|---------|---------|---------|---------|
| <1010 | 262 | 506 | 119 | 111 |
| 1015-1020 | 217 | 383 | 97 | 74 |
| 1025+ | 196 | 354 | 122 | -- |

Low pressure systems bring frontal boundaries with strong temperature/moisture gradients that create inversions and ducts. The key is gradient structure, not absolute pressure.

### Finding 4: Boundary Layer Depth — Time-Dependent Sweet Spot

Sounding data (3,901 profiles) reveals that BL depth interpretation depends on sounding time:

| Sounding Time | Avg BL Depth | Ducting % | Mechanism |
|---------------|-------------|-----------|-----------|
| 00Z (evening) | 1,370m | 54.0% | Elevated ducts within deep residual BL |
| 12Z (morning) | 335m | 53.7% | Surface/radiation ducts from shallow nocturnal BL |

Ducting rate is identical at both times (~54%), but via different mechanisms. At 12Z, shallow BL (<500m) is the signature. At 00Z, the duct is typically elevated within a deeper residual boundary layer. A single BL depth threshold won't work across both regimes — use BL depth relative to expected diurnal range.

HRRR profile data (4,522 profiles) confirms the relationship is monotonic:

| BL Depth Bin | Count | Avg N | Avg Min Gradient |
|-------------|-------|-------|------------------|
| 0-200m | 1,096 | 342.6 | -93.7 |
| 200-500m | 1,091 | 346.6 | -81.5 |
| 500-1000m | 1,242 | 337.0 | -70.1 |
| 1000-1500m | 735 | 328.8 | -58.3 |
| 1500-2000m | 299 | 325.2 | -51.0 |
| 2000m+ | 60 | 309.4 | -45.4 |

### Finding 5: Binary Duct Detection Is Weak — Use Continuous Gradient

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.

| Ducting | Count | Avg N | Avg Min Gradient | Avg BL Depth | Avg K-Index | Avg LI |
|---------|-------|-------|------------------|-------------|-------------|--------|
| No | 1,800 | 327.4 | -123.4 | 986m | 16.7 | 22.8 |
| Yes | 2,099 | 340.2 | -388.7 | 738m | 12.7 | 25.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.

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

| Band | Afternoon Avg | Night/Dawn Avg | Enhancement | P90 Boost |
|------|--------------|----------------|-------------|-----------|
| 10 GHz | 209.7 km | 218.6 km | +4% | +8% |
| 24 GHz | 93.8 km | 119.7 km | +28% | +16% |
| 47 GHz | 63.5 km | 86.6 km | +36% | +42% |
| 75 GHz | 38.1 km | 175.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.

### Finding 9: Ducting Peaks June-July, Not August

Monthly ducting probability from 3,901 soundings:

| Month | Soundings | Ducting % | Avg N | Avg Min Gradient | Avg PWAT (mm) |
|-------|-----------|-----------|-------|------------------|---------------|
| Jan | 32 | 21.9% | 306.2 | -133.4 | 9.5 |
| Feb | 58 | 17.2% | 304.4 | -132.7 | 6.6 |
| Mar | 37 | **10.8%** | 293.3 | -112.5 | 6.6 |
| Apr | 64 | 37.5% | 306.0 | -152.0 | 14.0 |
| May | 68 | 48.5% | 323.3 | -233.1 | 20.5 |
| Jun | 134 | **68.7%** | 335.0 | -322.7 | 28.6 |
| Jul | 85 | **76.5%** | 339.3 | -301.4 | 27.6 |
| Aug | 1,700 | 53.9% | 341.9 | -260.7 | 33.3 |
| Sep | 1,564 | 56.4% | 332.4 | -286.5 | 26.2 |
| Oct | 48 | 60.4% | 321.5 | -314.3 | 17.9 |
| Nov | 53 | 56.6% | 308.4 | -264.3 | 10.6 |
| Dec | 58 | 12.1% | 305.0 | -139.8 | 10.5 |

**March is the worst month** (10.8%), not winter. December-February averages ~17%. The sharp ramp April→July is nonlinear. Contest data (Aug-Sep) slightly undersamples the true ducting peak (Jun-Jul at 69-77%). Note: Aug/Sep have disproportionate sounding counts because data is enriched from QSO time windows during contest months.

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

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

| Field | Region | QSOs | Avg km | P90 km | Max km |
|-------|--------|------|--------|--------|--------|
| FM | Mid-Atlantic (DC/VA/MD) | 664 | 321 | 606 | 1041 |
| EL | Florida/Gulf Coast | 32 | 231 | 460 | 1609 |
| CM | N. California | 3,935 | 224 | 411 | 1460 |
| EN | Upper Midwest (WI/MN/IL) | 18,745 | 217 | 343 | 1223 |
| FN | Northeast (NY/NE) | 20,957 | 215 | 418 | 1212 |
| DM | SoCal/Southwest | 6,314 | 203 | 365 | 1448 |
| EM | South-Central (TX/OK) | 2,204 | 146 | 272 | 1609 |
| CN | Pacific Northwest | 119 | 84 | 192 | 468 |

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

| Field | Region | QSOs | Avg km | P90 km |
|-------|--------|------|--------|--------|
| DM | SoCal/Southwest | 279 | 157 | 227 |
| CM | N. California | 318 | 129 | 221 |
| EN | Upper Midwest | 1,135 | 98 | 178 |
| FN | Northeast | 1,644 | 90 | 144 |
| EM | South-Central | 251 | 43 | 83 |

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:

| Region | Stations | Soundings | Ducting % | Avg BL Depth |
|--------|----------|-----------|-----------|-------------|
| West Coast | 17 | 634 | 61.7% | 1043m |
| Southeast/Gulf | 29 | 1,357 | 55.6% | 741m |
| Central | 37 | 1,235 | 53.6% | 990m |
| Northeast/Mid-Atl | 16 | 930 | 52.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):**

| Variable | rho | n_valid |
|----------|-----|---------|
| Pressure (mb) | **-0.180** | 52,846 |
| Month | 0.105 | 52,846 |
| Dewpoint (°C) | -0.059 | 52,846 |
| HPBL (m) | 0.045 | 52,846 |
| PWAT (mm) | -0.039 | 52,846 |
| Refractivity Gradient | -0.034 | 52,347 |
| Temperature (°C) | 0.031 | 52,846 |
| Surface Refractivity | -0.024 | 52,347 |
| UTC Hour | **0.007** | 52,846 |

**24 GHz (n=3,621):**

| Variable | rho | n_valid |
|----------|-----|---------|
| Dewpoint (°C) | **-0.371** | 3,621 |
| PWAT (mm) | **-0.330** | 3,621 |
| Surface Refractivity | **-0.317** | 3,582 |
| Month | 0.272 | 3,621 |
| Temperature (°C) | -0.179 | 3,621 |
| Pressure (mb) | -0.172 | 3,621 |
| Refractivity Gradient | -0.075 | 3,582 |
| UTC Hour | 0.056 | 3,621 |
| HPBL (m) | -0.049 | 3,621 |

**47 GHz (n=680):**

| Variable | rho | n_valid |
|----------|-----|---------|
| Pressure (mb) | **-0.231** | 680 |
| PWAT (mm) | **-0.227** | 680 |
| Dewpoint (°C) | -0.181 | 680 |
| Refractivity Gradient | -0.139 | 678 |
| Month | 0.111 | 680 |
| Surface Refractivity | -0.109 | 678 |
| Temperature (°C) | 0.037 | 680 |
| UTC Hour | -0.024 | 680 |
| HPBL (m) | 0.004 | 680 |

**75 GHz (n=94):**

| Variable | rho | n_valid |
|----------|-----|---------|
| Dewpoint (°C) | **-0.703** | 94 |
| PWAT (mm) | **-0.608** | 94 |
| Temperature (°C) | **-0.589** | 94 |
| Pressure (mb) | **-0.570** | 94 |
| Surface Refractivity | **-0.526** | 94 |
| UTC Hour | -0.392 | 94 |
| HPBL (m) | 0.150 | 94 |
| Month | 0.144 | 94 |
| Refractivity Gradient | -0.082 | 94 |

### Key Insights

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

| Pressure Bin | n | Median km | p25 | p75 |
|-------------|---|-----------|-----|-----|
| <1005 mb | 47,669 | **197.1** | 121.2 | 285.1 |
| 1005-1013 mb | 3,447 | 151.3 | 100.7 | 289.2 |
| 1013-1020 mb | 1,254 | 130.8 | 77.9 | 239.2 |
| >1020 mb | 476 | **103.4** | 76.8 | 191.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. `hrrr_native_profiles.best_duct_band_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 Bin | n | Median km | p25 | p75 |
|---------|---|-----------|-----|-----|
| <10 mm | 1,834 | 160.6 | 96.8 | 265.0 |
| 10-20 mm | 15,041 | 193.8 | 125.1 | 280.7 |
| 20-30 mm | 17,788 | **218.8** | 129.9 | 295.6 |
| 30-40 mm | 13,999 | 173.9 | 106.5 | 289.6 |
| >40 mm | 4,184 | 155.2 | 84.5 | 256.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 `hrrr_native_profiles` 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 `hrrr_native_profiles` 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):

| Band | HRRR-matched contacts |
|---|---|
| 222 MHz | 5,392 |
| 432 MHz | 6,583 |
| 902 MHz | 1,317 |
| 1,296 MHz | 2,146 |
| 2,304 MHz | 564 |
| 3,400 MHz | 280 |
| 5,760 MHz | 246 |
| 10,000 MHz | 6,675 |
| 24,000 MHz | 613 |
| 47,000 MHz | 53 |

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

| Month | Soundings | Ducting % | Avg dN/dh | Avg PWAT (mm) |
|---|---|---|---|---|
| Jan | 95 | 28.4% | −171 | 10.9 |
| Feb | 131 | 29.0% | −163 | 7.9 |
| Mar | 81 | **17.3%** | −148 | 9.4 |
| Apr | 134 | 31.3% | −176 | 13.5 |
| May | 311 | 51.8% | −286 | 25.6 |
| Jun | 380 | **69.5%** | −356 | 32.0 |
| Jul | 302 | 67.5% | −294 | 29.7 |
| Aug | 5,007 | 55.2% | −255 | 35.2 |
| Sep | 2,335 | 56.2% | −280 | 27.2 |
| Oct | 155 | 57.4% | −307 | 19.3 |
| Nov | 158 | 50.0% | −257 | 12.0 |
| Dec | 140 | 25.7% | −185 | 10.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

`hrrr_native_profiles` (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 band | Profiles | % | Avg inversion top (m) | Avg θₑ jump (K) | Avg Bulk Richardson |
|---|---|---|---|---|---|
| <5 GHz | 1,432 | 12.5% | 2,097 | 5.4 | 18.4 |
| 5–15 GHz | 98 | 0.85% | 4,408 | 25.7 | 12.6 |
| 15–30 GHz | 10 | 0.087% | 2,091 | 5.9 | 8.7 |
| 30–75 GHz | 4 | 0.035% | 3,860 | 5.0 | 12.1 |
| None | 9,928 | 86.5% | 11,497 | 80.5 | 38.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. `best_duct_band_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.score_refractivity/5` uses this as a gating condition on the native-duct boost: a `best_duct_band_ghz` 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 is negative at every band tested** (r −0.20 to −0.38). Shallower BL → longer paths. Folded into `Scorer.score_refractivity/4` as a multiplier on the gradient score: 1.10× at <200 m, 1.00× at 200-1000 m, 0.92× at 1000-2000 m, 0.78× at ≥2000 m.

   | HPBL bin (m) | avg km | p50 km |
   |---|---|---|
   | <200 | 229.8 | 176.0 |
   | 200–500 | 186.2 | 154.5 |
   | 500–1000 | 158.9 | 126.0 |
   | 1000–1500 | 162.0 | 136.0 |
   | 1500–2000 | 136.5 | 124.5 |
   | ≥2000 | 100.3 | 90.0 |

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

### 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/recalibrate_algo.py` produces the full correlation matrix + binned distributions as a dated Markdown report under `docs/algo-reports/`. `scripts/derive_band_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 `precip_mm`) and the NEXRAD-derived rain rate (from `max_reflectivity_dbz` via Marshall-Palmer) so either source can trigger the rain penalty without double-counting.

`Scorer.dbz_to_rain_rate_mmhr/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

The base refractivity score uses HRRR's 13 pressure levels, which systematically under-read thin trapping layers (see Part 2c — only 0.12% of native-resolution cells support ≥15 GHz, but the pressure-level data looks the same above and below that threshold). When `hrrr_native_profiles.best_duct_band_ghz` is present and its value is ≥ the target band's frequency, `Scorer.score_refractivity/4` multiplies the base score by 1.15×. A duct that only supports sub-band frequencies does *not* boost — it's evidence that the gradient we have is all there is at the target band.

This composes with the HPBL multiplier: a thin shallow-BL cell with a 24-GHz-supporting native duct gets the full 1.05–1.10× HPBL bonus *and* the 1.15× native-duct bonus, clamped at 100. The boost only applies when the cell's `bulk_richardson` is in the stable regime (< 25) — a low duct-band reading under turbulent conditions (high Richardson) is likely a duct that would be broken up by mechanical mixing.

### 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 `commercial_samples`. These are short (2–6 km) LOS paths where the same refractivity anomaly that helps beyond-LOS amateur propagation degrades the link's rx_power through multipath fading.

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

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

| degradation_db | Bonus | Interpretation |
|---|---|---|
| <3 dB | 0 | Noise floor |
| 3–8 dB | +2 to +10 | Mild 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.

---

## Part 2d: Full-Corpus Per-Band Recalibration

Per-band composite weights are derived from a full-corpus correlation run (`scripts/recalibrate_algo.py` + `scripts/derive_band_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.

| Band | Contacts in DB | HRRR-matched | NARR-matched |
|---|---|---|---|
| 222 MHz | 7,595 | 5,392 | 0 |
| 432 MHz | 9,177 | 6,583 | 0 |
| 902 MHz | 1,794 | 1,317 | 0 |
| 1,296 MHz | 2,996 | 2,146 | 0 |
| 2,304 MHz | 749 | 564 | 0 |
| 3,400 MHz | 351 | 280 | 0 |
| 5,760 MHz | 328 | 246 | 0 |
| 10 GHz | 54,264 | 6,675 | 103 |
| 24 GHz | 3,806 | 613 | 0 |
| 47 GHz | 762 | 53 | 0 |

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) | Temp | Dewpoint | Pressure | PWAT | Surface N | dN/dh | HPBL |
|---|---|---|---|---|---|---|---|
| 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/derive_band_weights.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:

   ```
   s_band,factor = (|r_band| + 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.

3. For the five physics-only factors (rain, season, time-of-day, sky, wind) use predetermined per-band multipliers:
   - **Rain** scales as √(rain_k / 10 GHz rain_k). 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.

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

| Band | humid | tod | td | refr | sky | season | wind | rain | pwat | press |
|---|---|---|---|---|---|---|---|---|---|---|
| **default** | 0.1243 | 0.0496 | 0.0978 | 0.1049 | 0.0800 | 0.1112 | 0.0800 | 0.1362 | 0.1128 | 0.1032 |
| 222 MHz | 0.1593 | 0.0350 | 0.1250 | 0.1401 | 0.0706 | 0.1276 | 0.0706 | **0.0120** | **0.1916** | 0.0681 |
| 432 MHz | **0.2061** | 0.0329 | 0.1200 | 0.1186 | 0.0663 | 0.1106 | 0.0663 | **0.0113** | **0.1870** | 0.0809 |
| 902 MHz | **0.2201** | 0.0437 | 0.1102 | 0.0888 | 0.0783 | 0.1197 | 0.0783 | **0.0133** | **0.1635** | 0.0839 |
| 1,296 MHz | **0.2131** | 0.0494 | 0.0898 | 0.1392 | 0.0797 | 0.1108 | 0.0797 | **0.0136** | 0.1678 | 0.0568 |
| 2,304 MHz | 0.1941 | 0.0387 | 0.1385 | **0.1638** | 0.0625 | 0.0868 | 0.0625 | 0.0336 | 0.1762 | 0.0432 |
| 3,400 MHz | 0.1996 | 0.0398 | 0.1251 | 0.1310 | 0.0642 | 0.0893 | 0.0642 | 0.0489 | 0.1811 | 0.0568 |
| 5,760 MHz | 0.1829 | 0.0423 | 0.1205 | 0.0881 | 0.0683 | 0.0949 | 0.0683 | 0.0822 | **0.1925** | 0.0602 |
| 10 GHz | *defaults (reference band)* | | | | | | | | | |
| 24 GHz | 0.1481 | 0.0532 | **0.0360** | 0.1250 | 0.0477 | 0.0729 | 0.0477 | **0.2147** | 0.1344 | 0.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%).

| Month | Soundings | Ducting % | Avg dN/dh | Avg PWAT (mm) |
|---|---|---|---|---|
| Jan | 95 | 28.4% | −171 | 10.9 |
| Feb | 131 | 29.0% | −163 | 7.9 |
| Mar | 81 | **17.3%** | −148 | 9.4 |
| Apr | 134 | 31.3% | −176 | 13.5 |
| May | 311 | 51.8% | −286 | 25.6 |
| Jun | 380 | **69.5%** | −356 | 32.0 |
| Jul | 302 | 67.5% | −294 | 29.7 |
| Aug | 5,007 | 55.2% | −255 | 35.2 |
| Sep | 2,335 | 56.2% | −280 | 27.2 |
| Oct | 155 | 57.4% | −307 | 19.3 |
| Nov | 158 | 50.0% | −257 | 12.0 |
| Dec | 140 | 25.7% | −185 | 10.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.
- **HPBL is negative at every band we can fit** — preserved. Already folded into refractivity as a multiplier.
- **Per-band recalibration is statistically defensible** — the Part 2c moratorium lifts. 9 bands have n ≥ 200 matched contacts.
- **NARR historical calibration — deferred.** 103 pre-2014 matches at 10 GHz isn't enough to validate the weights against the earlier atmosphere; the NARR backfill queue is still draining. Re-run when `narr_profiles` crosses ~10K rows.
- **Commercial-link sensor calibration — deferred.** Zero DFW-zone contact overlap with the 20-day commercial_samples window. Re-run after the next contest season.

### 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. Moving to native-resolution HRRR (`hrrr_native_profiles.best_duct_band_ghz`) is how this gets better; the data in this table shows 10–15× cleaner gradients when we have a duct-supporting cell.

---

## Part 3: Band Configuration

```elixir
@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).

```elixir
def score_humidity(abs_humidity_gm3, band_config) do
  case band_config.humidity_effect do
    :beneficial ->
      # 10 GHz: more moisture = higher surface N = more beam bending
      # Extreme humidity risks scintillation
      cond do
        abs_humidity_gm3 < 4  -> 55   # Very dry — poor refractivity
        abs_humidity_gm3 < 7  -> 70   # Dry
        abs_humidity_gm3 < 10 -> 82   # Moderate
        abs_humidity_gm3 < 14 -> 90   # Good refractivity
        abs_humidity_gm3 < 18 -> 95   # Excellent refractivity
        abs_humidity_gm3 < 22 -> 88   # High — scintillation onset
        true                  -> 75   # Tropical — scintillation risk
      end

    :harmful ->
      # 24+ GHz: H2O absorption dominates
      # Penalty factor scales by band (1.0 for 47G window, 1.6 for 24G near line, 3.0 for 241G)
      r = abs_humidity_gm3 * band_config.humidity_penalty
      cond do
        r <= 6  -> 100
        r <= 9  -> round(95 - (r - 6) / 3 * 20)
        r <= 13 -> round(75 - (r - 9) / 4 * 30)
        r <= 18 -> round(45 - (r - 13) / 5 * 35)
        true    -> max(0, round(10 - (r - 18) * 2))
      end
  end
end
```

### 2. Time of Day Score — 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:

| Band | UTC Hour rho | Solar Hour rho | Improvement |
|------|-------------|---------------|-------------|
| 10 GHz | 0.007 | 0.016 | 2.4x (still weak — 10G ducts form at all times) |
| 24 GHz | 0.056 | **0.188** | 3.4x (now #5 predictor) |
| 47 GHz | -0.024 | **0.152** | Sign corrected (UTC was confounded by longitude) |
| 75 GHz | -0.392 | **0.239** | Sign 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.

```elixir
@sunrise_table [7.4, 7.3, 7.0, 6.7, 6.35, 6.25,
                6.35, 6.65, 6.9, 7.1, 7.35, 7.45]

def score_time_of_day(utc_hour, utc_minute, month, 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).

```elixir
def score_td_depression(temp_f, dewpoint_f, band_config) do
  dep = temp_f - dewpoint_f

  case band_config.humidity_effect do
    :beneficial ->
      cond do
        dep < 3  -> 40   # Near saturation — fog/scattering risk
        dep < 8  -> 75   # Moist — good refractivity
        dep < 14 -> 85   # Moderate — balanced
        dep < 22 -> 70   # Dry — reduced refractivity
        true     -> 55   # Very dry — poor refractivity
      end

    :harmful ->
      cond do
        dep > 22 -> 96   # Very dry aloft
        dep > 14 -> 80   # Good stability
        dep > 8  -> 60   # Moderate
        dep > 4  -> 38   # Marginal
        true     -> 18   # Humid aloft
      end
  end
end
```

### 4. Sky Cover Score

Data shows modest impact at 24/47 GHz, near-zero at 10 GHz. VV (vertical visibility / fog) is a moderate penalty due to near-surface moisture content. Supports both METAR categories (from ASOS) and percentage sky cover (from IEMRE gridded data).

```elixir
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.

```elixir
def score_season(month, band_config) do
  base = Map.get(band_config.seasonal_base, month, 50)
  adj = Map.get(band_config.seasonal_adj, month, 0)
  max(0, min(100, base + adj))
end
```

### 6. Wind Score — Reduced Weight

Data shows minimal impact on achieved distance. Retain mild penalty only for very high winds (turbulent scintillation).

```elixir
def score_wind(speed_kts) do
  cond do
    speed_kts < 5  -> 100
    speed_kts < 10 -> 90
    speed_kts < 15 -> 75
    speed_kts < 20 -> 55
    speed_kts < 25 -> 35
    true           -> 15
  end
end
```

### 7. Rain Score

Not validated by measured data. Based on ITU-R P.838-3 attenuation per km. At 10 GHz, moderate rain is tolerable. Above 75 GHz, even light rain effectively kills the path.

```elixir
def score_rain(rain_rate_mmhr, band_config) do
  if rain_rate_mmhr == nil or rain_rate_mmhr == 0 do
    100
  else
    gamma = band_config.rain_k * :math.pow(rain_rate_mmhr, band_config.rain_alpha)
    cond do
      gamma < 0.1 -> 95
      gamma < 0.5 -> 75
      gamma < 1.0 -> 50
      gamma < 2.0 -> 25
      gamma < 5.0 -> 10
      true        -> 0
    end
  end
end
```

### 8. Pressure Score — 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.

```elixir
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 Score | Harmful Score | Condition |
|---|---|---|---|
| < -200 | 98 | 85 | Strong ducting (HRRR p1) |
| < -150 | 92 | 80 | Enhanced refraction (HRRR p5) |
| < -100 | 82 | 72 | Above-average refraction (HRRR p25) |
| < -75 | 68 | 62 | Near-median gradient (HRRR p50) |
| < -55 | 55 | 55 | Below-median (HRRR p75) |
| < -40 | 48 | 48 | Weak gradient (HRRR p95) |
| ≥ -40 | 42 | 42 | Standard/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.

```elixir
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 (`hrrr_native_profiles` 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 `T500_c - Td500_c`. Td comes from SPFH via the Magnus inverse (same path as `HrrrNativeProfile.to_skew_t_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 `distance_km` and `composite_score` 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):

| Factor | Weight | Source |
|--------|--------|--------|
| Rain | 13.6% | ITU-R P.838-3 specific attenuation |
| Humidity | 12.4% | Absolute humidity from surface T/Td |
| PWAT | 11.3% | HRRR precipitable water (column-integrated) |
| Season | 11.1% | Per-band monthly lookup tables |
| Refractivity | 10.5% | Native HRRR dM/dh (10–50m), fallback to pressure-level (250m) |
| Pressure | 10.3% | Surface pressure (frontal activity proxy) |
| Td Depression | 9.8% | Surface T minus Td (stability indicator) |
| Sky Cover | 8.0% | HRRR total cloud cover |
| Wind | 8.0% | HRRR 10m wind speed |
| Time of Day | 5.0% | Solar-time adjusted diurnal cycle |

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

| Band | humidity | tod | td | refr | sky | season | wind | rain | pwat | press |
|---|---|---|---|---|---|---|---|---|---|---|
| 222 MHz | 0.1593 | 0.0350 | 0.1250 | 0.1401 | 0.0706 | 0.1276 | 0.0706 | 0.0120 | 0.1916 | 0.0681 |
| 432 MHz | 0.2061 | 0.0329 | 0.1200 | 0.1186 | 0.0663 | 0.1106 | 0.0663 | 0.0113 | 0.1870 | 0.0809 |
| 902 MHz | 0.2201 | 0.0437 | 0.1102 | 0.0888 | 0.0783 | 0.1197 | 0.0783 | 0.0133 | 0.1635 | 0.0839 |
| 1.296 GHz | 0.2131 | 0.0494 | 0.0898 | 0.1392 | 0.0797 | 0.1108 | 0.0797 | 0.0136 | 0.1678 | 0.0568 |
| 2.304 GHz | 0.1941 | 0.0387 | 0.1385 | 0.1638 | 0.0625 | 0.0868 | 0.0625 | 0.0336 | 0.1762 | 0.0432 |
| 3.4 GHz | 0.1996 | 0.0398 | 0.1251 | 0.1310 | 0.0642 | 0.0893 | 0.0642 | 0.0489 | 0.1811 | 0.0568 |
| 5.76 GHz | 0.1829 | 0.0423 | 0.1205 | 0.0881 | 0.0683 | 0.0949 | 0.0683 | 0.0822 | 0.1925 | 0.0602 |
| 10 GHz | *defaults (reference)* | | | | | | | | | |
| 24 GHz | 0.1481 | 0.0532 | 0.0360 | 0.1250 | 0.0477 | 0.0729 | 0.0477 | 0.2147 | 0.1344 | 0.1203 |

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

```elixir
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/recalibrate_algo.py` + `scripts/derive_band_weights.py` whenever the contact or HRRR corpus grows materially — the defaults in this section are a snapshot of the 2026-04-18 local `prop_dev` 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.

| Score | Label | 10G | 24G | 47G | 75G |
|-------|-------|-----|-----|-----|-----|
| 80-100 | EXCELLENT | 400-2000+ km | 200-500 km | 120-300 km | 80-200+ km |
| 65-79 | GOOD | 250-400 km | 120-200 km | 80-120 km | 50-80 km |
| 50-64 | MARGINAL | 150-250 km | 70-120 km | 50-80 km | 30-50 km |
| 33-49 | POOR | 80-150 km | 40-70 km | 25-50 km | 15-30 km |
| 0-32 | NEGLIGIBLE | <80 km | <40 km | <25 km | <15 km |

| Color | Hex |
|-------|-----|
| EXCELLENT | #00ffa3 |
| GOOD | #7dffd4 |
| MARGINAL | #ffe566 |
| POOR | #ff9044 |
| NEGLIGIBLE | #ff4f4f |

---

## Part 6: LOS Regime Scoring

For known fixed links or short paths with confirmed Fresnel clearance. Key difference: sub-refraction is neutral/beneficial (minimal multipath), and gaseous absorption is the primary variable.

### LOS Refractivity Score

```elixir
def score_refractivity_los(dn_dh) do
  cond do
    dn_dh > 0     -> 60   # Strong sub-refraction — unusual but not harmful
    dn_dh > -30   -> 85   # Moderate sub-refraction — stable, clean signal
    dn_dh > -40   -> 75   # Near standard
    dn_dh > -80   -> 60   # Enhanced — multipath onset
    dn_dh > -157  -> 45   # Strong enhancement — multipath likely
    true          -> 30   # Super-refraction — significant multipath fading
  end
end
```

### LOS Surface N Score

Higher N often means more moisture = more absorption at 24+ GHz. Validated by link data: N < 310 gave best 68 GHz signal, N > 340 gave worst.

```elixir
def score_surface_n(n_value, band_config) do
  case band_config.humidity_effect do
    :beneficial ->
      cond do
        n_value > 350 -> 90
        n_value > 330 -> 80
        n_value > 315 -> 65
        n_value > 300 -> 50
        true          -> 35
      end

    :harmful ->
      cond do
        n_value < 300 -> 90
        n_value < 315 -> 80
        n_value < 330 -> 65
        n_value < 345 -> 50
        true          -> 35
      end
  end
end
```

### LOS vs Beyond-LOS Selection

```elixir
def compute_score(conditions, band_config, path_type \\ :beyond_los) do
  base_factors = %{
    humidity: score_humidity(conditions.abs_humidity, band_config),
    wind: score_wind(conditions.wind_speed_kts),
    sky: score_sky(conditions.sky_condition),
    time_of_day: score_time_of_day(conditions.utc_hour, conditions.utc_minute, conditions.month, 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:

```elixir
def duct_enhancement_db(prop_score) do
  cond do
    prop_score >= 80 -> -14   # 14 dB improvement
    prop_score >= 65 -> -10
    prop_score >= 50 -> -6
    prop_score >= 33 -> -2
    true             -> 0
  end
end
```

### Knife-Edge Diffraction (ITU-R P.526-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

```elixir
def margin_to_success(margin_db, prop_score) do
  margin_pct = cond do
    margin_db <= 0  -> 0
    margin_db <= 10 -> margin_db / 10 * 20
    margin_db <= 15 -> 20 + (margin_db - 10) / 5 * 20
    margin_db <= 20 -> 40 + (margin_db - 15) / 5 * 20
    margin_db <= 25 -> 60 + (margin_db - 20) / 5 * 20
    margin_db <= 30 -> 80 + (margin_db - 25) / 5 * 20
    true            -> 100
  end

  # Propagation modulation: score 100 -> x1.30, score 50 -> x1.00, score 0 -> x0.70
  prop_factor = 0.70 + (prop_score / 100) * 0.60
  max(0, min(99, round(margin_pct * prop_factor)))
end
```

### Note: Antenna Height & Duct Coupling Geometry

Antenna height and dish elevation angle affect how efficiently a station couples into an atmospheric duct. This is a real physical effect but is **second-order** to duct characteristics at the ranges this model targets (50-1000+ km).

**Why it's not in the scoring model:**
- At >300 km, the duct's own refractive gradient (k-factor) dominates over all antenna geometry. The required aim angle to graze a duct converges toward 0° regardless of antenna height.
- Antenna height differences in the 15-21m range (typical amateur stations) shift beam geometry by ~0.001° at long range — well within the ±2-3 dB noise floor from diurnal variation.
- The primary benefit of antenna height (50+ ft) is clearing local obstructions and ground clutter in the near field (0-20 km), not geometric coupling to the duct layer.
- VE4MA (50 ft, flat prairie) and W5LUA (70 ft, suburban) achieve similar range classes, confirming duct geometry is the dominant term.

**Where it matters — beamwidth vs frequency:**
At 10 GHz a typical 60cm dish has ~3° beamwidth, making elevation angle errors forgiving. At 24 GHz beamwidth shrinks to ~1.5°, at 47 GHz to <1°. A 0.3° aim error that is irrelevant at 10 GHz becomes a contact killer at 47 GHz. If station profiles (antenna height, dish size, elevation setting) are added in the future, a frequency-dependent **beamwidth coupling penalty** in `margin_to_success` would be the right integration point — penalizing paths where the required aim angle to the detected duct layer exceeds the antenna's half-power beamwidth.

---

## Part 8: Short-Term Prediction Model

### Approach

Extrapolate current conditions forward 1-6 hours using observed trends, diurnal models, and forecast data when available.

### Prediction Confidence

Based on commercial link signal prediction accuracy:

| Horizon | Observed Accuracy | Confidence |
|---------|-------------------|------------|
| Current | +/- 1 dB | 95% |
| +30 min | +/- 1.5 dB | 90% |
| +1 hr | +/- 2 dB | 85% |
| +2 hr | +/- 3 dB | 75% |
| +3 hr | +/- 4 dB | 60% |
| +6 hr | +/- 5 dB | 40% |

### Diurnal Temperature Model

```elixir
def project_temperature(current_temp_f, trend_per_hour, hours_ahead,
                        future_local_hour, month) do
  sunrise = Enum.at(@sunrise_table, month - 1)

  diurnal_rate = cond do
    future_local_hour < sunrise - 1 -> -0.5    # Pre-dawn: slow cooling
    future_local_hour < sunrise + 2 -> 0.0     # Sunrise transition
    future_local_hour < 15          -> 2.0     # Morning: warming
    future_local_hour < 18          -> 0.5     # Late afternoon
    future_local_hour < 21          -> -1.5    # Evening: cooling
    true                            -> -1.0    # Night: slow cooling
  end

  # Blend: current trend dominates short-term, diurnal model dominates long-term
  weight = min(1.0, hours_ahead / 4.0)
  blended_rate = trend_per_hour * (1.0 - weight) + diurnal_rate * weight
  current_temp_f + blended_rate * hours_ahead
end
```

### Prediction Flow

```elixir
def predict_scores(current_obs, obs_3hr_ago, forecast, band_config) do
  temp_trend = (current_obs.temp_f - obs_3hr_ago.temp_f) / 3
  dp_trend = (current_obs.dewpoint_f - obs_3hr_ago.dewpoint_f) / 3
  pressure_trend = (current_obs.slp - obs_3hr_ago.slp) / 3

  for hours_ahead <- 1..6 do
    future_time = DateTime.add(current_obs.observed_at, hours_ahead * 3600)
    month = future_time.month

    projected_temp = project_temperature(current_obs.temp_f, temp_trend,
                       hours_ahead, future_time.hour, month)
    projected_dp = current_obs.dewpoint_f + dp_trend * hours_ahead
    projected_slp = current_obs.slp + pressure_trend * hours_ahead
    projected_sky = forecast_value(forecast, :sky, hours_ahead) || current_obs.sky_condition
    projected_rain = forecast_value(forecast, :rain_rate, hours_ahead) || 0
    projected_wind = forecast_value(forecast, :wind_kts, hours_ahead) || current_obs.wind_speed_kts

    # Compute absolute humidity from projected values
    tc = (projected_temp - 32) * 5 / 9
    td_c = (projected_dp - 32) * 5 / 9
    es = 6.112 * :math.exp(17.67 * tc / (tc + 243.5))
    ed = 6.112 * :math.exp(17.67 * td_c / (td_c + 243.5))
    rh = min(100, ed / es * 100)
    abs_hum = 217 * (rh / 100) * es / (tc + 273.15)

    factors = %{
      humidity: score_humidity(abs_hum, band_config),
      wind: score_wind(projected_wind),
      sky: score_sky(projected_sky),
      time_of_day: score_time_of_day(future_time.hour, future_time.minute, month, 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

```elixir
def compute_refractivity_profile(levels, sfc_height_m) do
  Enum.map(levels, fn level ->
    t_k = level.temp_c + 273.15
    e = 6.1121 * :math.exp((18.678 - level.temp_c / 234.5) * (level.temp_c / (257.14 + level.temp_c)))
    e_actual = if level.dewpoint_c, do: 6.1121 * :math.exp((18.678 - level.dewpoint_c / 234.5) * (level.dewpoint_c / (257.14 + level.dewpoint_c))), else: 0

    n = 77.6 * level.pressure_hpa / t_k + 3.73e5 * e_actual / (t_k * t_k)
    h_agl = level.height_m - sfc_height_m
    m = n + 0.157 * h_agl

    %{height_agl: h_agl, n: n, m: m, temp_c: level.temp_c, dewpoint_c: level.dewpoint_c}
  end)
end
```

### Duct Detection

Duct exists where dM/dh < 0. Filter for strength > 2 M-units.

```elixir
def detect_ducts(profile) do
  profile
  |> Enum.chunk_every(2, 1, :discard)
  |> Enum.reduce({[], nil}, fn [below, above], {ducts, duct_start} ->
    dm = above.m - below.m

    cond do
      dm < 0 and duct_start == nil ->
        {ducts, %{base: below.height_agl, base_m: below.m}}

      dm >= 0 and duct_start != nil ->
        strength = duct_start.base_m - below.m
        if strength > 2 do
          duct = %{base: duct_start.base, top: below.height_agl, strength: strength}
          {[duct | ducts], nil}
        else
          {ducts, nil}
        end

      true ->
        {ducts, duct_start}
    end
  end)
  |> elem(0)
  |> Enum.reverse()
end
```

### Inversion Detection

Temperature increasing with height. Merge adjacent inversions within 200m gap. Filter: strength >= 0.5C, base < 5000m AGL.

### Stability Indices

```
K-Index = (T850 - T500) + Td850 - (T700 - Td700)

Lifted Index = T500 - (Tsfc - (h500 - h_sfc) * 0.00976)
  LI < 0: Unstable (convection likely, inversion destroyed)
  LI > 0: Stable (inversion maintained)

Precipitable Water = sum[(MR_i + MR_{i-1}) / 2 * dP / (9.81 * 10)]
  MR = 622 * e / (P - e)
```

### Boundary Layer Depth

Find height where potential temperature (theta = T + 9.8 * h/1000) exceeds surface theta by 2C. The 500-1000m sweet spot indicates an elevated inversion — high enough to trap signals, not so deep that full mixing has occurred.

---

## Part 10: Band-Specific Propagation Mechanisms

### Coupling Sensitivity by Frequency

Duct coupling geometry becomes increasingly critical at higher frequencies due to narrower antenna beamwidths. A dish aimed 0.3° away from the optimal duct grazing angle:
- **10 GHz** (~3° beamwidth): Still within half-power beam — negligible loss
- **24 GHz** (~1.5° beamwidth): Approaching beam edge — moderate coupling loss
- **47 GHz** (<1° beamwidth): Outside half-power beam — potential contact killer
- **75+ GHz** (<0.5° beamwidth): Precision aim required — elevation error dominates

For surface ducts, the beam must arrive at <0.5° grazing incidence to be trapped. For elevated ducts (500-1500m AGL), the optimal elevation angle is path-distance dependent: slightly positive at close range, near-zero at the "sweet spot" distance, and slightly negative at extreme range due to Earth curvature.

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

| Variable | Description |
|----------|-------------|
| TMP | Temperature (K) |
| SPFH | Specific humidity (kg/kg) |
| HGT | Geopotential height (m) |
| UGRD | U-component wind (m/s) |
| VGRD | V-component wind (m/s) |
| TKE | Turbulent kinetic energy (m²/s²) |
| PRES | Pressure (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 `hrrr_native_profiles` table.

The pure-function `build_native_profile/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** — native_surface_refractivity, bulk_richardson, theta_e_jump, shear_at_top, duct_thickness, best_duct_freq

### 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 `duct_byte_ranges/1`)
2. **Extract**: wgrib2 `-lola` interpolates to the CONUS 0.125° grid (~95k cells)
3. **Reduce**: Cell-by-cell reducer (`compute_duct_metrics/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: `native_min_gradient` (dM/dh minimum from native levels), `best_duct_freq_ghz` (minimum trapped frequency), `max_duct_thickness_m`, `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 `min_refractivity_gradient` 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 = q*P / (0.622 + 0.378*q)`.

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:

| Property | Description |
|----------|-------------|
| base_m | Height (m) of duct base — where M begins decreasing |
| top_m | Height (m) of duct top — where M resumes increasing |
| thickness_m | Duct thickness (top - base) |
| m_deficit | Total 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 min_freq_ghz
- `best_duct_band_ghz` — the lowest min_freq_ghz across all detected ducts (nil if no ducts)

This enables per-band scoring: a duct is "usable" for a given band only if `min_freq_ghz <= band_frequency_ghz`.

---

## 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 Range | Regime | Propagation Impact |
|----------|--------|-------------------|
| < 0.25 | Turbulent | Bad — mixing disrupts duct structure |
| 0.25-1.0 | Transition | Marginal — intermittent ducting |
| > 1.0 | Laminar | Good — 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

| Parameter | Value |
|-----------|-------|
| Product | n0q composite reflectivity |
| Source | IEM archive (`mesonet.agron.iastate.edu`) |
| Format | Palettized 8-bit PNG |
| Dimensions | 12200 x 5400 pixels |
| Coverage | CONUS (-126W to -65W, 23N to 50N) |
| Resolution | 0.005 degrees/pixel |
| Cadence | Every 5 minutes |
| Pixel mapping | Value 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:

- **mean_reflectivity_dbz** — average dBZ of non-zero pixels
- **max_reflectivity_dbz** — 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 `nexrad_texture` feature in the backtest framework uses texture_variance 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
- **`lift_by_distance/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.
- **`lift_by_band/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):**

| Feature | Source | Description |
|---------|--------|-------------|
| naive_gradient | hrrr_profiles | Minimum refractivity gradient (N/km) — current scorer baseline |
| td_depression | hrrr_profiles | Surface T - Td (°C) — atmospheric stability proxy |
| pressure | hrrr_profiles | Surface pressure (hPa) — frontal activity proxy |
| time_of_day | timestamp | UTC hour as float [0, 24) — diurnal baseline |

**Native hybrid-sigma profile (duct/inversion):**

| Feature | Source | Description |
|---------|--------|-------------|
| native_surface_refractivity | hrrr_native_profiles | ITU-R P.453 N from native-level data — sanity check vs pressure-level |
| bulk_richardson | hrrr_native_profiles | Ri at inversion top — laminar (>1) vs turbulent (<0.25) |
| theta_e_jump | hrrr_native_profiles | θₑ jump (K) across inversion — decoupling strength |
| shear_at_top | hrrr_native_profiles | Wind shear (m/s) at inversion top |
| duct_thickness | hrrr_native_profiles | Max duct thickness (m) — larger ducts trap lower frequencies |
| best_duct_freq | hrrr_native_profiles | Lowest trapped frequency (GHz) — lower = stronger ducting |
| ~~duct_usable_10ghz~~ | hrrr_native_profiles | DEAD — no discrimination (always 1.0 for both QSO and baseline) |
| ~~duct_usable_24ghz~~ | hrrr_native_profiles | DEAD — no discrimination (always 1.0) |
| ~~duct_usable_47ghz~~ | hrrr_native_profiles | DEAD — no discrimination (always 1.0) |
| bulk_richardson | hrrr_native_profiles | Dead 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:**

| Feature | Source | Description |
|---------|--------|-------------|
| temperature_anomaly | hrrr_profiles + hrrr_climatology | Surface T minus climatological mean for (grid cell, month, hour) — anomalously hot days produce enhanced ducting |
| nexrad_texture | nexrad_observations | Reflectivity texture variance — convective turbulence proxy |

**Placeholder (not yet implemented):**

| Feature | Source | Description |
|---------|--------|-------------|
| distance_to_front | — | Distance (km) to nearest detected front — awaiting frontal analysis pipeline |
| parallel_to_front | — | cos²(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).

| Feature | QSO N | QSO Mean | QSO p50 | Baseline Mean | Baseline p50 | Signal |
|---|---|---|---|---|---|---|
| **theta_e_jump** | 4915 | 49.4 K | 5.8 K | 34.3 K | 2.1 K | **Strong** — 44% higher jumps during QSOs |
| **best_duct_freq** | 697 | 0.84 GHz | 0.40 GHz | 0.28 GHz | 0.26 GHz | **Strong** — QSO ducts trap lower freqs |
| **native_surface_refractivity** | 4915 | 331.7 | 334.4 | 324.3 | 330.1 | **Moderate** — higher N during QSOs |
| **duct_thickness** | 697 | 156 m | 159 m | 227 m | 217 m | **Inverted** — thinner ducts during QSOs (shallow surface ducts) |
| **td_depression** | 5000 | 6.8°C | 5.8°C | 5.9°C | 3.8°C | **Moderate** — wider depression during QSOs |
| **time_of_day** | 5000 | 16.7h | 17.9h | 11.9h | 11.8h | **Strong** — contests are evening-biased |
| **naive_gradient** | 5000 | -113 | -104 | -107 | -97 | **Weak** — small separation |
| **shear_at_top** | 4915 | 6.4 m/s | 4.0 m/s | 5.8 m/s | 3.3 m/s | **Marginal** |
| **pressure** | 5000 | 983.8 | 989.5 | 982.7 | 989.7 | **Weak** — near-identical |
| **nexrad_texture** | 1796 | 16.8 | 0.0 | 20.6 | 0.0 | **Weak/inverted** |
| bulk_richardson | 4915 | 24.8 | 6.3 | 23.4 | 5.8 | **Dead** — no discrimination |
| duct_usable_*ghz | 697 | ~1.0 | 1.0 | ~1.0 | 1.0 | **Dead** — always 1.0 |
| temperature_anomaly | 0 | — | — | — | — | NO DATA (climatology not built) |

**Key findings:**
- **theta_e_jump** is the single strongest native-level discriminator — large theta-e jumps at inversion tops indicate strong decoupling that traps microwave energy
- **best_duct_freq** 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
- **duct_usable_*ghz** features are dead because nearly all detected ducts are thick enough to trap 10-47 GHz — the threshold is too low to discriminate
- **bulk_richardson** 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.score_refractivity/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 `hrrr_climatology` table stores pre-computed mean and standard deviation of surface temperature from `hrrr_profiles`, 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 `temperature_anomaly` feature returns `surface_temp_c - climatological_mean` as a signed float.

---

## Constants Reference

| Constant | Value | Source |
|----------|-------|--------|
| Earth radius | 6371 km | WGS-84 mean |
| Standard K-factor | 4/3 | Standard atmosphere |
| Standard surface N | 315 | ITU-R P.453 |
| Standard dN/dh | -40 /km | ITU-R P.453 |
| Humidity penalty 24 GHz | 1.6 | Near 22.235 GHz H2O peak |
| Humidity penalty 47 GHz | 1.0 | Atmospheric window |
| Humidity penalty 68 GHz | 1.4 | 60 GHz O2 wing + H2O |
| Humidity penalty 241 GHz | 3.0 | Between H2O 183 & 325 |
| Duct M-unit threshold | 2 | Noise filter |
| Inversion min strength | 0.5C | Below is noise |
| Inversion height limit | 5000m AGL | Above irrelevant |
| BL depth shallow threshold | <300m | HRRR: avg gradient -93.7 at BL<200m |
| Ducting gradient threshold | -300 N/km | Sounding avg for ducting events: -389 |
| Non-ducting gradient avg | -123 N/km | Sounding avg for non-ducting events |
| Ducting surface N threshold | 330 | Above this, ducting probability >50% |
| Signal prediction floor | +/- 2-3 dB | Measured from link data |
| CW bandwidth advantage | ~7 dB | 10*log10(2700/500); 16-221% range increase depending on band |
| Pressure correlation (10 GHz) | rho=-0.180 | 57,248 QSO-HRRR analysis, Apr 2026 |
| PWAT optimal range (10 GHz) | 20-30 mm | Best median distance (219 km) |
| PWAT correlation (75 GHz) | rho=-0.608 | 57,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).

| Recommendation | Title | Used For |
|----------------|-------|----------|
| ITU-R P.453-14 | The radio refractive index: its formula and refractivity data | Surface refractivity N calculation, refractivity gradient |
| ITU-R P.525-4 | Calculation of free-space attenuation | Free-space path loss baseline |
| ITU-R P.676-13 | Attenuation by atmospheric gases and related effects | O2 and H2O absorption coefficients per band |
| ITU-R P.838-3 | Specific attenuation model for rain for use in prediction methods | Rain attenuation coefficients (k, alpha) per band |
| ITU-R P.526-16 | Propagation by diffraction | Knife-edge loss (Eq. 31), Deygout 3-edge method (Section 6), dynamic k-factor (Section 2) |
| ITU-R P.452-17 | Prediction procedure for the evaluation of interference between stations on the surface of the Earth | Clear-air propagation modeling framework |
| ITU-R P.835-6 | Reference standard atmospheres | Standard atmosphere profiles for baseline |
| ITU-R P.530-18 | Propagation data and prediction methods for terrestrial line-of-sight systems | Multipath 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 (rx_power_0, rx_power_1 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/Kp_ap_Ap_SN_F107_since_1932.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 (wx_codes)**: Stored in surface_observations 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.
