""" Knife-edge diffraction model based on ITU-R P.526. Used for calculating additional loss when terrain or obstacles block the line of sight between TX and RX. Reference: ITU-R P.526-15 """ import math class KnifeEdgeDiffractionModel: """ Single knife-edge diffraction model. Stateless utility — not a full PropagationModel since it calculates additional loss, not total path loss. """ @staticmethod def calculate_loss( d1_m: float, d2_m: float, h_m: float, wavelength_m: float, ) -> float: """ Calculate diffraction loss over single knife edge. Args: d1_m: Distance from TX to obstacle d2_m: Distance from obstacle to RX h_m: Obstacle height above LOS line (positive = above) wavelength_m: Signal wavelength Returns: Loss in dB (always >= 0) """ if d1_m <= 0 or d2_m <= 0 or wavelength_m <= 0: return 0.0 # Fresnel-Kirchhoff parameter v = h_m * math.sqrt(2 * (d1_m + d2_m) / (wavelength_m * d1_m * d2_m)) # Diffraction loss (Lee approximation) if v < -0.78: L = 0.0 elif v < 0: L = 6.02 + 9.11 * v - 1.27 * v ** 2 elif v < 2.4: L = 6.02 + 9.11 * v + 1.65 * v ** 2 else: L = 12.95 + 20 * math.log10(v) return max(0.0, L) @staticmethod def calculate_clearance_loss( clearance_m: float, frequency_mhz: float, ) -> float: """ Simplified diffraction loss from terrain clearance. Matches the existing coverage_service._diffraction_loss logic. Args: clearance_m: Minimum LOS clearance (negative = blocked) frequency_mhz: Signal frequency Returns: Loss in dB (0 if positive clearance) """ if clearance_m >= 0: return 0.0 v = abs(clearance_m) / 10 if v <= 0: loss = 0.0 elif v < 2.4: loss = 6.02 + 9.11 * v - 1.27 * v ** 2 else: loss = 13.0 + 20 * math.log10(v) return min(loss, 40.0)