@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)
This commit is contained in:
2026-02-01 23:12:26 +02:00
parent 1dde56705a
commit defa3ad440
71 changed files with 7134 additions and 256 deletions

View File

@@ -0,0 +1,3 @@
"""
Utility modules for RFCP backend.
"""

View File

@@ -0,0 +1,34 @@
"""
Structured logging for RFCP backend.
"""
import os
import sys
import time
import threading
_log_file = None
def rfcp_log(tag: str, msg: str):
"""Log with tag prefix, timestamp, and thread name.
Writes to stdout and a log file for reliability.
"""
global _log_file
ts = time.strftime('%H:%M:%S')
thr = threading.current_thread().name
line = f"[{tag} {ts}] [{thr}] {msg}"
print(line, flush=True)
try:
if _log_file is None:
log_dir = os.environ.get('RFCP_DATA_PATH', './data')
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, 'rfcp-backend.log')
_log_file = open(log_path, 'a')
_log_file.write(line + '\n')
_log_file.flush()
except Exception:
pass

View File

@@ -0,0 +1,44 @@
"""
Progress reporting for long-running calculations.
"""
import time
from typing import Optional, Callable, Awaitable
class ProgressTracker:
"""Track and report calculation progress."""
def __init__(
self,
total: int,
callback: Optional[Callable[[str, float, Optional[float]], Awaitable[None]]] = None,
phase: str = "calculating",
):
self.total = total
self.callback = callback
self.phase = phase
self.completed = 0
self.start_time = time.time()
@property
def progress(self) -> float:
if self.total == 0:
return 1.0
return self.completed / self.total
@property
def eta_seconds(self) -> Optional[float]:
if self.completed == 0:
return None
elapsed = time.time() - self.start_time
rate = self.completed / elapsed
remaining = self.total - self.completed
return remaining / rate if rate > 0 else None
def update(self, n: int = 1):
self.completed += n
async def report(self):
if self.callback:
await self.callback(self.phase, self.progress, self.eta_seconds)

View File

@@ -0,0 +1,54 @@
"""
RF unit conversions.
"""
import math
def dbm_to_watts(dbm: float) -> float:
"""Convert dBm to watts."""
return 10 ** ((dbm - 30) / 10)
def watts_to_dbm(watts: float) -> float:
"""Convert watts to dBm."""
if watts <= 0:
return -float('inf')
return 10 * math.log10(watts) + 30
def dbm_to_mw(dbm: float) -> float:
"""Convert dBm to milliwatts."""
return 10 ** (dbm / 10)
def mw_to_dbm(mw: float) -> float:
"""Convert milliwatts to dBm."""
if mw <= 0:
return -float('inf')
return 10 * math.log10(mw)
def frequency_to_wavelength(frequency_mhz: float) -> float:
"""Convert frequency (MHz) to wavelength (meters)."""
return 300.0 / frequency_mhz
def wavelength_to_frequency(wavelength_m: float) -> float:
"""Convert wavelength (meters) to frequency (MHz)."""
return 300.0 / wavelength_m
def eirp_dbm(power_dbm: float, gain_dbi: float) -> float:
"""Calculate EIRP in dBm."""
return power_dbm + gain_dbi
def eirp_watts(power_dbm: float, gain_dbi: float) -> float:
"""Calculate EIRP in watts."""
return dbm_to_watts(power_dbm + gain_dbi)
def path_loss_to_signal_dbm(power_dbm: float, gain_dbi: float, path_loss_db: float) -> float:
"""Calculate received signal level in dBm from EIRP and path loss."""
return power_dbm + gain_dbi - path_loss_db