diff --git a/lib/mix/tasks/calibrate.aprs_144.ex b/lib/mix/tasks/calibrate.aprs_144.ex index 72583ac0..42ef1445 100644 --- a/lib/mix/tasks/calibrate.aprs_144.ex +++ b/lib/mix/tasks/calibrate.aprs_144.ex @@ -42,8 +42,24 @@ defmodule Mix.Tasks.Calibrate.Aprs144 do @impl Mix.Task def run(argv) do Mix.Task.run("app.start") + + if BandConfig.get(@band_mhz) == nil do + Mix.raise("BandConfig has no #{@band_mhz} MHz entry; cannot calibrate.") + end + _ = Oban.pause_all_queues(Oban) + try do + do_run(argv) + after + # Allow `iex -S mix` workflows to keep using Oban after the task + # returns; for one-shot Mix invocations the BEAM exits and this is + # a no-op. + _ = Oban.resume_all_queues(Oban) + end + end + + defp do_run(argv) do {opts, _, _} = OptionParser.parse(argv, switches: [ @@ -60,10 +76,10 @@ defmodule Mix.Tasks.Calibrate.Aprs144 do max_packets = Keyword.get(opts, :max_packets, 50_000) Mix.shell().info("APRS-144 calibration") - Mix.shell().info(" since_hours: #{since_hours}") - Mix.shell().info(" epochs: #{epochs}") + Mix.shell().info(" since_hours: #{since_hours}") + Mix.shell().info(" epochs: #{epochs}") Mix.shell().info(" learning_rate: #{learning_rate}") - Mix.shell().info(" max_packets: #{max_packets}") + Mix.shell().info(" max_packets: #{max_packets}") Mix.shell().info("") since = DateTime.add(DateTime.utc_now(), -since_hours * 3600, :second) @@ -79,14 +95,22 @@ defmodule Mix.Tasks.Calibrate.Aprs144 do Mix.shell().info("Parsed #{length(hops)} verified RF hops") positives = compute_positive_factors(hops) - Mix.shell().info("Computed #{length(positives)} positive factor vectors with HRRR coverage") + + Mix.shell().info( + "Computed #{length(positives)} / #{length(hops)} positive factor vectors " <> + "(#{length(hops) - length(positives)} hops dropped for missing HRRR coverage)" + ) if length(positives) < @min_samples do Mix.shell().info("") Mix.shell().info("Refusing to fit: only #{length(positives)} positive samples (need >= #{@min_samples})") else - negatives = compute_negative_factors(length(positives)) - Mix.shell().info("Computed #{length(negatives)} negative factor vectors") + {negatives, attempted} = compute_negative_factors(length(positives)) + + Mix.shell().info( + "Computed #{length(negatives)} / #{attempted} negative factor vectors " <> + "(#{attempted - length(negatives)} samples dropped for missing HRRR coverage)" + ) if length(negatives) < @min_samples do Mix.shell().info("") @@ -142,14 +166,20 @@ defmodule Mix.Tasks.Calibrate.Aprs144 do end defp compute_negative_factors(n) do - n - |> Backtest.random_baseline(sample_size: n) - |> Enum.flat_map(fn {lat, lon, time} -> - case Weather.find_nearest_hrrr(lat, lon, time) do - nil -> [] - profile -> [Recalibrator.compute_factors(profile, time, @band_mhz)] - end - end) + # Use a wide contact-pool draw (5_000 minimum) so the random baseline + # samples don't cluster geographically when n is small. Backtest's + # default :sample_size is 5_000. + baselines = Backtest.random_baseline(n, sample_size: max(n, 5_000)) + + factors = + Enum.flat_map(baselines, fn {lat, lon, time} -> + case Weather.find_nearest_hrrr(lat, lon, time) do + nil -> [] + profile -> [Recalibrator.compute_factors(profile, time, @band_mhz)] + end + end) + + {factors, length(baselines)} end defp print_results(result, positives, negatives) do