Three signal sources we already collect but weren't using: * NEXRAD composite reflectivity → rain rate via Marshall-Palmer, taken as max of HRRR-derived and NEXRAD-derived rate so fast convective cells between HRRR hourly analyses can still trigger the rain penalty. Only active on f00 — forecast hours can't see future radar. New Scorer.dbz_to_rain_rate_mmhr/1 with 5 dBZ noise floor and 150 mm/hr hail-safe ceiling. * hrrr_native_profiles.best_duct_band_ghz → Scorer.score_refractivity/4 applies a 1.15× boost when the cell's native-resolution duct supports the target band's frequency. HRRR pressure-level gradients systematically under-read thin trapping layers the native profile can resolve. Sub-band ducts do NOT boost — they're evidence that the gradient we have is all there is at the target frequency. * Commercial LOS link rx_power fading → inverse tropo sensor. Commercial.link_degradation_at/3 computes the average 7-day-baseline vs current delta across enabled links within 75 km, ignoring links where link_state != 1. Scorer.commercial_link_boost/2 adds +2 to +25 to the composite score for 3+ dB of fading. ~150 km radius around DFW is the only zone this helps today, but it's the first *measured* signal in the algorithm vs the model-derived proxies. Also fix a latent test bug exposed by the earlier ERA5 poll-timeout bump: era5_batch_client_test's "uncached path returns error" tests hung for up to an hour when run with direnv's real CDS key. New describe-level setup explicitly unsets the env var so the tests stay hermetic. 1,359 tests, 0 failures.
216 lines
6.1 KiB
Elixir
216 lines
6.1 KiB
Elixir
defmodule Microwaveprop.CommercialTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Microwaveprop.Commercial
|
|
alias Microwaveprop.Commercial.Link
|
|
|
|
setup do
|
|
Repo.delete_all(Link)
|
|
:ok
|
|
end
|
|
|
|
@link_attrs %{
|
|
label: "test-link",
|
|
host: "10.0.0.1",
|
|
community: "public",
|
|
radio_type: "af11x",
|
|
weather_station: "KTKI",
|
|
enabled: true
|
|
}
|
|
|
|
describe "enabled_links/0" do
|
|
test "returns only enabled links" do
|
|
{:ok, _enabled} = Commercial.create_link(@link_attrs)
|
|
|
|
{:ok, _disabled} =
|
|
Commercial.create_link(%{@link_attrs | label: "disabled-link", enabled: false})
|
|
|
|
links = Commercial.enabled_links()
|
|
assert length(links) == 1
|
|
assert hd(links).label == "test-link"
|
|
end
|
|
|
|
test "returns empty list when no enabled links" do
|
|
assert Commercial.enabled_links() == []
|
|
end
|
|
end
|
|
|
|
describe "create_link/1" do
|
|
test "creates a link with valid attrs" do
|
|
assert {:ok, link} = Commercial.create_link(@link_attrs)
|
|
assert link.label == "test-link"
|
|
assert link.enabled == true
|
|
end
|
|
|
|
test "returns error for invalid attrs" do
|
|
assert {:error, _changeset} = Commercial.create_link(%{})
|
|
end
|
|
end
|
|
|
|
describe "create_sample/1" do
|
|
test "creates a sample for a link" do
|
|
{:ok, link} = Commercial.create_link(@link_attrs)
|
|
|
|
attrs = %{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.utc_now(),
|
|
rx_power_0: -55.0,
|
|
tx_power: 20.0,
|
|
link_state: 1
|
|
}
|
|
|
|
assert {:ok, sample} = Commercial.create_sample(attrs)
|
|
assert sample.rx_power_0 == -55.0
|
|
assert sample.link_id == link.id
|
|
end
|
|
end
|
|
|
|
describe "link_degradation_at/3" do
|
|
@endpoint_dfw %{"lat" => 33.2, "lon" => -96.5}
|
|
@far_point {0.0, 0.0}
|
|
@near_point {33.2, -96.5}
|
|
|
|
test "returns nil when no links are within the radius" do
|
|
{:ok, _} =
|
|
Commercial.create_link(
|
|
Map.merge(@link_attrs, %{
|
|
endpoint_a: @endpoint_dfw,
|
|
endpoint_b: @endpoint_dfw
|
|
})
|
|
)
|
|
|
|
valid_time = ~U[2026-04-13 18:00:00Z]
|
|
assert Commercial.link_degradation_at(@far_point, valid_time, radius_km: 50) == nil
|
|
end
|
|
|
|
test "returns nil when the link has no recent baseline samples" do
|
|
{:ok, _link} =
|
|
Commercial.create_link(Map.merge(@link_attrs, %{endpoint_a: @endpoint_dfw, endpoint_b: @endpoint_dfw}))
|
|
|
|
valid_time = ~U[2026-04-13 18:00:00Z]
|
|
assert Commercial.link_degradation_at(@near_point, valid_time) == nil
|
|
end
|
|
|
|
test "returns positive degradation when current rx_power is below baseline" do
|
|
{:ok, link} =
|
|
Commercial.create_link(Map.merge(@link_attrs, %{endpoint_a: @endpoint_dfw, endpoint_b: @endpoint_dfw}))
|
|
|
|
valid_time = ~U[2026-04-13 18:00:00Z]
|
|
|
|
# 7-day baseline: -52 dBm average
|
|
for day_offset <- 1..6 do
|
|
{:ok, _} =
|
|
Commercial.create_sample(%{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second),
|
|
rx_power_0: -52.0,
|
|
rx_power_1: -52.0,
|
|
link_state: 1
|
|
})
|
|
end
|
|
|
|
# Current (within 15 min) drops to -62 — 10 dB deep fade.
|
|
{:ok, _} =
|
|
Commercial.create_sample(%{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.add(valid_time, -300, :second),
|
|
rx_power_0: -62.0,
|
|
rx_power_1: -62.0,
|
|
link_state: 1
|
|
})
|
|
|
|
result = Commercial.link_degradation_at(@near_point, valid_time)
|
|
assert result
|
|
assert_in_delta result.degradation_db, 10.0, 0.5
|
|
assert result.n_links == 1
|
|
end
|
|
|
|
test "returns 0 degradation when rx_power matches baseline" do
|
|
{:ok, link} =
|
|
Commercial.create_link(Map.merge(@link_attrs, %{endpoint_a: @endpoint_dfw, endpoint_b: @endpoint_dfw}))
|
|
|
|
valid_time = ~U[2026-04-13 18:00:00Z]
|
|
|
|
for day_offset <- 1..6 do
|
|
{:ok, _} =
|
|
Commercial.create_sample(%{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second),
|
|
rx_power_0: -55.0,
|
|
link_state: 1
|
|
})
|
|
end
|
|
|
|
{:ok, _} =
|
|
Commercial.create_sample(%{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.add(valid_time, -60, :second),
|
|
rx_power_0: -55.0,
|
|
link_state: 1
|
|
})
|
|
|
|
result = Commercial.link_degradation_at(@near_point, valid_time)
|
|
assert result
|
|
assert_in_delta result.degradation_db, 0.0, 0.5
|
|
end
|
|
|
|
test "ignores links with link_state != 1 (down link isn't tropo)" do
|
|
{:ok, link} =
|
|
Commercial.create_link(Map.merge(@link_attrs, %{endpoint_a: @endpoint_dfw, endpoint_b: @endpoint_dfw}))
|
|
|
|
valid_time = ~U[2026-04-13 18:00:00Z]
|
|
|
|
for day_offset <- 1..6 do
|
|
{:ok, _} =
|
|
Commercial.create_sample(%{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.add(valid_time, -day_offset * 3600 * 24, :second),
|
|
rx_power_0: -52.0,
|
|
link_state: 1
|
|
})
|
|
end
|
|
|
|
{:ok, _} =
|
|
Commercial.create_sample(%{
|
|
link_id: link.id,
|
|
sampled_at: DateTime.add(valid_time, -120, :second),
|
|
rx_power_0: -90.0,
|
|
link_state: 0
|
|
})
|
|
|
|
assert Commercial.link_degradation_at(@near_point, valid_time) == nil
|
|
end
|
|
end
|
|
|
|
describe "weather_stations/0" do
|
|
test "returns unique weather stations from enabled links" do
|
|
{:ok, _} = Commercial.create_link(@link_attrs)
|
|
|
|
{:ok, _} =
|
|
Commercial.create_link(%{@link_attrs | label: "link-2", host: "10.0.0.2"})
|
|
|
|
{:ok, _} =
|
|
Commercial.create_link(%{
|
|
@link_attrs
|
|
| label: "link-3",
|
|
host: "10.0.0.3",
|
|
weather_station: "KDFW"
|
|
})
|
|
|
|
stations = Commercial.weather_stations()
|
|
assert Enum.sort(stations) == ["KDFW", "KTKI"]
|
|
end
|
|
|
|
test "excludes disabled links" do
|
|
{:ok, _} = Commercial.create_link(%{@link_attrs | enabled: false})
|
|
|
|
assert Commercial.weather_stations() == []
|
|
end
|
|
|
|
test "excludes nil weather stations" do
|
|
{:ok, _} = Commercial.create_link(%{@link_attrs | weather_station: nil})
|
|
|
|
assert Commercial.weather_stations() == []
|
|
end
|
|
end
|
|
end
|