fix(rover): keep cells without elevation; surface diagnostic warnings as flash

This commit is contained in:
Graham McIntire 2026-04-25 16:41:31 -05:00
parent 9ebb134a03
commit e2dfc05939
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 72 additions and 30 deletions

View file

@ -38,7 +38,11 @@ defmodule Microwaveprop.Rover.Compute do
min_elev_gain: integer()
}
@spec run(run_args(), keyword()) :: %{cells: [map()], top_candidates: [map()]}
@spec run(run_args(), keyword()) :: %{
cells: [map()],
top_candidates: [map()],
warnings: [String.t()]
}
def run(args, deps \\ []) do
scores_at = Keyword.get(deps, :scores_at, &Propagation.scores_at/3)
elev_lookup = Keyword.get(deps, :elev_lookup, &Elevation.lookup_many/1)
@ -76,15 +80,44 @@ defmodule Microwaveprop.Rover.Compute do
|> Enum.filter(&keep_cell?(&1, home_elev, min_elev_gain))
top_candidates =
cells |> Enum.sort_by(& &1.score, :desc) |> Enum.take(@top_n) |> Enum.map(&candidate_payload(&1, home))
cells
|> Enum.sort_by(& &1.score, :desc)
|> Enum.take(@top_n)
|> Enum.map(&candidate_payload(&1, home))
%{cells: cells, top_candidates: top_candidates}
warnings = build_warnings(raw_cells, in_radius, elev_map, cells)
%{cells: cells, top_candidates: top_candidates, warnings: warnings}
end
defp build_warnings(raw_cells, in_radius, elev_map, cells) do
[]
|> add_warning_if(raw_cells == [], "No HRRR score grid available for this band/time.")
|> add_warning_if(in_radius == [], "No score grid cells within the drive radius.")
|> add_warning_if(
in_radius != [] and elev_map_all_nil?(elev_map),
"Elevation tiles unavailable (SRTM not mounted); ranking ignores elevation."
)
|> add_warning_if(
in_radius != [] and cells == [],
"All cells filtered out by score/elevation thresholds."
)
end
defp add_warning_if(list, true, msg), do: list ++ [msg]
defp add_warning_if(list, false, _msg), do: list
defp elev_map_all_nil?(elev_map) do
elev_map != %{} and Enum.all?(elev_map, fn {_k, v} -> is_nil(v) end)
end
defp keep_cell?(nil, _home_elev, _min_gain), do: false
defp keep_cell?(%{elev_m: elev_m, score: score}, home_elev, min_gain) when is_integer(elev_m) do
elev_m - home_elev >= min_gain and score >= 0
defp keep_cell?(%{score: score, elev_m: elev_m}, home_elev, min_gain) do
# Cells with unknown elevation (no SRTM tile) are kept; elev gain
# filter only applies when we actually know the cell elevation.
elev_ok? = is_nil(elev_m) or elev_m - home_elev >= min_gain
elev_ok? and score >= 0
end
defp keep_cell?(_, _, _), do: false
@ -92,33 +125,27 @@ defmodule Microwaveprop.Rover.Compute do
defp annotate_cell(cell, elev_map, home, home_elev, mode, stations) do
elev_m = Map.get(elev_map, {cell.lat, cell.lon})
case elev_m do
margins = Enum.map(stations, fn _s -> LinkMargin.link_margin_from_score(cell.score, mode) end)
agg = Aggregator.cell_margin_db(margins)
case agg do
nil ->
nil
_ ->
margins = Enum.map(stations, fn _s -> LinkMargin.link_margin_from_score(cell.score, mode) end)
agg = Aggregator.cell_margin_db(margins)
agg_db ->
dist_km = DriveTime.haversine_km({home.lat, home.lon}, {cell.lat, cell.lon})
elev_bonus = elev_bonus(elev_m, home_elev)
drive_penalty = drive_penalty(dist_km)
score = agg_db + elev_bonus - drive_penalty
case agg do
nil ->
nil
agg_db ->
dist_km = DriveTime.haversine_km({home.lat, home.lon}, {cell.lat, cell.lon})
elev_bonus = elev_bonus(elev_m, home_elev)
drive_penalty = drive_penalty(dist_km)
score = agg_db + elev_bonus - drive_penalty
%{
lat: cell.lat,
lon: cell.lon,
elev_m: elev_m,
score: score,
distance_km: dist_km,
tier_color: tier_color(score)
}
end
%{
lat: cell.lat,
lon: cell.lon,
elev_m: elev_m,
score: score,
distance_km: dist_km,
tier_color: tier_color(score)
}
end
end
@ -141,6 +168,8 @@ defmodule Microwaveprop.Rover.Compute do
}
end
defp elev_bonus(nil, _elev_home), do: 0.0
defp elev_bonus(elev_c, elev_home) do
diff = (elev_c - elev_home) / 100.0 * @elev_bonus_db_per_100m
diff |> max(0.0) |> min(@elev_bonus_cap_db)

View file

@ -255,10 +255,13 @@ defmodule MicrowavepropWeb.RoverLive do
# ── Async ────────────────────────────────────────────────────────────
@impl true
def handle_async(:scoring, {:ok, %{cells: cells, top_candidates: cands}}, socket) do
def handle_async(:scoring, {:ok, %{cells: cells, top_candidates: cands} = result}, socket) do
warnings = Map.get(result, :warnings, [])
{:noreply,
socket
|> assign(top_candidates: cands, scoring_loading: false)
|> apply_scoring_warnings(warnings)
|> push_event("rover_results", %{
cells: cells,
drive_radius_km: socket.assigns.drive_radius_km
@ -267,7 +270,17 @@ defmodule MicrowavepropWeb.RoverLive do
def handle_async(:scoring, {:exit, reason}, socket) do
Logger.error("rover scoring crashed: #{inspect(reason)}")
{:noreply, assign(socket, top_candidates: [], scoring_loading: false)}
{:noreply,
socket
|> assign(top_candidates: [], scoring_loading: false)
|> put_flash(:error, "Calculate failed: #{inspect(reason)}")}
end
defp apply_scoring_warnings(socket, []), do: clear_flash(socket)
defp apply_scoring_warnings(socket, warnings) do
put_flash(socket, :info, Enum.join(warnings, " "))
end
# ── Mutation helpers ────────────────────────────────────────────────