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