prop/test/microwaveprop/pskr/recalibrator_test.exs
Graham McIntire a0065e2f44
refactor: deduplicate hrrr download, simplify radio/mqtt, fix credo/perf issues
- hrrr_client: extract shared fetch_grib_ranges/2 to eliminate ~80% duplicated code
- mqtt: encode_varint/1 uses IO list accumulator instead of O(n^2) binary concat
- commercial: single-pass reduce for aggregate_degradation, remove unused average/1
- scorer: single-pass reduce for safe_avg and build_path_conditions
- terrain_analysis: split 76-line do_analyse/4 into 3 focused functions
- radio: build_position_changes/3 simplified from then-chains to Enum.reduce
- recalibrator_test: fix credo warning (length > 0 -> != [])
2026-05-07 11:50:29 -05:00

163 lines
5.3 KiB
Elixir

defmodule Microwaveprop.Pskr.RecalibratorTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Pskr.CalibrationSample
alias Microwaveprop.Pskr.FeatureBin
alias Microwaveprop.Pskr.RecalibrationRun
alias Microwaveprop.Pskr.Recalibrator
alias Microwaveprop.Repo
defp insert_sample!(attrs) do
base = %{
hour_utc: ~U[2026-05-04 18:00:00Z],
band: "10000",
midpoint_lat: 33.0,
midpoint_lon: -97.0,
spot_count: 5,
pwat_mm: 30.0,
hpbl_m: 500.0,
min_refractivity_gradient: -100.0,
surface_pressure_mb: 1013.0,
kp_index: 2
}
{:ok, _} =
%CalibrationSample{}
|> CalibrationSample.changeset(Map.merge(base, attrs))
|> Repo.insert()
end
# Insert N samples for a band, optionally varying one feature.
# Uses a per-call unique offset so successive `insert_n!` invocations
# don't collide on the (hour, band, midpoint_lat, midpoint_lon) key.
defp insert_n!(n, band, attrs_fn) do
offset = :erlang.unique_integer([:positive])
Enum.each(1..n, fn i ->
attrs = attrs_fn.(i)
mp_lat = 33.0 + (offset * 1000 + i) * 0.0001
mp_lon = -97.0 - (offset * 1000 + i) * 0.0001
insert_sample!(Map.merge(attrs, %{band: band, midpoint_lat: mp_lat, midpoint_lon: mp_lon}))
end)
end
describe "run/0" do
test "skips with status `skipped_insufficient_data` when corpus is empty" do
run = Recalibrator.run()
assert run.status == "skipped_insufficient_data"
assert run.sample_count == 0
assert run.notes =~ "≥ 1000"
assert Repo.aggregate(FeatureBin, :count) == 0
end
test "skips when corpus has < 1000 total samples" do
insert_n!(500, "10000", fn _ -> %{} end)
run = Recalibrator.run()
assert run.status == "skipped_insufficient_data"
assert run.sample_count == 500
assert Repo.aggregate(FeatureBin, :count) == 0
end
test "completes and writes feature bins when corpus is dense enough" do
# Spread samples across PWAT bins so several have non-zero counts.
insert_n!(1200, "10000", fn i ->
pwat = 10.0 + rem(i, 60)
%{pwat_mm: pwat, spot_count: rem(i, 10) + 1}
end)
run = Recalibrator.run()
assert run.status == "completed"
assert run.sample_count == 1200
assert run.band_count == 1
assert run.avg_spots_per_sample > 0
bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id))
assert bins != []
# Every bin row should reference the run, target this band, and
# carry non-negative stats.
Enum.each(bins, fn bin ->
assert bin.run_id == run.id
assert bin.band == "10000"
assert bin.sample_count > 0
assert bin.spot_count_total >= 0
end)
# PWAT should populate every defined bin label since we
# spread values 10-69 across the range.
pwat_bins =
bins
|> Enum.filter(&(&1.feature == "pwat_mm"))
|> Enum.map(& &1.bin_label)
|> Enum.sort()
assert "<15" in pwat_bins
assert "15-25" in pwat_bins
assert "25-40" in pwat_bins
assert "40-55" in pwat_bins
assert ">55" in pwat_bins
end
test "skips bands below the per-band threshold even when total corpus passes" do
# Band A gets enough samples; band B (50 samples) is too thin
# to bin even though the total corpus passes the global
# threshold.
insert_n!(1100, "10000", fn _ -> %{spot_count: 3} end)
insert_n!(50, "144", fn _ -> %{spot_count: 1} end)
run = Recalibrator.run()
assert run.status == "completed"
assert run.sample_count == 1150
# band_count counts distinct bands in the corpus, not just the
# bands we ran analysis for, so 2 here is correct.
assert run.band_count == 2
bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id))
bands_with_bins = bins |> Enum.map(& &1.band) |> Enum.uniq()
assert "10000" in bands_with_bins
refute "144" in bands_with_bins
end
test "spot_count_avg reflects the actual cell mean" do
# Force every cell to have spot_count = 4 so we know the avg.
insert_n!(1000, "10000", fn _ -> %{spot_count: 4} end)
run = Recalibrator.run()
pwat_bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id and fb.feature == "pwat_mm"))
Enum.each(pwat_bins, fn bin ->
if bin.sample_count > 0 do
assert bin.spot_count_avg == 4.0
end
end)
end
test "ignores samples with nil feature value when binning that feature" do
# Half have pwat_mm; half don't. The pwat bins should only
# count the half that does.
insert_n!(500, "10000", fn _ -> %{pwat_mm: 30.0, spot_count: 1} end)
insert_n!(500, "10000", fn _ -> %{pwat_mm: nil, spot_count: 1} end)
run = Recalibrator.run()
assert run.status == "completed"
pwat_total =
FeatureBin
|> where([fb], fb.run_id == ^run.id and fb.feature == "pwat_mm")
|> Repo.all()
|> Enum.map(& &1.sample_count)
|> Enum.sum()
assert pwat_total == 500
end
test "every run records a row in pskr_recalibration_runs regardless of status" do
Recalibrator.run()
Recalibrator.run()
assert Repo.aggregate(RecalibrationRun, :count) == 2
end
end
end