Files
RadioPropagationApi/api/app/core/itm.py
T
2026-06-24 07:50:00 +03:00

134 lines
3.3 KiB
Python

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