@mytec: 1.2iter ready for test
This commit is contained in:
195
backend/app/services/los_service.py
Normal file
195
backend/app/services/los_service.py
Normal file
@@ -0,0 +1,195 @@
|
||||
import numpy as np
|
||||
from typing import Tuple, List
|
||||
from app.services.terrain_service import terrain_service, TerrainService
|
||||
|
||||
|
||||
class LineOfSightService:
|
||||
"""
|
||||
Line-of-Sight calculations with terrain
|
||||
"""
|
||||
|
||||
EARTH_RADIUS = 6371000 # meters
|
||||
K_FACTOR = 4 / 3 # Standard atmospheric refraction
|
||||
|
||||
def __init__(self, terrain: TerrainService = None):
|
||||
self.terrain = terrain or terrain_service
|
||||
|
||||
async def check_line_of_sight(
|
||||
self,
|
||||
tx_lat: float, tx_lon: float, tx_height: float,
|
||||
rx_lat: float, rx_lon: float, rx_height: float = 1.5,
|
||||
num_samples: int = 50
|
||||
) -> dict:
|
||||
"""
|
||||
Check line-of-sight between transmitter and receiver
|
||||
|
||||
Args:
|
||||
tx_lat, tx_lon: Transmitter coordinates
|
||||
tx_height: Transmitter antenna height above ground (meters)
|
||||
rx_lat, rx_lon: Receiver coordinates
|
||||
rx_height: Receiver height above ground (meters), default 1.5m (person)
|
||||
num_samples: Number of points to sample along path
|
||||
|
||||
Returns:
|
||||
{
|
||||
"has_los": bool,
|
||||
"clearance": float, # minimum clearance in meters (negative = blocked)
|
||||
"blocked_at": float | None, # distance where blocked (meters)
|
||||
"profile": [...] # elevation profile with LOS line
|
||||
}
|
||||
"""
|
||||
# Get elevation profile
|
||||
profile = await self.terrain.get_elevation_profile(
|
||||
tx_lat, tx_lon, rx_lat, rx_lon, num_samples
|
||||
)
|
||||
|
||||
if not profile:
|
||||
return {"has_los": True, "clearance": 0, "blocked_at": None, "profile": []}
|
||||
|
||||
# Get endpoint elevations
|
||||
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
|
||||
|
||||
# Check each point along path
|
||||
for point in profile:
|
||||
d = point["distance"]
|
||||
terrain_elev = point["elevation"]
|
||||
|
||||
if total_distance == 0:
|
||||
los_height = tx_total
|
||||
else:
|
||||
# Linear interpolation of LOS line
|
||||
los_height = tx_total + (rx_total - tx_total) * (d / total_distance)
|
||||
|
||||
# Earth curvature correction (with atmospheric refraction)
|
||||
# Effective Earth radius = K * actual radius
|
||||
effective_radius = self.K_FACTOR * self.EARTH_RADIUS
|
||||
curvature = (d * (total_distance - d)) / (2 * effective_radius)
|
||||
|
||||
# LOS height after curvature correction
|
||||
los_height_corrected = los_height - curvature
|
||||
|
||||
# Clearance at this point
|
||||
clearance = los_height_corrected - terrain_elev
|
||||
|
||||
# Add to profile for visualization
|
||||
point["los_height"] = los_height_corrected
|
||||
point["clearance"] = clearance
|
||||
|
||||
if clearance < min_clearance:
|
||||
min_clearance = clearance
|
||||
if clearance <= 0:
|
||||
blocked_at = d
|
||||
|
||||
has_los = min_clearance > 0
|
||||
|
||||
return {
|
||||
"has_los": has_los,
|
||||
"clearance": min_clearance,
|
||||
"blocked_at": blocked_at,
|
||||
"profile": profile
|
||||
}
|
||||
|
||||
async def calculate_fresnel_clearance(
|
||||
self,
|
||||
tx_lat: float, tx_lon: float, tx_height: float,
|
||||
rx_lat: float, rx_lon: float, rx_height: float,
|
||||
frequency_mhz: float,
|
||||
num_samples: int = 50
|
||||
) -> dict:
|
||||
"""
|
||||
Calculate Fresnel zone clearance
|
||||
|
||||
60% clearance of 1st Fresnel zone = good signal
|
||||
|
||||
Returns:
|
||||
{
|
||||
"clearance_percent": float, # worst-case clearance as % of required
|
||||
"has_adequate_clearance": bool, # >= 60%
|
||||
"worst_point_distance": float,
|
||||
"fresnel_profile": [...]
|
||||
}
|
||||
"""
|
||||
profile = await self.terrain.get_elevation_profile(
|
||||
tx_lat, tx_lon, rx_lat, rx_lon, num_samples
|
||||
)
|
||||
|
||||
if not profile:
|
||||
return {
|
||||
"clearance_percent": 100.0,
|
||||
"has_adequate_clearance": True,
|
||||
"worst_point_distance": 0,
|
||||
"fresnel_profile": []
|
||||
}
|
||||
|
||||
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"]
|
||||
|
||||
# Wavelength (lambda = c / f)
|
||||
wavelength = 300.0 / frequency_mhz # meters
|
||||
|
||||
worst_clearance_pct = 100.0
|
||||
worst_distance = 0.0
|
||||
|
||||
for point in profile:
|
||||
d = point["distance"]
|
||||
terrain_elev = point["elevation"]
|
||||
|
||||
if d == 0 or d == total_distance:
|
||||
continue # Skip endpoints
|
||||
|
||||
# LOS height at this point
|
||||
if total_distance > 0:
|
||||
los_height = tx_total + (rx_total - tx_total) * (d / total_distance)
|
||||
else:
|
||||
los_height = tx_total
|
||||
|
||||
# 1st Fresnel zone radius at this point
|
||||
d1 = d
|
||||
d2 = total_distance - d
|
||||
fresnel_radius = np.sqrt((wavelength * d1 * d2) / total_distance)
|
||||
|
||||
# Required clearance (60% of 1st Fresnel zone)
|
||||
required_clearance = 0.6 * fresnel_radius
|
||||
|
||||
# Actual clearance
|
||||
actual_clearance = los_height - terrain_elev
|
||||
|
||||
# Clearance as percentage of required
|
||||
if required_clearance > 0:
|
||||
clearance_pct = (actual_clearance / required_clearance) * 100
|
||||
else:
|
||||
clearance_pct = 100.0
|
||||
|
||||
# Add to profile
|
||||
point["fresnel_radius"] = fresnel_radius
|
||||
point["required_clearance"] = required_clearance
|
||||
point["clearance_percent"] = clearance_pct
|
||||
|
||||
if clearance_pct < worst_clearance_pct:
|
||||
worst_clearance_pct = clearance_pct
|
||||
worst_distance = d
|
||||
|
||||
return {
|
||||
"clearance_percent": worst_clearance_pct,
|
||||
"has_adequate_clearance": worst_clearance_pct >= 60.0,
|
||||
"worst_point_distance": worst_distance,
|
||||
"fresnel_profile": profile
|
||||
}
|
||||
|
||||
|
||||
# Singleton instance
|
||||
los_service = LineOfSightService()
|
||||
Reference in New Issue
Block a user