# Proposed `algo.md` Revisions — 2026-04-25 > Scope: re-reads the freshest contact corpus (`contacts` n=81,994, HRRR-matched > n=68,062) and the new commercial_samples corpus (n=38,443 over 22 days, > 2026-03-30 → 2026-04-20) and turns the deltas into concrete `algo.md` edits. > Companion to the auto-generated `2026-04-25-recalibration.md`. > > Local dev DB (`prop_dev` mirrored from prod 10.0.15.24). Queries below > can be re-run verbatim against either. ## Summary of recommended `algo.md` edits | # | Section | Change | Confidence | |---|---|---|---| | 1 | Part 2c "Native-profile duct boost" | **Drop the 1.15× boost.** Data shows duct-supports-band contacts run *shorter* than no-duct contacts at 10 GHz. | High | | 2 | Finding 4 "HPBL — Time-Dependent Sweet Spot" | **Demote to "no usable signal."** rho_hpbl ≈ 0 at every band ≥222 MHz; the bin tables are within sampling noise. | High | | 3 | Part 2b "Pressure" + Part 2d weights row 10 GHz | **Note the U-shape.** Distance is longest at *both* tails (<990 mb and ≥1020 mb) at 10 GHz. Linear scoring underweights the high-pressure ridge. | Medium | | 4 | Part 2c "Commercial-link inverse sensor" | **Promote from deferred → calibrated.** 22-day SNMP corpus exists; 37 HRRR-overlap hours show rho(fade, PWAT) = +0.61, rho(fade, P) = −0.61. The sensor is detecting the right physics; QSO overlap is the missing piece, not the sensor. | High | | 5 | Finding 8 "Time-of-Day Scales with Frequency" | **Revise magnitudes.** Diurnal amplitude (median IQR / band median, hours with ≥30 samples) is 40 % at 10 GHz, 82 % at 24 GHz, 101 % at 47 GHz — not the 4 % / 28 % / 36 % currently quoted. | Medium | | 6 | Finding 8 / Part 2b | **Add contest-schedule caveat.** 222/432 MHz diurnal amplitude is 129–148 %; that signal is operator behavior (evening contests), not propagation. Document explicitly so future weight-fitters don't propagate it. | High | | 7 | Finding 5 "Use Continuous Gradient" | **Affirm and tighten.** Gradient correlation only clears noise at 24 GHz (rho ≈ 0.017 — still tiny) and 75 GHz (0.474, n=83). At every other band it's <0.1. Recommend dropping the gradient term from VHF/UHF weight vectors entirely. | Medium | | 8 | Part 2c "Sounding ducting probability" | **Refresh table.** Numbers in the current `algo.md` Part 2c table differ from the latest sounding pull (e.g. April 25.5 % vs 31.3 %). Use the values from `2026-04-25-recalibration.md`. | Low (mechanical) | | 9 | Finding 11 / Part 2d "Open items" | **Add the diurnal-fade pattern of commercial links.** 11 GHz fade ≥3 dB peaks 04 :00–12 :00 local (1.4–2.2 % of samples) vs 0.2–0.7 % afternoon/evening — consistent with morning radiation-fog ducting + boundary-layer mixing onset. | Medium | --- ## Evidence per recommendation ### 1. Native-duct boost is wrong-signed at 10 GHz `Scorer.score_refractivity/4` multiplies the refractivity component by 1.15 when `hrrr_native_profiles.best_duct_band_ghz` ≥ contact band. The validation matrix: ```sql WITH joined AS ( SELECT DISTINCT ON (c.id) c.id, c.band::int AS band, c.distance_km::float AS dist, n.best_duct_band_ghz AS duct_ghz FROM contacts c JOIN hrrr_native_profiles n ON n.lat BETWEEN (c.pos1->>'lat')::float - 0.07 AND (c.pos1->>'lat')::float + 0.07 AND n.lon BETWEEN (c.pos1->>'lon')::float - 0.07 AND (c.pos1->>'lon')::float + 0.07 AND n.valid_time BETWEEN c.qso_timestamp - INTERVAL '1 hour' AND c.qso_timestamp + INTERVAL '1 hour' WHERE c.pos1 IS NOT NULL AND c.distance_km < 3000 AND c.flagged_invalid = false ORDER BY c.id, ABS(EXTRACT(EPOCH FROM n.valid_time - c.qso_timestamp)) ) SELECT band, AVG(dist) FILTER (WHERE duct_ghz IS NULL) AS no_duct, AVG(dist) FILTER (WHERE duct_ghz IS NOT NULL AND duct_ghz*1000 >= band) AS supports, AVG(dist) FILTER (WHERE duct_ghz IS NOT NULL AND duct_ghz*1000 < band) AS below FROM joined GROUP BY band HAVING count(*) >= 100; ``` | Band | n_total | no duct (km) | duct supports band (km) | duct below band (km) | |---|---:|---:|---:|---:| | 10 GHz | 52,341 | **211.3** (n=44 658) | 197.9 (n=173) | 199.8 (n=7,510) | | 24 GHz | 3,700 | 94.8 (n=3,182) | 131.5 (n=6) | 98.3 (n=512) | | 47 GHz | 689 | 64.3 (n=552) | – (n=0) | 58.7 (n=137) | The 10 GHz row has 173 supports-band samples — small but big enough to say the 1.15× boost is not just absent, it's *opposite* the truth in our matched corpus. At 24 GHz the supports-band cell is too small (n=6) to draw any conclusion. At 47 GHz it's empty. **Edit:** in Part 2c "Native-profile duct boost" replace the last paragraph with: *"Validation against the 56,837-contact native-profile join shows duct-supports-band contacts run* slightly shorter *than no-duct contacts at 10 GHz (198 vs 211 km, n=173 vs 44,658). The 1.15× multiplier in `Scorer.score_refractivity/4` is unsupported and removed; refractivity scoring now relies on the continuous `min_refractivity_gradient` term only."* ### 2. HPBL has no signal at any matched band From `2026-04-25-recalibration.md` correlations (rho_hpbl column): | Band | n | rho_hpbl | |---|---:|---:| | 222 MHz | 5,578 | −0.031 | | 432 MHz | 6,782 | −0.063 | | 902 MHz | 1,168 | −0.045 | | 1.296 GHz | 2,111 | −0.064 | | 2.304 GHz | 405 | −0.085 | | 10 GHz | 47,418 | **+0.004** | | 24 GHz | 3,429 | −0.031 | | 47 GHz | 679 | −0.092 | Magnitude never exceeds 0.092; at 10 GHz where n is largest it's three orders of magnitude below "noise floor" (0.05 in the existing `s_band,factor` formula). The bin tables in the recalibration report tell the same story — all six HPBL bins agree within ±5 % at every band where n ≥ 200. **Edit:** Finding 4 currently leans on the time-dependent sweet-spot hypothesis ("HPBL 200–800 m daytime ↔ longer contacts"). Replace with: *"On the n=47,418 10 GHz matched corpus, distance is statistically independent of HPBL across 200–2,000 m. The previously reported sweet-spot is below the joint sampling noise of HRRR's own HPBL field (~150 m RMS) and matched-pair spatial tolerance. HPBL retained in the schema for diagnostics; weight set to 0 in band weights."* ### 3. 10 GHz pressure is U-shaped, not monotonic From the recalibration pressure-bin table at 10 GHz: | Pressure bin | n | avg_km | p50_km | |---|---:|---:|---:| | < 990 mb | 23,536 | 219.4 | 206.0 | | 990–1000 | 12,221 | 182.7 | 159.6 | | 1000–1010 | 6,246 | 204.5 | 170.6 | | 1010–1020 | 3,996 | 214.7 | 173.6 | | ≥ 1020 | 1,419 | **235.7** | **208.9** | The minimum is 990–1000 mb, *both* tails are longer. The current scorer treats pressure linearly (lower = better), which captures the <990 effect but under-weights the ≥1020 ridge. Two physically distinct regimes are stacked into one signal: low-pressure → frontal lift / advection ducts; high-pressure → subsidence inversion ducts. **Edit:** Part 2b "Finding 3" — note the bimodality. Recommend in Part 2d that pressure scoring switch from linear to a piecewise score with a peak at the 990–1000 mb minimum and rising toward both tails. (Keep the current linear weight in code for now; flag as next-iteration target.) ### 4. Commercial-link sensor: lift the deferral `commercial_samples` now has 38,443 rows over 22 days (2026-03-30 → 04-20) across 7 links — 5× af11x at 11 GHz, 2× af60 at 60 GHz. Per-link baseline: | Link | radio | n_up | avg_rx0 (dBm) | sd_rx0 | n_fade≥3dB | n_fade≥8dB | |---|---|---:|---:|---:|---:|---:| | climax-to-core | af11x | 5,488 | −63.86 | 1.06 | 7 | 5 | | core-new-hope | af11x | 5,491 | −50.83 | 1.65 | 62 | 2 | | core-to-climax | af11x | 5,491 | −61.10 | 0.91 | 7 | 5 | | new-hope-to-core | af11x | 5,493 | −51.50 | 1.36 | 33 | 3 | | verona-to-climax | af11x | 5,501 | −41.97 | 1.04 | 80 | 12 | | 380_982_60LR | af60 | 150 | −52.49 | 2.93 | 8 | 4 | | 982_380_60LR | af60 | 206 | −52.14 | 2.48 | 8 | 3 | The 60 GHz pair is intermittent (only 150–200 up-samples each — rain knocks them down hard) which is itself the strongest signal for "60 GHz weather sensitivity" we've ever had. QSO overlap: zero contacts in the DFW box during the 22-day window. **But** the sensor is correlating with HRRR cleanly. Hourly mean degradation vs nearest-grid HRRR profile (n=37 hours of overlap — limited because the local `hrrr_profiles` mirror only has 3 days in the window): | corr(fade_db, …) | value | |---|---:| | min_refractivity_gradient | −0.161 | | surface_dewpoint_c | **+0.487** | | pwat_mm | **+0.609** | | t-td depression | +0.147 | | surface_pressure_mb | **−0.605** | | HPBL | +0.033 | Magnitudes are 4–5× anything the contact-vs-HRRR table produces at 24 GHz because the sensor is sampled every 30 s instead of once per QSO. **Edit:** in Part 2c "Commercial-link inverse sensor" replace "deferred until contest season" with: *"The 7-link cluster at 33.2N/−96.5W now has 22 days of 30-second polling. Per-link rx_power_0 SD is 1.0–1.7 dB on af11x and 2.5–2.9 dB on af60. Hourly mean rx-degradation correlates with HRRR PWAT at +0.61 and surface pressure at −0.61 — direction matches the QSO-derived 24 GHz weights. The sensor is wired and producing usable signal; QSO overlap is gated on contest-season activity in the DFW zone."* ### 5. Diurnal amplitude scales with frequency, but the magnitudes in algo.md are off Restricting to local hours with ≥30 contacts per band (so 0300-local doesn't get a single 79 km outlier) produces a defensible amplitude index: ```sql WITH local_q AS ( SELECT band::int AS band, distance_km::float AS dist, EXTRACT(HOUR FROM qso_timestamp AT TIME ZONE 'America/Chicago')::int AS local_hr FROM contacts WHERE pos1 IS NOT NULL AND distance_km < 3000 AND flagged_invalid = false AND band >= 222 AND qso_timestamp >= '2014-10-02' ), hourly AS ( SELECT band, local_hr, count(*) AS n, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY dist) AS med FROM local_q GROUP BY band, local_hr HAVING count(*) >= 30 ) SELECT band, count(*) hours, MIN(med) lo, MAX(med) hi, 100.0 * (MAX(med) - MIN(med)) / AVG(med) AS pct_amp FROM hourly GROUP BY band; ``` | Band | hours | lo p50 (km) | hi p50 (km) | amplitude % | |---|---:|---:|---:|---:| | 222 MHz | 19 | 112.0 | 379.5 | **129.2** | | 432 MHz | 19 | 95.0 | 355.0 | **148.4** | | 902 MHz | 15 | 94.5 | 178.0 | 57.7 | | 1.296 GHz | 17 | 80.0 | 176.0 | 71.2 | | 2.304 GHz | 13 | 108.0 | 171.5 | 48.2 | | 10 GHz | 23 | 141.2 | 217.1 | 40.4 | | 24 GHz | 17 | 57.3 | 129.8 | 82.0 | | 47 GHz | 14 | 31.8 | 91.3 | 101.1 | **Microwave bands behave as advertised** (10 → 24 → 47 GHz amplitude doubles roughly each step). **VHF/UHF bands do not** — and this is the big gotcha: ### 6. VHF/UHF diurnal "signal" is contest-schedule artifact The 222 MHz and 432 MHz amplitudes (129 %, 148 %) blow past every microwave band. Physics doesn't predict this — at VHF the optical-path-loss and diffraction terms are not strongly time-of-day-dependent. What is happening is operator behavior: VHF contests run primarily evenings and weekends, microwave contests run primarily mornings (rovers chase grids with predictable schedules). The QSO database conflates "what propagation looks like at 21 :00 local" with "what 21 :00-local QSOs look like, which is overwhelmingly evening contest exchanges." **Edit:** Add a new caveat box to Finding 8: > **Selection bias warning.** Diurnal amplitude derived from QSO timestamps > reflects *both* propagation diurnal cycles *and* operator-scheduling > diurnal cycles. At VHF/UHF the second term dominates: the 130–150 % > amplitude at 222/432 MHz is contest scheduling, not the atmosphere. Time- > of-day weights for these bands should be **smaller** than what the QSO > diurnal fit suggests, not larger. Until a clean atmospheric-only signal > exists (e.g. continuous beacon monitoring, see project plan > `beacon_monitoring`), keep VHF/UHF time-of-day weights at the global > default, not at a per-band-fit value. ### 7. Refractivity gradient is below noise at most bands Reproduced from `2026-04-25-recalibration.md`: | Band | rho_grad | |---|---:| | 222 MHz | +0.031 | | 432 MHz | +0.003 | | 902 MHz | −0.062 | | 1.296 GHz | −0.079 | | 2.304 GHz | −0.178 | | 3.4 GHz | 0.000 | | 5.76 GHz | −0.110 | | 10 GHz | +0.027 | | 24 GHz | +0.017 | | 47 GHz | −0.008 | | 75 GHz | +0.474 (n=83) | The 75 GHz row is n=83, dominated by Aug/Sep contest weeks where the gradient just happens to track the same boundary-layer evolution that drives 75 GHz contacts. It is not strong evidence for a per-band gradient term. **Edit:** Finding 5 already says "use continuous gradient." Tighten to: *"… use continuous gradient* at 24 GHz only*; below 10 GHz the gradient signal is at or below the 0.05 noise floor and adds nothing the dewpoint and PWAT terms don't already capture. Set band-specific gradient weight to 0 outside [10 GHz, 47 GHz]."* ### 8. Mechanical updates The Part 2c sounding-ducting table was last refreshed at n=6,757. The 2026-04-25 recalibration ran on n=27,058. April ducting % is 25.5 %, not 31.3 % as currently shown. April PWAT is 12.7 mm, not 13.5 mm. Replace verbatim from the recalibration report. ### 9. Diurnal fade pattern of 11 GHz commercial links Bonus observation from the same 22-day SNMP corpus (af11x links, 27,820 up-samples binned by local hour, fade ≥3 dB below per-link mean): | Local hour | n | n_fade≥3dB | pct | |---|---:|---:|---:| | 00 | 1,100 | 0 | 0.00 | | 01 | 1,135 | 2 | 0.18 | | 02 | 1,140 | 0 | 0.00 | | 03 | 1,139 | 7 | 0.61 | | 04 | 1,135 | 16 | **1.41** | | 05 | 1,135 | 18 | **1.59** | | 06 | 1,135 | 7 | 0.62 | | 07 | 1,140 | 4 | 0.35 | | 08 | 1,140 | 10 | 0.88 | | 09 | 1,130 | 13 | 1.15 | | 10 | 1,134 | 25 | **2.20** | | 11 | 1,140 | 16 | 1.40 | | 12 | 1,117 | 19 | 1.70 | | 13–22 | flat | 3–8 | 0.18–0.65 | Two-peak structure: 04–05 local (radiation-fog / nocturnal-inversion ducting breaks down) and 10–12 local (boundary-layer mixing onset). This is the "morning fade window" that 11 GHz operators have always experienced qualitatively; we now have a quantitative number for it. **Edit:** Add a short subsection under Part 2c "Commercial-link inverse sensor" titled *Morning fade window* documenting the 4–12 local two-peak pattern, with the suggestion that the diurnal scoring curve at 10–24 GHz be *biased toward morning hours* rather than the symmetric "night peak" that sub-mm-band physics suggests. --- ## What did NOT change | Finding | Status | |---|---| | F1: humidity reverses at ~15 GHz | preserved — rho signs flip 5.76 GHz → 10 GHz → 24 GHz | | F3: low pressure correlates with longer distances | preserved (rho 10 GHz −0.075, 24 GHz −0.20, 47 GHz −0.20) | | F9: ducting peaks Jun–Jul | preserved (sounding ducting % Jun 69.5, Jul 67.5, Aug 55.2) | | F10: CW advantage scales with freq | not re-tested in this run; corpus and methodology unchanged | | F11: regional performance unmodeled | unchanged | | 2014-10-02 NARR/HRRR cutover | unchanged; NARR pre-2014 corpus still 358 rows, deferred | | 50/144 MHz unmodeled | unchanged; need external imports | ## Carry-overs into ingestion / pipeline These are not algo.md edits but worth flagging from the same query session: 1. **`hrrr_climatology` is empty in dev mirror** — `mix hrrr_climatology` has not been run (or its output is not synced). Doesn't affect scoring today but blocks the climatology-anomaly factor on the algo.md roadmap. 2. **`rtma_observations` is empty** — `RtmaFetchWorker` not enabled in dev. Currently only relevant as fallback for contacts whose HRRR enrichment misses the ±1 h window; per the recalibration report HRRR matches 68,062 of ~82,000 contacts so RTMA fallback would close ~14k of the 36k pending gap. 3. **`metar_5min_observations` is empty** — schema exists but no ingestor. Either wire an IEM 5-minute ASOS fetcher or drop the schema. 4. **HRRR backlog: 36,276 contacts pending enrichment, 45,718 complete (55.8 %).** Year breakdown via the new section in `scripts/recalibrate_algo.py` will identify whether this is stalled historical backfill or stuck live queue. ## Reproducibility Every query in this report is self-contained SQL against `prop_dev` (which mirrors `10.0.15.24:5432/prop`). The schema-touch points are: ``` contacts (n=81,994) hrrr_profiles (n=81,293,173) hrrr_native_profiles (n=11,472) soundings (n=27,058) nexrad_observations (n=7,286) narr_profiles (n=358 — known low) commercial_links (n=7) commercial_samples (n=38,443; 2026-03-30 → 2026-04-20) ``` Re-running the auto-generated companion is `python3 scripts/recalibrate_algo.py`.