62 lines
1.8 KiB
Python
62 lines
1.8 KiB
Python
from __future__ import annotations
|
|
|
|
from math import log10, sqrt
|
|
|
|
from app.core.fresnel import wavelength
|
|
from app.core.surface import SurfaceProfile
|
|
|
|
|
|
def knife_edge_loss(v: float) -> float:
|
|
if v <= -0.78:
|
|
return 0.0
|
|
return 6.9 + 20 * log10(sqrt((v - 0.1) ** 2 + 1) + v - 0.1)
|
|
|
|
|
|
def knife_edge_v(h: float, d1: float, d2: float, freq_hz: float) -> float:
|
|
if d1 <= 0 or d2 <= 0:
|
|
return float("-inf")
|
|
lmbda = wavelength(freq_hz)
|
|
return h * sqrt(2 * (d1 + d2) / (lmbda * d1 * d2))
|
|
|
|
|
|
def bullington_loss(
|
|
profile: SurfaceProfile,
|
|
tx_height_agl: float,
|
|
rx_height_agl: float,
|
|
freq_hz: float,
|
|
) -> float:
|
|
"""Bullington-style equivalent edge loss for a terrain profile.
|
|
|
|
The current implementation uses the dominant obstacle relative to the TX-RX
|
|
chord as the equivalent Bullington edge, then applies ITU-R P.526 J(v).
|
|
"""
|
|
if len(profile.samples) < 3:
|
|
return 0.0
|
|
|
|
total_distance = profile.distance_m
|
|
if total_distance <= 0:
|
|
return 0.0
|
|
|
|
tx_elevation = profile.samples[0].ground_m + tx_height_agl
|
|
rx_elevation = profile.samples[-1].ground_m + rx_height_agl
|
|
max_v = float("-inf")
|
|
|
|
for sample in profile.samples[1:-1]:
|
|
d1 = sample.distance_m
|
|
d2 = total_distance - d1
|
|
path_height = tx_elevation + (rx_elevation - tx_elevation) * (d1 / total_distance)
|
|
h = sample.surface_m - path_height
|
|
max_v = max(max_v, knife_edge_v(h, d1, d2, freq_hz))
|
|
|
|
return knife_edge_loss(max_v)
|
|
|
|
|
|
def deygout(
|
|
profile: SurfaceProfile,
|
|
tx_height_agl: float,
|
|
rx_height_agl: float,
|
|
freq_hz: float,
|
|
) -> float:
|
|
"""Compatibility wrapper; use Bullington equivalent loss for multi-edge profiles."""
|
|
return bullington_loss(profile, tx_height_agl, rx_height_agl, freq_hz)
|