defmodule Mix.Tasks.ScorerDiff do @shortdoc "Compare two weight sets on recent propagation scores" @moduledoc """ Loads the most recent HRRR frame's propagation scores and compares the current weights against a new set, printing a markdown diff report. ## Usage mix scorer_diff --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}' ## Options * `--new-weights` — JSON string of new weights (required) * `--out` — optional file path to write the report """ use Mix.Task alias Microwaveprop.Propagation.BandConfig alias Microwaveprop.Propagation.ScorerDiff @impl Mix.Task def run(argv) do Mix.Task.run("app.start") Oban.pause_all_queues(Oban) {opts, _, _} = OptionParser.parse(argv, switches: [new_weights: :string, out: :string] ) new_weights_json = Keyword.fetch!(opts, :new_weights) out_path = Keyword.get(opts, :out) new_weights = Jason.decode!(new_weights_json) validate_weights!(new_weights) old_weights = stringify_weights(BandConfig.weights()) Mix.shell().info("Comparing current weights vs new weights...") Mix.shell().info("Old: #{inspect(old_weights)}") Mix.shell().info("New: #{inspect(new_weights)}") Mix.shell().info("") result = ScorerDiff.compare(old_weights, new_weights) markdown = ScorerDiff.to_markdown(result) IO.puts(markdown) if out_path do File.mkdir_p!(Path.dirname(out_path)) File.write!(out_path, markdown) Mix.shell().info("Wrote report to #{out_path}") end end defp stringify_weights(weights) do Map.new(weights, fn {k, v} -> {to_string(k), v} end) end defp validate_weights!(weights) do validate_weight_keys!(weights) validate_weight_sum!(weights) end defp validate_weight_keys!(weights) do expected_keys = MapSet.new(~w(humidity time_of_day td_depression refractivity sky season wind rain pressure pwat)) provided_keys = MapSet.new(Map.keys(weights)) missing = MapSet.difference(expected_keys, provided_keys) extra = MapSet.difference(provided_keys, expected_keys) errors = [] |> prepend_if(MapSet.size(missing) > 0, "Missing: #{inspect(MapSet.to_list(missing))}") |> prepend_if(MapSet.size(extra) > 0, "Extra: #{inspect(MapSet.to_list(extra))}") if errors != [] do Mix.raise("Invalid weight keys. " <> Enum.join(errors, " ")) end end defp validate_weight_sum!(weights) do sum = weights |> Map.values() |> Enum.sum() |> Float.round(4) if !(sum >= 0.99 and sum <= 1.01) do Mix.raise("Weights must sum to ~1.0, got #{sum}") end end defp prepend_if(list, true, item), do: list ++ [item] defp prepend_if(list, false, _item), do: list end