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 deygout( profile: SurfaceProfile, tx_height_agl: float, rx_height_agl: float, freq_hz: float, ) -> float: """Recursive Deygout diffraction loss using the dominant edge and subprofiles.""" if len(profile.samples) < 3: return 0.0 endpoint_heights = { 0: profile.samples[0].ground_m + tx_height_agl, len(profile.samples) - 1: profile.samples[-1].ground_m + rx_height_agl, } def sample_height(index: int) -> float: return endpoint_heights.get(index, profile.samples[index].surface_m) def solve(left: int, right: int) -> float: if right - left < 2: return 0.0 left_sample = profile.samples[left] right_sample = profile.samples[right] span_m = right_sample.distance_m - left_sample.distance_m left_height = sample_height(left) right_height = sample_height(right) max_v = float("-inf") max_index: int | None = None for index in range(left + 1, right): sample = profile.samples[index] d1 = sample.distance_m - left_sample.distance_m d2 = right_sample.distance_m - sample.distance_m path_height = left_height + (right_height - left_height) * (d1 / span_m) h = sample.surface_m - path_height v = knife_edge_v(h, d1, d2, freq_hz) if v > max_v: max_v = v max_index = index if max_index is None: return 0.0 main_loss = knife_edge_loss(max_v) if main_loss == 0.0: return 0.0 return main_loss + solve(left, max_index) + solve(max_index, right) return solve(0, len(profile.samples) - 1)