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.
51 lines
1.9 KiB
Elixir
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
|