131 lines
3.3 KiB
Python
131 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from math import log10
|
|
|
|
from app.core.geo import GeoPoint, haversine
|
|
from app.core.itm import itm_path_loss, p1812_path_loss
|
|
from app.core.p452 import p452_path_loss
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LinkBudget:
|
|
distance_km: float
|
|
fspl_db: float
|
|
diffraction_db: float
|
|
vegetation_db: float
|
|
atmospheric_db: float
|
|
total_loss_db: float
|
|
rx_power_dbm: float
|
|
fade_margin_db: float
|
|
fresnel_clear: bool
|
|
link_viable: bool
|
|
|
|
|
|
def fspl(freq_mhz: float, dist_km: float) -> float:
|
|
if freq_mhz <= 0:
|
|
raise ValueError("freq_mhz must be positive")
|
|
if dist_km <= 0:
|
|
raise ValueError("dist_km must be positive")
|
|
return 32.44 + 20 * log10(freq_mhz) + 20 * log10(dist_km)
|
|
|
|
|
|
def itm_loss(
|
|
tx: GeoPoint,
|
|
rx: GeoPoint,
|
|
*,
|
|
tx_height_agl: float,
|
|
rx_height_agl: float,
|
|
freq_mhz: float,
|
|
elevation_profile_m: list[float] | None = None,
|
|
dem_path: str | None = None,
|
|
) -> float:
|
|
return itm_path_loss(
|
|
tx,
|
|
rx,
|
|
tx_height_agl=tx_height_agl,
|
|
rx_height_agl=rx_height_agl,
|
|
freq_mhz=freq_mhz,
|
|
elevation_profile_m=elevation_profile_m,
|
|
dem_path=dem_path,
|
|
)
|
|
|
|
|
|
def p1812_field(
|
|
tx: GeoPoint,
|
|
rx: GeoPoint,
|
|
*,
|
|
tx_height_agl: float,
|
|
rx_height_agl: float,
|
|
freq_mhz: float,
|
|
environment: str = "rural",
|
|
elevation_profile_m: list[float] | None = None,
|
|
dem_path: str | None = None,
|
|
) -> float:
|
|
return p1812_path_loss(
|
|
tx,
|
|
rx,
|
|
tx_height_agl=tx_height_agl,
|
|
rx_height_agl=rx_height_agl,
|
|
freq_mhz=freq_mhz,
|
|
environment=environment,
|
|
elevation_profile_m=elevation_profile_m,
|
|
dem_path=dem_path,
|
|
)
|
|
|
|
|
|
def p452_loss(
|
|
tx: GeoPoint,
|
|
rx: GeoPoint,
|
|
*,
|
|
tx_height_agl: float,
|
|
rx_height_agl: float,
|
|
freq_mhz: float,
|
|
environment: str = "rural",
|
|
elevation_profile_m: list[float] | None = None,
|
|
dem_path: str | None = None,
|
|
) -> float:
|
|
return p452_path_loss(
|
|
tx,
|
|
rx,
|
|
tx_height_agl=tx_height_agl,
|
|
rx_height_agl=rx_height_agl,
|
|
freq_mhz=freq_mhz,
|
|
environment=environment,
|
|
elevation_profile_m=elevation_profile_m,
|
|
dem_path=dem_path,
|
|
)
|
|
|
|
|
|
def manual_link_budget(
|
|
tx: GeoPoint,
|
|
rx: GeoPoint,
|
|
tx_power_dbm: float,
|
|
tx_gain_dbi: float,
|
|
rx_gain_dbi: float,
|
|
sensitivity_dbm: float,
|
|
frequency_mhz: float,
|
|
diffraction_db: float = 0.0,
|
|
vegetation_db: float = 0.0,
|
|
atmospheric_db: float = 0.0,
|
|
misc_loss_db: float = 0.0,
|
|
fresnel_clear: bool = True,
|
|
) -> LinkBudget:
|
|
distance_km = haversine(tx, rx) / 1000.0
|
|
free_space_loss = fspl(frequency_mhz, distance_km)
|
|
total_loss = free_space_loss + diffraction_db + vegetation_db + atmospheric_db + misc_loss_db
|
|
rx_power = tx_power_dbm + tx_gain_dbi + rx_gain_dbi - total_loss
|
|
margin = rx_power - sensitivity_dbm
|
|
return LinkBudget(
|
|
distance_km=distance_km,
|
|
fspl_db=free_space_loss,
|
|
diffraction_db=diffraction_db,
|
|
vegetation_db=vegetation_db,
|
|
atmospheric_db=atmospheric_db,
|
|
total_loss_db=total_loss,
|
|
rx_power_dbm=rx_power,
|
|
fade_margin_db=margin,
|
|
fresnel_clear=fresnel_clear,
|
|
link_viable=margin > 0,
|
|
)
|