Files
RadioTestApi/app/services/link_calculator.py
T
2026-06-17 13:26:35 +03:00

145 lines
4.5 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from app.models.schemas import (
GeoAntenna,
LinkBudget,
LinkResponse,
Obstacle,
PathProfilePoint,
)
from app.services.buildings import BuildingService
from app.services.canopy import CanopyService
from app.services.fresnel import clearance_m, fresnel_radius_m
from app.services.geodesy import line_distance_m, los_height_at, sample_line
from app.services.path_loss import compute_link_budget, fspl_db
from app.services.terrain import TerrainService
@dataclass
class LinkCalculator:
terrain: TerrainService
canopy: CanopyService
buildings: BuildingService
async def calculate_link(
self,
from_pt: GeoAntenna,
to_pt: GeoAntenna,
frequency_mhz: float,
fresnel_clearance: float,
tx_power_dbm: float,
rx_sensitivity_dbm: float,
sample_step_m: float,
) -> LinkResponse:
total_distance = line_distance_m(from_pt.lat, from_pt.lng, to_pt.lat, to_pt.lng)
path_points = sample_line(
from_pt.lat,
from_pt.lng,
to_pt.lat,
to_pt.lng,
sample_step_m,
)
coordinates = [(lat, lng) for lat, lng, _ in path_points]
terrains = self.terrain.get_elevations(coordinates)
canopies = self.canopy.get_canopy_heights(coordinates)
building_heights = await self.buildings.get_max_heights(coordinates)
from_terrain = self.terrain.get_elevation(from_pt.lat, from_pt.lng)
to_terrain = self.terrain.get_elevation(to_pt.lat, to_pt.lng)
from_antenna_asl = from_terrain + from_pt.antenna_height_m
to_antenna_asl = to_terrain + to_pt.antenna_height_m
profile: list[PathProfilePoint] = []
obstacles: list[Obstacle] = []
los_clear = True
fresnel_clear = True
for index, (lat, lng, distance) in enumerate(path_points):
terrain_m = terrains[index]
canopy_m = canopies[index]
building_m = building_heights[index]
obstacle_height = max(canopy_m, building_m)
total_m = terrain_m + obstacle_height
los_height = los_height_at(distance, from_antenna_asl, to_antenna_asl, total_distance)
d1 = distance
d2 = max(total_distance - distance, 0.0)
fresnel_r = fresnel_radius_m(d1, d2, total_distance, frequency_mhz)
point_clearance = clearance_m(los_height, total_m, fresnel_r, fresnel_clearance)
if los_height < total_m:
los_clear = False
if point_clearance < 0:
fresnel_clear = False
obstacles.append(
Obstacle(
lat=round(lat, 6),
lng=round(lng, 6),
distance_from_start_m=round(distance, 1),
terrain_height_m=round(terrain_m, 1),
canopy_height_m=round(canopy_m, 1),
building_height_m=round(building_m, 1),
total_height_m=round(total_m, 1),
fresnel_radius_m=round(fresnel_r, 1),
clearance_m=round(point_clearance, 1),
)
)
profile.append(
PathProfilePoint(
distance_m=round(distance, 1),
lat=round(lat, 6),
lng=round(lng, 6),
terrain_m=round(terrain_m, 1),
canopy_m=round(canopy_m, 1),
building_m=round(building_m, 1),
total_m=round(total_m, 1),
los_height_m=round(los_height, 1),
fresnel_radius_m=round(fresnel_r, 1),
clearance_m=round(point_clearance, 1),
)
)
link_budget: LinkBudget = compute_link_budget(
tx_power_dbm,
rx_sensitivity_dbm,
total_distance,
frequency_mhz,
)
return LinkResponse(
distance_m=round(total_distance, 1),
los=los_clear,
fresnel_clear=fresnel_clear,
free_space_loss_db=round(fspl_db(total_distance, frequency_mhz), 1),
link_budget=link_budget,
obstacles=obstacles,
path_profile=profile,
)
async def elevation_profile(
self,
from_lat: float,
from_lng: float,
to_lat: float,
to_lng: float,
step_m: float,
) -> tuple[float, list[dict[str, float]]]:
total_distance = line_distance_m(from_lat, from_lng, to_lat, to_lng)
path_points = sample_line(from_lat, from_lng, to_lat, to_lng, step_m)
coordinates = [(lat, lng) for lat, lng, _ in path_points]
terrains = self.terrain.get_elevations(coordinates)
profile = [
{
"distance_m": round(distance, 1),
"lat": round(lat, 6),
"lng": round(lng, 6),
"terrain_m": round(terrains[index], 1),
}
for index, (lat, lng, distance) in enumerate(path_points)
]
return round(total_distance, 1), profile