From 695bbf5cee35f73410fc403f11b8ec57b1621542 Mon Sep 17 00:00:00 2001 From: Graham McInitre Date: Thu, 16 Jul 2026 07:37:34 -0500 Subject: [PATCH] perf: add batch hrrr_profiles_for_contacts/1 to collapse N+1 per-contact queries into one DB round-trip --- lib/microwaveprop/weather.ex | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index 492ccc1c..3750bd73 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -1738,6 +1738,79 @@ defmodule Microwaveprop.Weather do |> Enum.reject(&is_nil/1) end + @doc """ + Batch version of `hrrr_profiles_for_path/1`. Queries all HRRR profiles + for a list of contacts in a single DB round-trip, then maps results + back per-contact. Returns a list of `{contact, [profile]}` tuples. + """ + @spec hrrr_profiles_for_contacts([map()]) :: [{map(), [HrrrProfile.t()]}] + def hrrr_profiles_for_contacts(contacts) do + for_result = + for c <- contacts, c.pos1 != nil, {lat, lon} <- Radio.contact_path_points(c) do + {{lat, lon, c.qso_timestamp}, c} + end + + points = + Enum.uniq_by(for_result, fn {key, _} -> key end) + + {keys, contact_map} = + Enum.reduce(points, {[], %{}}, fn {{lat, lon, ts} = key, c}, {ks, cm} -> + {[key | ks], Map.update(cm, c, [key], &[key | &1])} + end) + + index = find_nearest_hrrrs_batch(keys) + + Enum.map(contacts, fn c -> + point_keys = Map.get(contact_map, c, []) + profiles = point_keys |> Enum.map(&Map.get(index, &1)) |> Enum.reject(&is_nil/1) + {c, profiles} + end) + end + + # Batch nearest-HRRR lookup: query all profiles in the union bounding + # box once, then match each point to its nearest profile in Elixir. + @spec find_nearest_hrrrs_batch([{float(), float(), DateTime.t()}]) :: %{ + {float(), float(), DateTime.t()} => HrrrProfile.t() + } + defp find_nearest_hrrrs_batch([]), do: %{} + + defp find_nearest_hrrrs_batch(points) do + d = 0.07 + margin_s = 3600 + + {lats, lons, times} = Enum.unzip(points) + lat_min = Enum.min(lats) - d + lat_max = Enum.max(lats) + d + lon_min = Enum.min(lons) - d + lon_max = Enum.max(lons) + d + time_min = times |> Enum.min_by(&DateTime.to_unix/1) |> DateTime.add(-margin_s, :second) + time_max = times |> Enum.max_by(&DateTime.to_unix/1) |> DateTime.add(margin_s, :second) + + profiles = + Repo.all( + from(h in HrrrProfile, + where: + h.lat >= ^lat_min and h.lat <= ^lat_max and + h.lon >= ^lon_min and h.lon <= ^lon_max and + h.valid_time >= ^time_min and h.valid_time <= ^time_max, + select: %{lat: h.lat, lon: h.lon, valid_time: h.valid_time, profile: h} + ) + ) + + Map.new(points, fn {lat, lon, ts} -> + nearest = + Enum.min_by( + profiles, + fn p -> + abs(p.lat - lat) + abs(p.lon - lon) + abs(DateTime.diff(p.valid_time, ts)) + end, + fn -> nil end + ) + + {{lat, lon, ts}, nearest && nearest.profile} + end) + end + @spec find_nearest_native_profile(float(), float(), DateTime.t()) :: HrrrNativeProfile.t() | nil def find_nearest_native_profile(lat, lon, timestamp) do