""" Line-of-sight checks using terrain profile data. """ import math from typing import Optional, Dict, List EARTH_RADIUS = 6371000 K_FACTOR = 4 / 3 # Standard atmospheric refraction def check_los_terrain( profile: List[dict], tx_height: float, rx_height: float, ) -> dict: """ Check line-of-sight from a terrain elevation profile. Args: profile: List of dicts with 'elevation' and 'distance' keys. tx_height: TX antenna height above ground (meters). rx_height: RX height above ground (meters). Returns: dict with has_los, clearance, blocked_at """ if not profile: return {"has_los": True, "clearance": 0.0, "blocked_at": None} tx_ground = profile[0]["elevation"] rx_ground = profile[-1]["elevation"] tx_total = tx_ground + tx_height rx_total = rx_ground + rx_height total_distance = profile[-1]["distance"] min_clearance = float("inf") blocked_at = None for point in profile: d = point["distance"] terrain_elev = point["elevation"] if total_distance == 0: los_height = tx_total else: los_height = tx_total + (rx_total - tx_total) * (d / total_distance) # Earth curvature correction effective_radius = K_FACTOR * EARTH_RADIUS curvature = (d * (total_distance - d)) / (2 * effective_radius) los_height_corrected = los_height - curvature clearance = los_height_corrected - terrain_elev if clearance < min_clearance: min_clearance = clearance if clearance <= 0: blocked_at = d return { "has_los": min_clearance > 0, "clearance": min_clearance, "blocked_at": blocked_at, } def fresnel_radius( d1_m: float, d2_m: float, wavelength_m: float, zone: int = 1 ) -> float: """Calculate Fresnel zone radius at a point along the path. Args: d1_m: Distance from TX to point d2_m: Distance from point to RX wavelength_m: Signal wavelength zone: Fresnel zone number (default 1) Returns: Radius in meters """ total = d1_m + d2_m if total <= 0: return 0.0 return math.sqrt(zone * wavelength_m * d1_m * d2_m / total)