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, )