refactor: deduplicate hrrr download, simplify radio/mqtt, fix credo/perf issues
- hrrr_client: extract shared fetch_grib_ranges/2 to eliminate ~80% duplicated code - mqtt: encode_varint/1 uses IO list accumulator instead of O(n^2) binary concat - commercial: single-pass reduce for aggregate_degradation, remove unused average/1 - scorer: single-pass reduce for safe_avg and build_path_conditions - terrain_analysis: split 76-line do_analyse/4 into 3 focused functions - radio: build_position_changes/3 simplified from then-chains to Enum.reduce - recalibrator_test: fix credo warning (length > 0 -> != [])
This commit is contained in:
parent
fe2ad2df15
commit
a0065e2f44
7 changed files with 109 additions and 127 deletions
|
|
@ -102,14 +102,19 @@ defmodule Microwaveprop.Commercial do
|
||||||
defp aggregate_degradation([]), do: nil
|
defp aggregate_degradation([]), do: nil
|
||||||
|
|
||||||
defp aggregate_degradation(list) do
|
defp aggregate_degradation(list) do
|
||||||
baseline_avg = average(Enum.map(list, & &1.baseline_dbm))
|
{baseline_total, current_total, count} =
|
||||||
current_avg = average(Enum.map(list, & &1.current_dbm))
|
Enum.reduce(list, {0.0, 0.0, 0}, fn item, {bt, ct, c} ->
|
||||||
|
{bt + item.baseline_dbm, ct + item.current_dbm, c + 1}
|
||||||
|
end)
|
||||||
|
|
||||||
|
baseline_avg = baseline_total / count
|
||||||
|
current_avg = current_total / count
|
||||||
|
|
||||||
%{
|
%{
|
||||||
degradation_db: Float.round(baseline_avg - current_avg, 2),
|
degradation_db: Float.round(baseline_avg - current_avg, 2),
|
||||||
baseline_dbm: Float.round(baseline_avg, 2),
|
baseline_dbm: Float.round(baseline_avg, 2),
|
||||||
current_dbm: Float.round(current_avg, 2),
|
current_dbm: Float.round(current_avg, 2),
|
||||||
n_links: length(list)
|
n_links: count
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -172,9 +177,6 @@ defmodule Microwaveprop.Commercial do
|
||||||
# which module asked.
|
# which module asked.
|
||||||
defp haversine_km(lat1, lon1, lat2, lon2), do: Microwaveprop.Radio.haversine_km(lat1, lon1, lat2, lon2)
|
defp haversine_km(lat1, lon1, lat2, lon2), do: Microwaveprop.Radio.haversine_km(lat1, lon1, lat2, lon2)
|
||||||
|
|
||||||
defp average([]), do: 0.0
|
|
||||||
defp average(list), do: Enum.sum(list) / length(list)
|
|
||||||
|
|
||||||
@spec create_link(map()) :: {:ok, Link.t()} | {:error, Ecto.Changeset.t()}
|
@spec create_link(map()) :: {:ok, Link.t()} | {:error, Ecto.Changeset.t()}
|
||||||
def create_link(attrs) do
|
def create_link(attrs) do
|
||||||
%Link{}
|
%Link{}
|
||||||
|
|
|
||||||
|
|
@ -638,8 +638,10 @@ defmodule Microwaveprop.Propagation.Scorer do
|
||||||
|
|
||||||
defp build_path_conditions(%{temps: temps, dewpoints: dewpoints} = buckets, contact) do
|
defp build_path_conditions(%{temps: temps, dewpoints: dewpoints} = buckets, contact) do
|
||||||
lon = contact.pos1["lon"] || -97.0
|
lon = contact.pos1["lon"] || -97.0
|
||||||
avg_temp_c = Enum.sum(temps) / length(temps)
|
{sum_t, count_t} = Enum.reduce(temps, {0, 0}, fn x, {s, c} -> {s + x, c + 1} end)
|
||||||
avg_dewpoint_c = Enum.sum(dewpoints) / length(dewpoints)
|
avg_temp_c = sum_t / count_t
|
||||||
|
{sum_d, count_d} = Enum.reduce(dewpoints, {0, 0}, fn x, {s, c} -> {s + x, c + 1} end)
|
||||||
|
avg_dewpoint_c = sum_d / count_d
|
||||||
|
|
||||||
%{
|
%{
|
||||||
abs_humidity: absolute_humidity(avg_temp_c, avg_dewpoint_c),
|
abs_humidity: absolute_humidity(avg_temp_c, avg_dewpoint_c),
|
||||||
|
|
@ -661,5 +663,9 @@ defmodule Microwaveprop.Propagation.Scorer do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp safe_avg([]), do: nil
|
defp safe_avg([]), do: nil
|
||||||
defp safe_avg(list), do: Enum.sum(list) / length(list)
|
|
||||||
|
defp safe_avg(list) do
|
||||||
|
{sum, count} = Enum.reduce(list, {0, 0}, fn x, {s, c} -> {s + x, c + 1} end)
|
||||||
|
sum / count
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -152,10 +152,16 @@ defmodule Microwaveprop.Pskr.Mqtt do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@spec encode_varint(non_neg_integer()) :: binary()
|
@spec encode_varint(non_neg_integer()) :: binary()
|
||||||
def encode_varint(n) when n in 0..127, do: <<n>>
|
def encode_varint(n) do
|
||||||
|
n
|
||||||
|
|> encode_varint_io([])
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
end
|
||||||
|
|
||||||
def encode_varint(n) when n <= 268_435_455 do
|
defp encode_varint_io(n, acc) when n <= 127, do: [acc, <<n>>]
|
||||||
<<(n &&& 0x7F) ||| 0x80>> <> encode_varint(n >>> 7)
|
|
||||||
|
defp encode_varint_io(n, acc) when n <= 268_435_455 do
|
||||||
|
encode_varint_io(n >>> 7, [acc, <<(n &&& 0x7F) ||| 0x80>>])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
|
|
||||||
|
|
@ -559,11 +559,8 @@ defmodule Microwaveprop.Radio do
|
||||||
defp compute_distance(_pos1, _pos2, existing_distance), do: existing_distance
|
defp compute_distance(_pos1, _pos2, existing_distance), do: existing_distance
|
||||||
|
|
||||||
defp build_position_changes(contact, pos1, pos2, distance) do
|
defp build_position_changes(contact, pos1, pos2, distance) do
|
||||||
%{}
|
Enum.reduce([pos1: pos1, pos2: pos2, distance_km: distance], %{}, fn {key, val}, acc ->
|
||||||
|> then(fn c -> if pos1 && is_nil(contact.pos1), do: Map.put(c, :pos1, pos1), else: c end)
|
if val && is_nil(Map.get(contact, key)), do: Map.put(acc, key, val), else: acc
|
||||||
|> then(fn c -> if pos2 && is_nil(contact.pos2), do: Map.put(c, :pos2, pos2), else: c end)
|
|
||||||
|> then(fn c ->
|
|
||||||
if distance && is_nil(contact.distance_km), do: Map.put(c, :distance_km, distance), else: c
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,63 +68,18 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
|
|
||||||
lambda_m = 0.3 / freq_ghz
|
lambda_m = 0.3 / freq_ghz
|
||||||
n = length(profile) - 1
|
n = length(profile) - 1
|
||||||
|
elev1 = hd(profile).elev + ant_ht_a
|
||||||
|
elev2 = List.last(profile).elev + ant_ht_b
|
||||||
|
|
||||||
first = hd(profile)
|
points = build_analysis_points(profile, n, dist_km, lambda_m, k, elev1, elev2)
|
||||||
last = List.last(profile)
|
|
||||||
elev1 = first.elev + ant_ht_a
|
|
||||||
elev2 = last.elev + ant_ht_b
|
|
||||||
|
|
||||||
# Build effective profile with earth curvature correction
|
|
||||||
points =
|
|
||||||
profile
|
|
||||||
|> Enum.with_index()
|
|
||||||
|> Enum.map(fn {p, i} ->
|
|
||||||
frac = i / n
|
|
||||||
d1_m = frac * dist_km * 1000
|
|
||||||
d2_m = (1 - frac) * dist_km * 1000
|
|
||||||
|
|
||||||
beam = beam_height(frac, elev1, elev2)
|
|
||||||
bulge = earth_bulge(frac, dist_km, k)
|
|
||||||
eff_terrain = p.elev + bulge
|
|
||||||
r1 = fresnel_radius(d1_m, d2_m, lambda_m)
|
|
||||||
clearance = beam - eff_terrain
|
|
||||||
f1_clear = if r1 > 0, do: clearance - r1, else: clearance
|
|
||||||
|
|
||||||
is_endpoint = i == 0 or i == n
|
|
||||||
|
|
||||||
%{
|
|
||||||
lat: p.lat,
|
|
||||||
lon: p.lon,
|
|
||||||
d: p.d,
|
|
||||||
elev: p.elev,
|
|
||||||
dist_km: p.dist_km,
|
|
||||||
idx: i,
|
|
||||||
beam: beam,
|
|
||||||
eff_terrain: eff_terrain,
|
|
||||||
bulge: bulge,
|
|
||||||
r1: r1,
|
|
||||||
d1_m: d1_m,
|
|
||||||
d2_m: d2_m,
|
|
||||||
clearance: clearance,
|
|
||||||
f1_clear: f1_clear,
|
|
||||||
obstructed: not is_endpoint and clearance < 0,
|
|
||||||
fresnel_penetrated: not is_endpoint and r1 > 0 and f1_clear < 0 and clearance >= 0
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
interior = Enum.slice(points, 1..-2//1)
|
interior = Enum.slice(points, 1..-2//1)
|
||||||
|
|
||||||
obstructed = Enum.filter(interior, & &1.obstructed)
|
obstructed = Enum.filter(interior, & &1.obstructed)
|
||||||
fresnel_hit = Enum.filter(interior, & &1.fresnel_penetrated)
|
fresnel_hit = Enum.filter(interior, & &1.fresnel_penetrated)
|
||||||
|
|
||||||
# Deygout diffraction: compute loss across all interior points
|
|
||||||
diffraction_db = deygout_diffraction(interior, lambda_m)
|
diffraction_db = deygout_diffraction(interior, lambda_m)
|
||||||
|
|
||||||
verdict = classify_verdict(diffraction_db, obstructed, fresnel_hit)
|
verdict = classify_verdict(diffraction_db, obstructed, fresnel_hit)
|
||||||
|
|
||||||
elev_vals = Enum.map(profile, & &1.elev)
|
{max_elevation_m, min_clearance_m} = compute_terrain_summary(profile, interior)
|
||||||
clearance_vals = Enum.map(interior, & &1.clearance)
|
|
||||||
max_elevation_m = Enum.max(elev_vals, fn -> 0.0 end)
|
|
||||||
min_clearance_m = if clearance_vals == [], do: 999.0, else: Enum.min(clearance_vals)
|
|
||||||
|
|
||||||
%{
|
%{
|
||||||
points: points,
|
points: points,
|
||||||
|
|
@ -138,6 +93,56 @@ defmodule Microwaveprop.Terrain.TerrainAnalysis do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp build_analysis_points(profile, n, dist_km, lambda_m, k, elev1, elev2) do
|
||||||
|
profile
|
||||||
|
|> Enum.with_index()
|
||||||
|
|> Enum.map(fn {p, i} ->
|
||||||
|
frac = i / n
|
||||||
|
d1_m = frac * dist_km * 1000
|
||||||
|
d2_m = (1 - frac) * dist_km * 1000
|
||||||
|
|
||||||
|
beam = beam_height(frac, elev1, elev2)
|
||||||
|
bulge = earth_bulge(frac, dist_km, k)
|
||||||
|
r1 = fresnel_radius(d1_m, d2_m, lambda_m)
|
||||||
|
clearance = beam - (p.elev + bulge)
|
||||||
|
|
||||||
|
is_endpoint = i == 0 or i == n
|
||||||
|
|
||||||
|
f1_clear = if r1 > 0, do: clearance - r1, else: clearance
|
||||||
|
|
||||||
|
%{
|
||||||
|
lat: p.lat,
|
||||||
|
lon: p.lon,
|
||||||
|
d: p.d,
|
||||||
|
elev: p.elev,
|
||||||
|
dist_km: p.dist_km,
|
||||||
|
idx: i,
|
||||||
|
beam: beam,
|
||||||
|
eff_terrain: p.elev + bulge,
|
||||||
|
bulge: bulge,
|
||||||
|
r1: r1,
|
||||||
|
d1_m: d1_m,
|
||||||
|
d2_m: d2_m,
|
||||||
|
clearance: clearance,
|
||||||
|
f1_clear: f1_clear,
|
||||||
|
obstructed: not is_endpoint and clearance < 0,
|
||||||
|
fresnel_penetrated: not is_endpoint and r1 > 0 and f1_clear < 0 and clearance >= 0
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp compute_terrain_summary(profile, interior) do
|
||||||
|
max_elevation_m = Enum.reduce(profile, 0.0, fn p, acc -> max(p.elev, acc) end)
|
||||||
|
|
||||||
|
min_clearance_m =
|
||||||
|
case interior do
|
||||||
|
[] -> 999.0
|
||||||
|
_ -> Enum.reduce(interior, :infinity, fn p, acc -> min(p.clearance, acc) end)
|
||||||
|
end
|
||||||
|
|
||||||
|
{max_elevation_m, min_clearance_m}
|
||||||
|
end
|
||||||
|
|
||||||
@doc "First Fresnel zone radius at a point between two antennas."
|
@doc "First Fresnel zone radius at a point between two antennas."
|
||||||
@spec fresnel_radius(number(), number(), number()) :: number()
|
@spec fresnel_radius(number(), number(), number()) :: number()
|
||||||
def fresnel_radius(d1_m, d2_m, lambda_m) do
|
def fresnel_radius(d1_m, d2_m, lambda_m) do
|
||||||
|
|
|
||||||
|
|
@ -134,10 +134,9 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
|
|
||||||
{:ok, profiles}
|
{:ok, profiles}
|
||||||
|
|
||||||
{:error, reason} = error ->
|
{:error, _reason} = error ->
|
||||||
# Drain the pressure task so it doesn't leak past this scope.
|
# Drain the pressure task so it doesn't leak past this scope.
|
||||||
_ = Task.await(prs_task, 20 * 60 * 1000)
|
_ = Task.await(prs_task, 20 * 60 * 1000)
|
||||||
_ = reason
|
|
||||||
error
|
error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -540,46 +539,11 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
def download_grib_ranges_to_file(_url, [], _dest_path), do: :ok
|
def download_grib_ranges_to_file(_url, [], _dest_path), do: :ok
|
||||||
|
|
||||||
def download_grib_ranges_to_file(url, ranges, dest_path) do
|
def download_grib_ranges_to_file(url, ranges, dest_path) do
|
||||||
# Fetch ranges concurrently — Range requests against the HRRR
|
case fetch_grib_ranges(url, ranges) do
|
||||||
# mirror are independent and typically complete in ~200-500ms each
|
{:error, _} = error ->
|
||||||
# when sequential. With 40+ ranges per native-duct fetch, serial
|
error
|
||||||
# download dominated the per-forecast-hour wall time. 8-way
|
|
||||||
# parallelism drops a 20s download to ~3s while still respecting
|
|
||||||
# the mirror's modest connection concurrency. Routed through the
|
|
||||||
# partitioned Task.Supervisor to avoid contention when the hourly
|
|
||||||
# PropagationGridWorker fetches f00-f18 alongside recalibrator/GEFS.
|
|
||||||
merged = merge_ranges(ranges)
|
|
||||||
supervisor = {:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}
|
|
||||||
|
|
||||||
results =
|
{:ok, results} ->
|
||||||
supervisor
|
|
||||||
|> Task.Supervisor.async_stream_nolink(
|
|
||||||
merged,
|
|
||||||
fn {start, stop} ->
|
|
||||||
case Req.get(url, [{:headers, [{"Range", "bytes=#{start}-#{stop}"}]} | req_options()]) do
|
|
||||||
{:ok, %{status: 206, body: body}} -> {:ok, start, body}
|
|
||||||
{:ok, %{status: status}} -> {:error, "HRRR grib HTTP #{status}"}
|
|
||||||
{:error, reason} -> {:error, reason}
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
max_concurrency: 8,
|
|
||||||
timeout: 120_000
|
|
||||||
)
|
|
||||||
|> Enum.map(fn
|
|
||||||
{:ok, result} ->
|
|
||||||
result
|
|
||||||
|
|
||||||
{:exit, reason} ->
|
|
||||||
Logger.error("HRRR grib range download crashed (to_file): url=#{url} reason=#{inspect(reason)}")
|
|
||||||
|
|
||||||
{:error, reason}
|
|
||||||
end)
|
|
||||||
|
|
||||||
case Enum.find(results, &match?({:error, _}, &1)) do
|
|
||||||
{:error, reason} ->
|
|
||||||
{:error, reason}
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
file = File.open!(dest_path, [:write, :binary])
|
file = File.open!(dest_path, [:write, :binary])
|
||||||
|
|
||||||
try do
|
try do
|
||||||
|
|
@ -595,13 +559,25 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp download_grib_ranges_remote(url, ranges) do
|
defp download_grib_ranges_remote(url, ranges) do
|
||||||
# Merge adjacent/overlapping ranges to reduce HTTP requests
|
case fetch_grib_ranges(url, ranges) do
|
||||||
|
{:error, _} = error ->
|
||||||
|
error
|
||||||
|
|
||||||
|
{:ok, results} ->
|
||||||
|
binary =
|
||||||
|
results
|
||||||
|
|> Enum.sort_by(fn {:ok, offset, _} -> offset end)
|
||||||
|
|> Enum.map(fn {:ok, _, body} -> body end)
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|
||||||
|
{:ok, binary}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_grib_ranges(url, ranges) do
|
||||||
merged = merge_ranges(ranges)
|
merged = merge_ranges(ranges)
|
||||||
supervisor = {:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}
|
supervisor = {:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}
|
||||||
|
|
||||||
# Download ranges in parallel, routed through the partitioned
|
|
||||||
# Task.Supervisor so concurrent f00-f18 fetches do not serialize on
|
|
||||||
# a single supervisor PID.
|
|
||||||
results =
|
results =
|
||||||
supervisor
|
supervisor
|
||||||
|> Task.Supervisor.async_stream_nolink(
|
|> Task.Supervisor.async_stream_nolink(
|
||||||
|
|
@ -622,23 +598,13 @@ defmodule Microwaveprop.Weather.HrrrClient do
|
||||||
|
|
||||||
{:exit, reason} ->
|
{:exit, reason} ->
|
||||||
Logger.error("HRRR grib range download crashed: url=#{url} reason=#{inspect(reason)}")
|
Logger.error("HRRR grib range download crashed: url=#{url} reason=#{inspect(reason)}")
|
||||||
|
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# Sort by offset and concatenate
|
if error = Enum.find(results, &match?({:error, _}, &1)) do
|
||||||
case Enum.find(results, &match?({:error, _}, &1)) do
|
error
|
||||||
{:error, reason} ->
|
else
|
||||||
{:error, reason}
|
{:ok, results}
|
||||||
|
|
||||||
nil ->
|
|
||||||
binary =
|
|
||||||
results
|
|
||||||
|> Enum.sort_by(fn {:ok, offset, _} -> offset end)
|
|
||||||
|> Enum.map(fn {:ok, _, body} -> body end)
|
|
||||||
|> IO.iodata_to_binary()
|
|
||||||
|
|
||||||
{:ok, binary}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ defmodule Microwaveprop.Pskr.RecalibratorTest do
|
||||||
assert run.avg_spots_per_sample > 0
|
assert run.avg_spots_per_sample > 0
|
||||||
|
|
||||||
bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id))
|
bins = Repo.all(from(fb in FeatureBin, where: fb.run_id == ^run.id))
|
||||||
assert length(bins) > 0
|
assert bins != []
|
||||||
|
|
||||||
# Every bin row should reference the run, target this band, and
|
# Every bin row should reference the run, target this band, and
|
||||||
# carry non-negative stats.
|
# carry non-negative stats.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue