from __future__ import annotations from dataclasses import dataclass from math import log10 from app.core.geo import GeoPoint, haversine @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(*args: object, **kwargs: object) -> float: raise NotImplementedError("Longley-Rice ITM integration is implemented in a later stage") def p1812_field(*args: object, **kwargs: object) -> float: raise NotImplementedError("ITU-R P.1812 integration is implemented in a later stage") 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, )