from __future__ import annotations import numpy as np from astropy import units as u from pycraf import pathprof from pyproj import Geod from app.config import get_settings from app.core.dem import elevations_along from app.core.geo import GeoPoint, haversine, sample_path _GEOD = Geod(ellps="WGS84") def _clutter_zone(environment: str) -> pathprof.CLUTTER: mapping = { "urban": pathprof.CLUTTER.URBAN, "suburban": pathprof.CLUTTER.SUBURBAN, "rural": pathprof.CLUTTER.SPARSE, } return mapping.get(environment, pathprof.CLUTTER.SPARSE) def _height_profile( tx: GeoPoint, rx: GeoPoint, *, dem_path: str | None = None, samples: int = 64, ) -> tuple[np.ndarray, np.ndarray, float, float]: points = sample_path(tx, rx, samples) try: heights = elevations_along(points, dem_path=dem_path or get_settings().dem_path) except Exception: heights = np.zeros(len(points), dtype=float) dists = np.array([point.distance_m for point in points], dtype=float) * u.m return dists, heights * u.m, points[0].lon, points[0].lat def p452_path_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: distance_km = haversine(tx, rx) / 1000.0 if elevation_profile_m is not None: count = len(elevation_profile_m) dists = np.linspace(0, distance_km * 1000, count) * u.m heights = np.array(elevation_profile_m, dtype=float) * u.m bearing, _, _ = _GEOD.inv(tx.lon, tx.lat, rx.lon, rx.lat) backbearing = (bearing + 180) % 360 else: dists, heights, _, _ = _height_profile(tx, rx, dem_path=dem_path) bearing, _, _ = _GEOD.inv(tx.lon, tx.lat, rx.lon, rx.lat) backbearing = (bearing + 180) % 360 pathprop = pathprof.PathProp( freq_mhz * u.MHz, 293 * u.K, 1013 * u.hPa, tx.lon * u.deg, tx.lat * u.deg, rx.lon * u.deg, rx.lat * u.deg, tx_height_agl * u.m, rx_height_agl * u.m, max(100.0, distance_km * 10) * u.m, 50 * u.percent, zone_t=_clutter_zone(environment), zone_r=_clutter_zone(environment), hprof_dists=dists, hprof_heights=heights, hprof_bearing=bearing * u.deg, hprof_backbearing=backbearing * u.deg, ) losses = pathprof.loss_complete(pathprop) total = losses[0] return float(total.value)