defmodule Microwaveprop.Terrain.TerrainAnalysisTest do use ExUnit.Case, async: true alias Microwaveprop.Terrain.TerrainAnalysis describe "fresnel_radius/3" do test "returns 0 when d1 or d2 is 0" do assert TerrainAnalysis.fresnel_radius(0, 1000, 0.023) == 0 assert TerrainAnalysis.fresnel_radius(1000, 0, 0.023) == 0 end test "computes correct Fresnel radius for known values" do # 1296 MHz -> lambda = 0.2315 m lambda = 0.3 / 1.296 d1 = 50_000.0 d2 = 50_000.0 r = TerrainAnalysis.fresnel_radius(d1, d2, lambda) assert_in_delta r, 76.1, 1.0 end end describe "earth_bulge/3" do test "returns 0 at endpoints" do assert TerrainAnalysis.earth_bulge(0.0, 100.0) == 0.0 assert TerrainAnalysis.earth_bulge(1.0, 100.0) == 0.0 end test "maximum bulge at midpoint of 100 km path" do bulge = TerrainAnalysis.earth_bulge(0.5, 100.0) assert_in_delta bulge, 147.0, 2.0 end test "bulge increases with path length" do bulge_50 = TerrainAnalysis.earth_bulge(0.5, 50.0) bulge_100 = TerrainAnalysis.earth_bulge(0.5, 100.0) assert bulge_100 > bulge_50 end end describe "knife_edge_loss/1" do test "returns 0 for v <= -0.7787 (clear path)" do assert TerrainAnalysis.knife_edge_loss(-1.0) == 0 assert TerrainAnalysis.knife_edge_loss(-0.8) == 0 end test "returns ~6 dB for v = 0 (grazing)" do loss = TerrainAnalysis.knife_edge_loss(0.0) assert_in_delta loss, 6.0, 0.5 end test "loss increases with v > 0" do loss_05 = TerrainAnalysis.knife_edge_loss(0.5) loss_10 = TerrainAnalysis.knife_edge_loss(1.0) loss_20 = TerrainAnalysis.knife_edge_loss(2.0) assert loss_05 > 0 assert loss_10 > loss_05 assert loss_20 > loss_10 end test "asymptotic formula for v > 2.4" do loss = TerrainAnalysis.knife_edge_loss(3.0) assert_in_delta loss, 22.5, 0.5 end end describe "analyse/5" do test "returns CLEAR for flat terrain with antenna heights" do # 10 km path, flat terrain at 0m, antennas at 30m each # Earth bulge at midpoint ~ 1.5m, beam at 30m, plenty of clearance profile = for i <- 0..10 do f = i / 10 %{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: 0.0, dist_km: f * 10.0} end result = TerrainAnalysis.analyse(profile, 10.0, 1.296, 30.0, 30.0) assert result.verdict == "CLEAR" assert result.obstructed_count == 0 assert result.diffraction_db == 0 end test "returns BLOCKED for high obstacle" do # 50 km path, endpoints at 100m, 500m peak in middle profile = for i <- 0..10 do f = i / 10 elev = if i == 5, do: 500.0, else: 100.0 %{lat: 32.9 + f, lon: -97.0, d: f, elev: elev, dist_km: f * 50.0} end result = TerrainAnalysis.analyse(profile, 50.0, 1.296) assert result.verdict == "BLOCKED" assert result.obstructed_count >= 1 assert result.diffraction_db > 0 end test "max_elevation_m reflects the terrain peak" do profile = for i <- 0..4 do f = i / 4 elev = if i == 2, do: 500.0, else: 100.0 %{lat: 32.9 + f, lon: -97.0, d: f, elev: elev, dist_km: f * 50.0} end result = TerrainAnalysis.analyse(profile, 50.0, 1.296) assert_in_delta result.max_elevation_m, 500.0, 0.1 end test "elevated endpoints clear over flat terrain" do # 20 km path, terrain at 0m, endpoints at 200m (hilltops) # Earth bulge midpoint ~ 5.9m, beam at 200m, terrain + bulge = 5.9m profile = for i <- 0..10 do f = i / 10 %{lat: 32.9 + f * 0.18, lon: -97.0, d: f, elev: 0.0, dist_km: f * 20.0} end # Use antenna heights to represent elevated positions result = TerrainAnalysis.analyse(profile, 20.0, 1.296, 200.0, 200.0) assert result.verdict == "CLEAR" end test "returns fresnel verdict for moderate ridge" do # 20 km path, endpoints at 200m elevation, small ridge at midpoint # Beam at midpoint = 200m, earth bulge ~ 5.9m # Ridge at 160m + bulge 5.9m = 165.9m effective -> clearance = 200 - 165.9 = 34.1m # Fresnel r1 at midpoint 1296 MHz 20km = sqrt(0.2315*10000*10000/20000) ~ 34m # f1_clear = 34.1 - 34 = 0.1m -> just barely clear # Need ridge slightly higher: 165m + 5.9 = 170.9 -> clearance 29.1, f1_clear = -4.9 -> penetrated profile = for i <- 0..10 do f = i / 10 elev = if i == 5, do: 165.0, else: 0.0 %{lat: 32.9 + f * 0.18, lon: -97.0, d: f, elev: elev, dist_km: f * 20.0} end result = TerrainAnalysis.analyse(profile, 20.0, 1.296, 200.0, 200.0) assert result.verdict in ["FRESNEL_MINOR", "FRESNEL_PARTIAL"] assert result.fresnel_hit_count >= 1 end test "antenna heights raise beam above terrain" do # 10 km path, 30m hill, no antenna height -> blocked (beam at 0m, terrain+bulge > 0) # With 50m antennas -> clear profile = for i <- 0..10 do f = i / 10 elev = if i == 5, do: 30.0, else: 0.0 %{lat: 32.9 + f * 0.09, lon: -97.0, d: f, elev: elev, dist_km: f * 10.0} end result_low = TerrainAnalysis.analyse(profile, 10.0, 1.296, 0.0, 0.0) result_high = TerrainAnalysis.analyse(profile, 10.0, 1.296, 100.0, 100.0) assert result_low.verdict == "BLOCKED" assert result_high.verdict == "CLEAR" end test "min_clearance_m can be negative for obstructed paths" do profile = for i <- 0..4 do f = i / 4 elev = if i == 2, do: 500.0, else: 100.0 %{lat: 32.9 + f, lon: -97.0, d: f, elev: elev, dist_km: f * 50.0} end result = TerrainAnalysis.analyse(profile, 50.0, 1.296) assert result.min_clearance_m < 0 end end end