Major refactoring of RFCP backend: - Modular propagation models (8 models) - SharedMemoryManager for terrain data - ProcessPoolExecutor parallel processing - WebSocket progress streaming - Building filtering pipeline (351k → 15k) - 82 unit tests Performance: Standard preset 38s → 5s (7.6x speedup) Known issue: Detailed preset timeout (fix in 3.1.0)
86 lines
2.2 KiB
Python
86 lines
2.2 KiB
Python
"""
|
|
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)
|