From a81e55e3484f8ba6cabe4005a6c0b45a08685154 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 21 Apr 2026 09:16:01 -0500 Subject: [PATCH] refactor(propagation): drop ShadowComparator (Phase 1 only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 cutover completed in 65693ed — Rust prop-grid-rs owns f01-f18 production writes and the shadow path has been dead for weeks. No callers in lib/, test/, or mix tasks. --- .../propagation/shadow_comparator.ex | 119 ------------------ 1 file changed, 119 deletions(-) delete mode 100644 lib/microwaveprop/propagation/shadow_comparator.ex diff --git a/lib/microwaveprop/propagation/shadow_comparator.ex b/lib/microwaveprop/propagation/shadow_comparator.ex deleted file mode 100644 index 39ee2d0d..00000000 --- a/lib/microwaveprop/propagation/shadow_comparator.ex +++ /dev/null @@ -1,119 +0,0 @@ -defmodule Microwaveprop.Propagation.ShadowComparator do - @moduledoc """ - Shadow-mode validator for the Rust `prop-grid-rs` port. - - During Phase 1 shadow mode the Rust worker writes score-grid files to - `/data/scores_shadow//.ntms` while Elixir keeps writing to - `/data/scores/`. This module compares the two files and logs the - per-band delta distribution so drift is visible before cutover. - - Callable from IEx on demand or from a one-shot Mix task; not started - in the supervision tree (this is temporary instrumentation). - """ - - alias Microwaveprop.Propagation.ScoresFile - - require Logger - - @default_prod_dir "/data/scores" - @default_shadow_dir "/data/scores_shadow" - - @type diff :: %{ - band_mhz: non_neg_integer(), - valid_time: DateTime.t(), - total_cells: non_neg_integer(), - matching_cells: non_neg_integer(), - missing_shadow_cells: non_neg_integer(), - max_abs_delta: non_neg_integer(), - mean_abs_delta: float() - } - - @spec compare(non_neg_integer(), DateTime.t(), keyword()) :: - {:ok, diff()} | {:error, term()} - def compare(band_mhz, %DateTime{} = valid_time, opts \\ []) do - prod_dir = Keyword.get(opts, :prod_dir, @default_prod_dir) - shadow_dir = Keyword.get(opts, :shadow_dir, @default_shadow_dir) - - with {:ok, prod} <- read_file(prod_dir, band_mhz, valid_time), - {:ok, shadow} <- read_file(shadow_dir, band_mhz, valid_time) do - {:ok, diff_payloads(band_mhz, valid_time, prod, shadow)} - end - end - - defp read_file(dir, band_mhz, valid_time) do - path = band_mhz |> ScoresFile.path_for(valid_time) |> swap_base(dir) - - case File.read(path) do - {:ok, binary} -> decode_scores_file(binary) - {:error, reason} -> {:error, {:read, path, reason}} - end - end - - defp swap_base(path, new_base) do - suffix = Path.relative_to(path, ScoresFile.base_dir()) - Path.join(new_base, suffix) - end - - # Inline parse of the v1 scores-file on-disk format so this module - # doesn't reach into `ScoresFile`'s private `decode/1`. The format is - # documented in `ScoresFile`'s moduledoc; only the body size and - # dimensions matter for a diff, so we intentionally keep this minimal. - # The literal "NTMS" magic is the file-header marker already on disk. - defp decode_scores_file( - <<"NTMS", 1::unsigned-8, _band_mhz::unsigned-little-32, _valid_time_unix::signed-little-64, - _lat_min::float-little-32, _lon_min::float-little-32, _step::float-little-32, n_rows::unsigned-little-16, - n_cols::unsigned-little-16, body::binary>> - ) do - expected = n_rows * n_cols - - if byte_size(body) == expected do - {:ok, %{n_rows: n_rows, n_cols: n_cols, scores: body}} - else - {:error, :invalid_format} - end - end - - defp decode_scores_file(_), do: {:error, :invalid_format} - - defp diff_payloads(band_mhz, valid_time, prod, shadow) do - %{scores: p_body, n_cols: n_cols, n_rows: n_rows} = prod - %{scores: s_body} = shadow - - {total, matching, missing, max_abs, sum_abs} = - for row <- 0..(n_rows - 1), - col <- 0..(n_cols - 1), - reduce: {0, 0, 0, 0, 0} do - {t, m, miss, max_d, sum_d} -> - idx = row * n_cols + col - p = :binary.at(p_body, idx) - s = :binary.at(s_body, idx) - - cond do - p == 255 and s == 255 -> - {t, m, miss, max_d, sum_d} - - p != 255 and s == 255 -> - {t + 1, m, miss + 1, max_d, sum_d} - - p == s -> - {t + 1, m + 1, miss, max_d, sum_d} - - true -> - d = abs(p - s) - {t + 1, m, miss, max(max_d, d), sum_d + d} - end - end - - mean = if total > 0, do: sum_abs / total, else: 0.0 - - %{ - band_mhz: band_mhz, - valid_time: valid_time, - total_cells: total, - matching_cells: matching, - missing_shadow_cells: missing, - max_abs_delta: max_abs, - mean_abs_delta: mean - } - end -end