Files
rfcp/backend/app/core/calculator.py
mytec defa3ad440 @mytec: feat: Phase 3.0 Architecture Refactor
Major refactoring of RFCP backend:
- Modular propagation models (8 models)
- SharedMemoryManager for terrain data
- ProcessPoolExecutor parallel processing
- WebSocket progress streaming
- Building filtering pipeline (351k → 15k)
- 82 unit tests

Performance: Standard preset 38s → 5s (7.6x speedup)

Known issue: Detailed preset timeout (fix in 3.1.0)
2026-02-01 23:12:26 +02:00

104 lines
3.5 KiB
Python

"""
Point calculator — coordinates per-point propagation calculation.
"""
import math
from typing import Optional
from app.propagation.base import PropagationModel, PropagationInput
from app.propagation.itu_r_p526 import KnifeEdgeDiffractionModel
from app.core.result import PointResult
class PointCalculator:
"""Calculates propagation for individual grid points."""
def __init__(self, model: PropagationModel, environment: str = "urban"):
self.model = model
self.environment = environment
self.diffraction = KnifeEdgeDiffractionModel()
def calculate_point(
self,
site_lat: float, site_lon: float, site_height: float,
site_power: float, site_gain: float, site_frequency: float,
point_lat: float, point_lon: float,
distance: float,
has_los: bool = True,
terrain_clearance: Optional[float] = None,
building_loss: float = 0.0,
extra_loss: float = 0.0,
azimuth: Optional[float] = None,
beamwidth: float = 360,
) -> PointResult:
if distance < 1:
distance = 1
prop_input = PropagationInput(
frequency_mhz=site_frequency,
distance_m=distance,
tx_height_m=site_height,
rx_height_m=1.5,
environment=self.environment,
)
if self.model.is_valid_for(prop_input):
output = self.model.calculate(prop_input)
path_loss = output.path_loss_db
else:
from app.propagation.free_space import FreeSpaceModel
output = FreeSpaceModel().calculate(prop_input)
path_loss = output.path_loss_db
antenna_loss = 0.0
if azimuth is not None and beamwidth < 360:
antenna_loss = self._antenna_pattern_loss(
site_lat, site_lon, point_lat, point_lon, azimuth, beamwidth,
)
terrain_loss = 0.0
if terrain_clearance is not None and terrain_clearance < 0:
terrain_loss = self.diffraction.calculate_clearance_loss(
terrain_clearance, site_frequency,
)
has_los = False
rsrp = (
site_power + site_gain
- path_loss - antenna_loss
- terrain_loss - building_loss - extra_loss
)
return PointResult(
lat=point_lat, lon=point_lon, rsrp=rsrp,
distance=distance, path_loss=path_loss,
terrain_loss=terrain_loss, building_loss=building_loss,
diffraction_loss=terrain_loss, has_los=has_los,
model_used=self.model.name,
)
@staticmethod
def _antenna_pattern_loss(
site_lat: float, site_lon: float,
point_lat: float, point_lon: float,
azimuth: float, beamwidth: float,
) -> float:
lat1, lon1 = math.radians(site_lat), math.radians(site_lon)
lat2, lon2 = math.radians(point_lat), math.radians(point_lon)
dlon = lon2 - lon1
x = math.sin(dlon) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(dlon)
bearing = (math.degrees(math.atan2(x, y)) + 360) % 360
angle_diff = abs(bearing - azimuth)
if angle_diff > 180:
angle_diff = 360 - angle_diff
half_bw = beamwidth / 2
if angle_diff <= half_bw:
loss = 3 * (angle_diff / half_bw) ** 2
else:
loss = 3 + 12 * ((angle_diff - half_bw) / half_bw) ** 2
loss = min(loss, 25)
return loss