from __future__ import annotations import math import numpy as np from itmlogic.misc.qerfi import qerfi from itmlogic.preparatory_subroutines.qlrpfl import qlrpfl from itmlogic.statistics.avar import avar from app.config import get_settings from app.core.dem import elevations_along from app.core.geo import GeoPoint, haversine, sample_path _DB = 8.685890 def _climate_code(environment: str) -> int: mapping = { "urban": 5, "suburban": 6, "rural": 7, } return mapping.get(environment, 7) def _build_prop( *, freq_mhz: float, distance_km: float, tx_height_m: float, rx_height_m: float, surface_profile_m: list[float], climate: int, ) -> dict: prop: dict = { "fmhz": freq_mhz, "d": distance_km, "hg": [tx_height_m, rx_height_m], "ipol": 0, "eps": 15, "sgm": 0.005, "klim": climate, "ens0": 314, "lvar": 5, "gma": 157e-9, "kwx": 0, "wn": freq_mhz / 47.7, "ens": 314, "mdvarx": 11, "klimx": 0, } pfl = [len(surface_profile_m) - 1, 0, *surface_profile_m] if pfl[0] > 0: pfl[1] = distance_km * 1000 / pfl[0] prop["pfl"] = pfl prop["gme"] = prop["gma"] * (1 - 0.04665 * math.exp(prop["ens"] / 179.3)) zq = complex(prop["eps"], 376.62 * prop["sgm"] / prop["wn"]) prop["zgnd"] = np.sqrt(zq - 1) prop = qlrpfl(prop) return prop def itm_path_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, climate: int = 5, ) -> float: distance_km = haversine(tx, rx) / 1000.0 if elevation_profile_m is None: points = sample_path(tx, rx, 64) try: elevation_profile_m = elevations_along( points, dem_path=dem_path or get_settings().dem_path, ).tolist() except Exception: elevation_profile_m = [0.0, 0.0] profile = [float(value) for value in elevation_profile_m] if profile: profile[0] = profile[0] + tx_height_agl profile[-1] = profile[-1] + rx_height_agl prop = _build_prop( freq_mhz=freq_mhz, distance_km=distance_km, tx_height_m=profile[0] if profile else tx_height_agl, rx_height_m=profile[-1] if profile else rx_height_agl, surface_profile_m=profile, climate=climate, ) fs = _DB * math.log(2 * prop["wn"] * prop["dist"]) zr = qerfi([0.5]) zc = qerfi([0.5]) correction, _ = avar(zr[0], 0, zc[0], prop) return float(fs + correction) def p1812_path_loss( tx: GeoPoint, rx: GeoPoint, *, tx_height_agl: float, rx_height_agl: float, freq_mhz: float, environment: str, elevation_profile_m: list[float] | None = None, dem_path: str | None = None, ) -> float: climate = _climate_code(environment) base_loss = 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, climate=climate, ) clutter_db = { "urban": 8.0, "suburban": 4.0, "rural": 0.0, }.get(environment, 0.0) return base_loss + clutter_db