- 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 -> != [])
163 lines
5.3 KiB
Elixir
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
|