Compute.run now accepts a `progress` callback and reports human-readable labels before each pipeline step (loading grid, looking up elevation, checking road access, scoring cells, etc.). RoverLive feeds it via send/2 to itself and renders the latest label as small text to the left of the Calculate button while it's running.
197 lines
6.2 KiB
Elixir
197 lines
6.2 KiB
Elixir
defmodule Microwaveprop.Rover.ComputeTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Rover.Compute
|
|
|
|
defp build_grid(home_lat, home_lon) do
|
|
# 3x3 grid of cells around home spaced 0.1 deg apart
|
|
for dlat <- [-0.1, 0.0, 0.1], dlon <- [-0.1, 0.0, 0.1] do
|
|
%{
|
|
lat: Float.round(home_lat + dlat, 4),
|
|
lon: Float.round(home_lon + dlon, 4),
|
|
score: 80
|
|
}
|
|
end
|
|
end
|
|
|
|
test "returns top_candidates ordered by score desc with required fields" do
|
|
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
|
|
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
|
|
grid = build_grid(home.lat, home.lon)
|
|
|
|
scores_at = fn _band, _t, _bbox -> grid end
|
|
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
|
|
clearance_lookup = fn _cells, _stations -> %{} end
|
|
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
|
|
road_lookup = fn _cells, _bbox -> {:error, :stubbed} end
|
|
|
|
args = %{
|
|
home: home,
|
|
stations: stations,
|
|
band_mhz: 10_000,
|
|
valid_time: ~U[2026-04-25 12:00:00Z],
|
|
mode: :ssb,
|
|
max_distance_km: 65.0,
|
|
min_elev_gain: 0
|
|
}
|
|
|
|
result =
|
|
Compute.run(args,
|
|
scores_at: scores_at,
|
|
elev_lookup: elev_lookup,
|
|
clearance_lookup: clearance_lookup,
|
|
prominence_lookup: prominence_lookup,
|
|
road_lookup: road_lookup
|
|
)
|
|
|
|
assert is_map(result)
|
|
assert Map.has_key?(result, :cells)
|
|
assert Map.has_key?(result, :top_candidates)
|
|
|
|
assert is_list(result.cells)
|
|
assert length(result.top_candidates) <= 5
|
|
refute result.top_candidates == []
|
|
|
|
# Sorted by score desc.
|
|
scores = Enum.map(result.top_candidates, & &1.score)
|
|
assert scores == Enum.sort(scores, :desc)
|
|
|
|
# Required candidate fields.
|
|
cand = hd(result.top_candidates)
|
|
|
|
for key <- [
|
|
:grid,
|
|
:lat,
|
|
:lon,
|
|
:elev_m,
|
|
:drive_min,
|
|
:score,
|
|
:tier_color,
|
|
:distance_km,
|
|
:bearing_compass,
|
|
:name
|
|
] do
|
|
assert Map.has_key?(cand, key), "expected candidate to have key #{inspect(key)}"
|
|
end
|
|
|
|
assert is_binary(cand.grid)
|
|
assert is_binary(cand.tier_color)
|
|
assert is_binary(cand.bearing_compass)
|
|
assert is_binary(cand.name)
|
|
end
|
|
|
|
test "penalizes cells with tall nearby buildings (clutter)" do
|
|
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
|
|
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
|
|
grid = build_grid(home.lat, home.lon)
|
|
|
|
tall_cell = {Float.round(home.lat + 0.1, 4), Float.round(home.lon, 4)}
|
|
ref_cell = {Float.round(home.lat - 0.1, 4), Float.round(home.lon, 4)}
|
|
|
|
scores_at = fn _band, _t, _bbox -> grid end
|
|
elev_lookup = fn points -> Map.new(points, fn p -> {p, 250} end) end
|
|
clearance_lookup = fn _cells, _stations -> %{} end
|
|
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
|
|
road_lookup = fn _cells, _bbox -> {:error, :stubbed} end
|
|
|
|
buildings_clutter_lookup = fn cells ->
|
|
Map.new(cells, fn c ->
|
|
height = if {c.lat, c.lon} == tall_cell, do: 30.0, else: 0.0
|
|
{{c.lat, c.lon}, height}
|
|
end)
|
|
end
|
|
|
|
args = %{
|
|
home: home,
|
|
stations: stations,
|
|
band_mhz: 10_000,
|
|
valid_time: ~U[2026-04-25 12:00:00Z],
|
|
mode: :ssb,
|
|
max_distance_km: 65.0,
|
|
min_elev_gain: 0
|
|
}
|
|
|
|
result =
|
|
Compute.run(args,
|
|
scores_at: scores_at,
|
|
elev_lookup: elev_lookup,
|
|
clearance_lookup: clearance_lookup,
|
|
prominence_lookup: prominence_lookup,
|
|
road_lookup: road_lookup,
|
|
buildings_clutter_lookup: buildings_clutter_lookup
|
|
)
|
|
|
|
cells_by_pos = Map.new(result.cells, fn c -> {{c.lat, c.lon}, c.score} end)
|
|
tall_score = Map.fetch!(cells_by_pos, tall_cell)
|
|
ref_score = Map.fetch!(cells_by_pos, ref_cell)
|
|
|
|
# 30m nearby building → penalty = 30/5 = 6.0 dB
|
|
assert ref_score - tall_score >= 5.5
|
|
end
|
|
|
|
test "calls progress callback with each pipeline step label" do
|
|
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
|
|
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
|
|
grid = build_grid(home.lat, home.lon)
|
|
|
|
test_pid = self()
|
|
progress = fn label -> send(test_pid, {:progress, label}) end
|
|
|
|
Compute.run(
|
|
%{
|
|
home: home,
|
|
stations: stations,
|
|
band_mhz: 10_000,
|
|
valid_time: ~U[2026-04-25 12:00:00Z],
|
|
mode: :ssb,
|
|
max_distance_km: 65.0,
|
|
min_elev_gain: 0
|
|
},
|
|
scores_at: fn _, _, _ -> grid end,
|
|
elev_lookup: fn points -> Map.new(points, fn p -> {p, 250} end) end,
|
|
clearance_lookup: fn _, _ -> %{} end,
|
|
prominence_lookup: fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end,
|
|
road_lookup: fn _, _ -> {:error, :stubbed} end,
|
|
progress: progress
|
|
)
|
|
|
|
assert_received {:progress, "Loading propagation grid"}
|
|
assert_received {:progress, "Looking up elevation"}
|
|
assert_received {:progress, "Computing path clearance"}
|
|
assert_received {:progress, "Scoring cells"}
|
|
end
|
|
|
|
test "filters cells failing min_elev_gain" do
|
|
home = %{lat: 33.0, lon: -96.0, elev_m: 500}
|
|
stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}]
|
|
grid = build_grid(home.lat, home.lon)
|
|
|
|
# All elevations below home — nothing should pass min_elev_gain=200.
|
|
scores_at = fn _band, _t, _bbox -> grid end
|
|
elev_lookup = fn points -> Map.new(points, fn p -> {p, 100} end) end
|
|
clearance_lookup = fn _cells, _stations -> %{} end
|
|
prominence_lookup = fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end
|
|
road_lookup = fn _cells, _bbox -> {:error, :stubbed} end
|
|
|
|
result =
|
|
Compute.run(
|
|
%{
|
|
home: home,
|
|
stations: stations,
|
|
band_mhz: 10_000,
|
|
valid_time: ~U[2026-04-25 12:00:00Z],
|
|
mode: :ssb,
|
|
max_distance_km: 65.0,
|
|
min_elev_gain: 200
|
|
},
|
|
scores_at: scores_at,
|
|
elev_lookup: elev_lookup,
|
|
clearance_lookup: clearance_lookup,
|
|
prominence_lookup: prominence_lookup,
|
|
road_lookup: road_lookup
|
|
)
|
|
|
|
assert result.cells == []
|
|
assert result.top_candidates == []
|
|
end
|
|
end
|