- Replace deprecated xref:exclude with elixirc_options in vendor/oban_pro
- Remove unused require Logger from 8 files
- Pin bitstring size variables with ^ in simple_packing, wgrib2,
complex_packing, section, nexrad_client, mqtt, and radar worker
- Remove dead defp clauses (format/1 nil, depression/2 nil)
- Rename Buildings.Parser type record/0 -> building_record/0 to avoid
overriding built-in type
- Remove redundant catch-all in candidate_detail.ex
- Simplify contact_live/show.ex conditional based on type narrowing
- Fix DateTime.from_iso8601 return pattern in vendor/oban_web
- Upgrade phoenix_live_view to 1.1.31
- Drop --warnings-as-errors from precommit alias; known false positives
from @after_verify-generated code in Elixir 1.20.0-rc.6 + PLV 1.1.x
- Add credo --strict to precommit as replacement static-analysis gate
`mix prop.compare` runs both scorers against a recent contact sample
and measures both against achieved contact distance. Each run appends
to priv/calibration/history.jsonl, so trends in alg-ML divergence and
algorithm-distance correlation surface over weeks of running.
When drift exceeds threshold (alg-ML RMSE > 8 score points, or current
correlation > 0.10 below the history median), the task writes a
recommendation pointing at the right remediation:
mix recalibrate_scorer # algorithm drift → refit weights
mix propagation_train # ML drift → retrain on the new algorithm
That is the feedback loop: measure → recommend → recalibrate/retrain →
loop. Operational rather than automatic, so a bad data window can't
silently corrupt the model.
Pure analysis lives in Microwaveprop.Propagation.Calibration with its
own unit tests; the Mix task only handles data loading, file I/O, and
console formatting.
Retrained on 60k month-balanced HRRR profiles through 2026-04-05
(test RMSE 1.7 score points, R² 0.97). Added explain_prediction/4
returning ranked feature contributions via batched finite-difference
attribution, so the UI can show users which weather factors drove
each prediction.
Final coverage round: 84.44% → 86.18% (target was 85%).
Two lib_ml/ tasks were stranded at 0% (1035 combined lines) because
the qsos → contacts table rename left stale refs in
PropagationAnalyze's SQL. Fixed:
- `FROM qsos q` → `FROM contacts q`
- `terrain_profiles tp ON tp.qso_id = q.id` → `tp.contact_id = q.id`
Adds test/mix/tasks/propagation_ml_tasks_test.exs with 4 tests:
- PropagationAnalyze end-to-end against empty DB (walks every section
header: correlation, binned factor, interaction, close).
- PropagationAnalyze with seeded contact + matching HRRR at both
endpoints yields a 1-row dataset (exercises derive_averages +
format_band + median + percentile helpers).
- PropagationAnalyze with a pre-2016-06-30 contact is excluded by the
WHERE clause.
- PropagationTrain on empty hrrr_profiles raises the expected Nx
"cannot build empty tensor" error after walking header + load path.
215 → 221 properties, 2812 → 2846 tests, 0 failures.
57,186 prod contacts stored pos1/pos2 with 'lng'; 1,133 used 'lon'.
Every Elixir caller carried a `pos["lon"] || pos["lng"]` fallback
— which just caused a SQL widget to silently miscount 98% of contacts
(count_narr_done used `pos1->>'lon'` directly, no fallback, so every
lng-keyed row returned NULL and failed the coverage check).
- Migration rewrites every pos1/pos2 JSONB in place, renaming 'lng' to
'lon' and dropping 'lng'.
- Removes all 20+ `|| pos["lng"]` fallbacks across lib/, workers,
scorer, weather, radio.ex, contact show view, and recalibrator.
- lib_ml/propagation_analyze.ex SQL now reads pos1->>'lon' directly
(was reading 'lng' only, which would have broken after migration).
- priv/repo/import_contacts.exs one-time seed script now emits 'lon'
with string keys, matching production shape.
- Test fixtures in 4 test files normalized to 'lon'.
- Two lng-characterization tests deleted — nonsensical post-normalize.
- Updated notebook + old import_weather script to match.
- JS hook contact_map_hook.ts TypeScript type narrowed to 'lon'.
Replaces the two-phase (algorithm-scores pretrain + QSO-distance
finetune) pipeline with a single unbiased phase that samples
hrrr_profiles stratified uniformly by calendar month and trains on the
physical algorithm score from Scorer.composite_score/2.
Previous Phase 2 target was 'within-band distance percentile' computed
from the contacts table. Because non-contest months have almost no
QSOs, the model learned 'these conditions → QSO happened → good' and
implicitly 'no QSO → bad', so it systematically under-predicted
propagation during quiet months (Feb, Mar, Nov). The new pipeline
decouples the target from QSO activity entirely.
Stratification uses ROW_NUMBER() OVER (PARTITION BY month ORDER BY
random()) so every month contributes the same number of profile rows
regardless of contest-driven density. Each profile is exploded into
one training row per band.
Trained model: val RMSE=1.66 pts, R²=0.9744 on 840k rows (10k
profiles/month × 7 bands, 50 epochs). Monthly sanity check at fixed
weather conditions (10 GHz, 15°C/10°C) now shows Jan=Feb=Mar=68 — the
model no longer penalizes quiet months.
- Nx, Axon, EXLA, Polaris deps restricted to only: [:dev, :test]
- model.ex and training mix tasks moved to lib_ml/ (compiled via
elixirc_paths in dev/test only)
- load_ml_model uses Code.ensure_loaded? + apply/3 to avoid
compile-time references to ML modules in production
- Verified: MIX_ENV=prod compiles clean with no ML warnings