Files
RadioPropagationApi/api/app/core/fresnel.py
T
2026-06-23 15:31:40 +03:00

128 lines
3.7 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from math import sqrt
from app.core.surface import SurfaceProfile
C_M_PER_S = 299_792_458.0
EARTH_RADIUS_M = 6_371_000.0
@dataclass(frozen=True)
class LosSample:
lat: float
lon: float
distance_m: float
surface_m: float
path_height_m: float
obstacle_height_m: float
clearance_m: float
fresnel_radius_m: float
required_clearance_m: float
obstruction_type: str
@dataclass(frozen=True)
class LosResult:
los_clear: bool
geometric_los: bool
first_fresnel_clearance_pct: float
worst_sample: LosSample | None
obstructions: list[LosSample]
samples: list[LosSample]
def wavelength(freq_hz: float) -> float:
if freq_hz <= 0:
raise ValueError("freq_hz must be positive")
return C_M_PER_S / freq_hz
def fresnel_radius(lmbda: float, d1: float, d2: float, n: int = 1) -> float:
if lmbda <= 0:
raise ValueError("lmbda must be positive")
if d1 < 0 or d2 < 0:
raise ValueError("distances must be non-negative")
total = d1 + d2
if total == 0:
return 0.0
return sqrt(n * lmbda * d1 * d2 / total)
def earth_bulge(d1: float, d2: float, k: float) -> float:
if k <= 0:
raise ValueError("k must be positive")
return (d1 * d2) / (2 * k * EARTH_RADIUS_M)
def los_analysis(
profile: SurfaceProfile,
tx_height_agl: float,
rx_height_agl: float,
freq_hz: float,
clearance: float = 0.6,
k: float = 1.333,
) -> LosResult:
if not profile.samples:
raise ValueError("profile must contain samples")
total_distance = profile.distance_m
lmbda = wavelength(freq_hz)
tx_elevation = profile.samples[0].ground_m + tx_height_agl
rx_elevation = profile.samples[-1].ground_m + rx_height_agl
obstructions: list[LosSample] = []
samples: list[LosSample] = []
worst_sample: LosSample | None = None
min_ratio = float("inf")
geometric_los = True
for sample in profile.samples:
d1 = sample.distance_m
d2 = total_distance - d1
path_height = (
tx_elevation
if total_distance == 0
else tx_elevation + (rx_elevation - tx_elevation) * (d1 / total_distance)
)
obstacle_height = sample.surface_m + earth_bulge(d1, d2, k)
clearance_m = path_height - obstacle_height
f1 = fresnel_radius(lmbda, d1, d2)
required = clearance * f1
ratio = 100.0 if required == 0 else (clearance_m / required) * 100.0
obstruction_type = sample.dominant_obstruction
los_sample = LosSample(
lat=sample.lat,
lon=sample.lon,
distance_m=d1,
surface_m=sample.surface_m,
path_height_m=path_height,
obstacle_height_m=obstacle_height,
clearance_m=clearance_m,
fresnel_radius_m=f1,
required_clearance_m=required,
obstruction_type=obstruction_type,
)
samples.append(los_sample)
if d1 == 0 or d1 == total_distance:
continue
if worst_sample is None or clearance_m - required < (
worst_sample.clearance_m - worst_sample.required_clearance_m
):
worst_sample = los_sample
min_ratio = min(min_ratio, ratio)
if clearance_m < 0:
geometric_los = False
if clearance_m < required:
obstructions.append(los_sample)
return LosResult(
los_clear=not obstructions,
geometric_los=geometric_los,
first_fresnel_clearance_pct=100.0 if min_ratio == float("inf") else min_ratio,
worst_sample=worst_sample,
obstructions=obstructions,
samples=samples,
)