from __future__ import annotations from math import log10, sqrt from app.core.fresnel import wavelength from app.core.surface import SurfaceProfile def knife_edge_loss(v: float) -> float: if v <= -0.78: return 0.0 return 6.9 + 20 * log10(sqrt((v - 0.1) ** 2 + 1) + v - 0.1) def knife_edge_v(h: float, d1: float, d2: float, freq_hz: float) -> float: if d1 <= 0 or d2 <= 0: return float("-inf") lmbda = wavelength(freq_hz) return h * sqrt(2 * (d1 + d2) / (lmbda * d1 * d2)) def bullington_loss( profile: SurfaceProfile, tx_height_agl: float, rx_height_agl: float, freq_hz: float, ) -> float: """Bullington-style equivalent edge loss for a terrain profile. The current implementation uses the dominant obstacle relative to the TX-RX chord as the equivalent Bullington edge, then applies ITU-R P.526 J(v). """ if len(profile.samples) < 3: return 0.0 total_distance = profile.distance_m if total_distance <= 0: return 0.0 tx_elevation = profile.samples[0].ground_m + tx_height_agl rx_elevation = profile.samples[-1].ground_m + rx_height_agl max_v = float("-inf") for sample in profile.samples[1:-1]: d1 = sample.distance_m d2 = total_distance - d1 path_height = tx_elevation + (rx_elevation - tx_elevation) * (d1 / total_distance) h = sample.surface_m - path_height max_v = max(max_v, knife_edge_v(h, d1, d2, freq_hz)) return knife_edge_loss(max_v) def deygout( profile: SurfaceProfile, tx_height_agl: float, rx_height_agl: float, freq_hz: float, ) -> float: """Compatibility wrapper; use Bullington equivalent loss for multi-edge profiles.""" return bullington_loss(profile, tx_height_agl, rx_height_agl, freq_hz)