defmodule Microwaveprop.Propagation.ScorerDiffTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Propagation.GridScore alias Microwaveprop.Propagation.ScorerDiff @valid_time ~U[2026-04-10 12:00:00Z] @old_weights %{ "humidity" => 0.18, "time_of_day" => 0.10, "td_depression" => 0.10, "refractivity" => 0.08, "sky" => 0.08, "season" => 0.08, "wind" => 0.05, "rain" => 0.08, "pressure" => 0.15, "pwat" => 0.10 } # Shift weight from pressure to time_of_day @new_weights %{ "humidity" => 0.18, "time_of_day" => 0.20, "td_depression" => 0.10, "refractivity" => 0.08, "sky" => 0.08, "season" => 0.08, "wind" => 0.05, "rain" => 0.08, "pressure" => 0.05, "pwat" => 0.10 } defp insert_score!(attrs) do %GridScore{} |> GridScore.changeset(attrs) |> Repo.insert!() end defp make_factors(overrides \\ %{}) do base = %{ "humidity" => 80, "time_of_day" => 60, "td_depression" => 70, "refractivity" => 55, "sky" => 88, "season" => 65, "wind" => 90, "rain" => 95, "pressure" => 40, "pwat" => 75 } Map.merge(base, overrides) end setup do # Insert scores at two different bands for the same valid_time insert_score!(%{ lat: 32.875, lon: -97.0, valid_time: @valid_time, band_mhz: 10_000, score: 70, factors: make_factors() }) insert_score!(%{ lat: 33.0, lon: -97.125, valid_time: @valid_time, band_mhz: 10_000, score: 50, factors: make_factors(%{"time_of_day" => 20, "pressure" => 90}) }) insert_score!(%{ lat: 32.875, lon: -97.0, valid_time: @valid_time, band_mhz: 24_000, score: 65, factors: make_factors(%{"humidity" => 40, "season" => 90}) }) # Insert an older score that should not be included insert_score!(%{ lat: 32.875, lon: -97.0, valid_time: ~U[2026-04-10 11:00:00Z], band_mhz: 10_000, score: 55, factors: make_factors(%{"humidity" => 30}) }) :ok end describe "compare/3" do test "returns summary with mean_diff, max_diff, regressions, improvements" do result = ScorerDiff.compare(@old_weights, @new_weights) assert is_map(result.summary) assert is_float(result.summary.mean_diff) assert is_number(result.summary.max_diff) assert is_integer(result.summary.regressions) assert is_integer(result.summary.improvements) end test "returns band_diffs keyed by band_mhz" do result = ScorerDiff.compare(@old_weights, @new_weights) assert Map.has_key?(result.band_diffs, 10_000) assert Map.has_key?(result.band_diffs, 24_000) assert is_float(result.band_diffs[10_000].mean_diff) end test "returns worst_regressions as a list" do result = ScorerDiff.compare(@old_weights, @new_weights) assert is_list(result.worst_regressions) for entry <- result.worst_regressions do assert Map.has_key?(entry, :lat) assert Map.has_key?(entry, :lon) assert Map.has_key?(entry, :band_mhz) assert Map.has_key?(entry, :old_score) assert Map.has_key?(entry, :new_score) end end test "computes correct scores from factors and weights" do result = ScorerDiff.compare(@old_weights, @new_weights) # For the first 10 GHz point (lat 32.875, lon -97.0): # factors: humidity=80, time_of_day=60, td_depression=70, refractivity=55, # sky=88, season=65, wind=90, rain=95, pressure=40, pwat=75 # # old: 80*0.18 + 60*0.10 + 70*0.10 + 55*0.08 + 88*0.08 + 65*0.08 + 90*0.05 + 95*0.08 + 40*0.15 + 75*0.10 # = 14.4 + 6.0 + 7.0 + 4.4 + 7.04 + 5.2 + 4.5 + 7.6 + 6.0 + 7.5 = 69.64 -> 70 # # new: 80*0.18 + 60*0.20 + 70*0.10 + 55*0.08 + 88*0.08 + 65*0.08 + 90*0.05 + 95*0.08 + 40*0.05 + 75*0.10 # = 14.4 + 12.0 + 7.0 + 4.4 + 7.04 + 5.2 + 4.5 + 7.6 + 2.0 + 7.5 = 71.64 -> 72 # # diff = 72 - 70 = +2 (improvement) # For the second 10 GHz point (lat 33.0, lon -97.125): # factors: time_of_day=20, pressure=90 (others same) # # old: 80*0.18 + 20*0.10 + 70*0.10 + 55*0.08 + 88*0.08 + 65*0.08 + 90*0.05 + 95*0.08 + 90*0.15 + 75*0.10 # = 14.4 + 2.0 + 7.0 + 4.4 + 7.04 + 5.2 + 4.5 + 7.6 + 13.5 + 7.5 = 73.14 -> 73 # # new: 80*0.18 + 20*0.20 + 70*0.10 + 55*0.08 + 88*0.08 + 65*0.08 + 90*0.05 + 95*0.08 + 90*0.05 + 75*0.10 # = 14.4 + 4.0 + 7.0 + 4.4 + 7.04 + 5.2 + 4.5 + 7.6 + 4.5 + 7.5 = 66.14 -> 66 # # diff = 66 - 73 = -7 (regression!) # We should see at least 1 regression (diff > 5 in magnitude) assert result.summary.regressions >= 1 # Total count should be 3 (only the most recent valid_time) assert result.summary.total == 3 end test "identical weights produce zero diff" do result = ScorerDiff.compare(@old_weights, @old_weights) assert result.summary.mean_diff == 0.0 assert result.summary.max_diff == 0 assert result.summary.regressions == 0 assert result.summary.improvements == 0 end test "only loads scores from the most recent valid_time" do result = ScorerDiff.compare(@old_weights, @new_weights) # 3 scores at @valid_time (2 for 10GHz, 1 for 24GHz), 1 at earlier time assert result.summary.total == 3 end test "accepts limit option" do result = ScorerDiff.compare(@old_weights, @new_weights, limit: 1) # Should only process 1 score assert result.summary.total == 1 end end describe "weighted_sum/2" do test "computes weighted sum from factors and weights" do factors = %{"humidity" => 100, "time_of_day" => 50} weights = %{"humidity" => 0.5, "time_of_day" => 0.5} assert ScorerDiff.weighted_sum(factors, weights) == 75 end test "rounds to nearest integer" do factors = %{"humidity" => 80, "time_of_day" => 60} weights = %{"humidity" => 0.6, "time_of_day" => 0.4} # 80*0.6 + 60*0.4 = 48 + 24 = 72 assert ScorerDiff.weighted_sum(factors, weights) == 72 end end end