prop/test/microwaveprop/weather/ncei_metar_client_test.exs
Graham McIntire e7a7ae073d Phase 9.3, 9.4, and Phase 3 NEXRAD pipeline
Task 9.3 - Weight recalibration via gradient descent:
- Recalibrator module fits logistic regression weights using Nx
- Trains on QSO positives vs random baseline negatives
- Cross-validates by month, normalizes weights to sum to 1.0
- Mix task: mix recalibrate_scorer --sample 5000 --epochs 2000

Task 9.4 - Side-by-side scorer comparison:
- ScorerDiff.compare/3 re-scores grid with old vs new weights
- Reports mean diff, regressions, improvements, per-band breakdown
- Mix task: mix scorer_diff --new-weights '{...}'

Phase 3 - NEXRAD ingestion pipeline:
- NexradClient fetches IEM n0q composite PNGs, extracts per-point
  box statistics (mean/max dBZ, texture variance)
- NexradObservation schema with unique (lat, lon, observed_at)
- NexradWorker on :nexrad queue for background processing
- nexrad_texture backtest feature in Features module
- mix nexrad_backfill --limit 200

All tasks added to AdminTaskWorker and Release for production use.
1116 tests, 0 failures.
2026-04-10 12:48:36 -05:00

51 lines
1.9 KiB
Elixir

defmodule Microwaveprop.Weather.NceiMetarClientTest do
use ExUnit.Case, async: true
alias Microwaveprop.Weather.NceiMetarClient
@sample_line "03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094"
describe "parse/1" do
test "parses a single KDFW observation" do
[obs] = NceiMetarClient.parse(@sample_line)
assert obs.icao == "KDFW"
assert obs.observed_at == ~U[2026-03-01 00:00:00Z]
# T02110094 → 21.1°C = 70.0°F, 9.4°C = 48.9°F
assert_in_delta obs.temp_f, 70.0, 0.2
assert_in_delta obs.dewpoint_f, 48.9, 0.2
assert obs.wind_speed_kts == 12.0
assert obs.wind_direction_deg == 170
assert_in_delta obs.altimeter_setting, 29.97, 0.01
assert obs.sky_condition == "CLR"
end
test "parses multiple lines" do
text = """
03927KDFW DFW20260301000010303/01/26 00:00:31 5-MIN KDFW 010600Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094
03927KDFW DFW20260301000510303/01/26 00:05:31 5-MIN KDFW 010605Z 17012KT 10SM CLR 21/09 A2997 560 47 1400 160/12 RMK AO2 T02110094
"""
obs = NceiMetarClient.parse(text)
assert length(obs) == 2
assert Enum.at(obs, 0).observed_at == ~U[2026-03-01 00:00:00Z]
assert Enum.at(obs, 1).observed_at == ~U[2026-03-01 00:05:00Z]
end
test "skips blank or too-short lines" do
assert NceiMetarClient.parse("") == []
assert NceiMetarClient.parse("short\n\n") == []
end
test "handles negative temperatures via M prefix" do
line =
"03927KDFW DFW20260115120010103/15/26 12:00:31 5-MIN KDFW 151200Z 36005KT 10SM CLR M02/M08 A3032 560 47 1400 360/05 RMK AO2"
[obs] = NceiMetarClient.parse(line)
# M02 = -2°C = 28.4°F, M08 = -8°C = 17.6°F
assert_in_delta obs.temp_f, 28.4, 0.2
assert_in_delta obs.dewpoint_f, 17.6, 0.2
end
end
end