@mytec: iter2.4 ready for testing

This commit is contained in:
2026-02-01 10:48:23 +02:00
parent 7893c57bc9
commit 5488633e43
19 changed files with 1448 additions and 69 deletions

View File

@@ -55,6 +55,7 @@ from app.services.indoor_service import indoor_service
from app.services.atmospheric_service import atmospheric_service
from app.services.parallel_coverage_service import (
calculate_coverage_parallel, get_cpu_count, get_parallel_backend,
CancellationToken,
)
@@ -280,7 +281,8 @@ class CoverageService:
async def calculate_coverage(
self,
site: SiteParams,
settings: CoverageSettings
settings: CoverageSettings,
cancel_token: Optional[CancellationToken] = None,
) -> List[CoveragePoint]:
"""
Calculate coverage grid for a single site
@@ -352,6 +354,32 @@ class CoverageService:
f"pre-computed {len(grid)} elevations")
_clog(f"━━━ PHASE 2 done: {terrain_time:.1f}s ━━━")
# ━━━ PHASE 2.5: Vectorized pre-computation (GPU/NumPy) ━━━
from app.services.gpu_service import gpu_service
t_gpu = time.time()
grid_lats = np.array([lat for lat, lon in grid])
grid_lons = np.array([lon for lat, lon in grid])
pre_distances = gpu_service.precompute_distances(
grid_lats, grid_lons, site.lat, site.lon
)
pre_path_loss = gpu_service.precompute_path_loss(
pre_distances, site.frequency, site.height
)
# Build lookup dict for point loop
precomputed = {}
for i, (lat, lon) in enumerate(grid):
precomputed[(lat, lon)] = {
'distance': float(pre_distances[i]),
'path_loss': float(pre_path_loss[i]),
}
gpu_time = time.time() - t_gpu
_clog(f"━━━ PHASE 2.5: Vectorized pre-computation done: {gpu_time:.3f}s "
f"({len(grid)} points, backend={'GPU' if gpu_service.available else 'CPU/NumPy'}) ━━━")
# ━━━ PHASE 3: Point calculation ━━━
dominant_path_service._log_count = 0 # Reset diagnostic counter
t_points = time.time()
@@ -368,12 +396,15 @@ class CoverageService:
loop = asyncio.get_event_loop()
result_dicts, timing = await loop.run_in_executor(
None,
calculate_coverage_parallel,
grid, point_elevations,
site.model_dump(), settings.model_dump(),
self.terrain._tile_cache,
buildings, streets, water_bodies, vegetation_areas,
site_elevation, num_workers, _clog,
lambda: calculate_coverage_parallel(
grid, point_elevations,
site.model_dump(), settings.model_dump(),
self.terrain._tile_cache,
buildings, streets, water_bodies, vegetation_areas,
site_elevation, num_workers, _clog,
cancel_token=cancel_token,
precomputed=precomputed,
),
)
# Convert dicts back to CoveragePoint objects
@@ -389,10 +420,13 @@ class CoverageService:
loop = asyncio.get_event_loop()
points, timing = await loop.run_in_executor(
None,
self._run_point_loop,
grid, site, settings, buildings, streets,
spatial_idx, water_bodies, vegetation_areas,
site_elevation, point_elevations
lambda: self._run_point_loop(
grid, site, settings, buildings, streets,
spatial_idx, water_bodies, vegetation_areas,
site_elevation, point_elevations,
cancel_token=cancel_token,
precomputed=precomputed,
),
)
points_time = time.time() - t_points
@@ -423,7 +457,8 @@ class CoverageService:
async def calculate_multi_site_coverage(
self,
sites: List[SiteParams],
settings: CoverageSettings
settings: CoverageSettings,
cancel_token: Optional[CancellationToken] = None,
) -> List[CoveragePoint]:
"""
Calculate combined coverage from multiple sites
@@ -437,7 +472,7 @@ class CoverageService:
# Get all individual coverages
all_coverages = await asyncio.gather(*[
self.calculate_coverage(site, settings)
self.calculate_coverage(site, settings, cancel_token)
for site in sites
])
@@ -485,7 +520,8 @@ class CoverageService:
def _run_point_loop(
self, grid, site, settings, buildings, streets,
spatial_idx, water_bodies, vegetation_areas,
site_elevation, point_elevations
site_elevation, point_elevations,
cancel_token=None, precomputed=None,
):
"""Sync point loop - runs in ThreadPoolExecutor, bypasses event loop."""
points = []
@@ -496,14 +532,22 @@ class CoverageService:
log_interval = max(1, total // 20)
for i, (lat, lon) in enumerate(grid):
if cancel_token and cancel_token.is_cancelled:
_clog(f"Cancelled at {i}/{total}")
break
if i % log_interval == 0:
_clog(f"Progress: {i}/{total} ({i*100//total}%)")
pre = precomputed.get((lat, lon)) if precomputed else None
point = self._calculate_point_sync(
site, lat, lon, settings, buildings, streets,
spatial_idx, water_bodies, vegetation_areas,
site_elevation, point_elevations.get((lat, lon), 0.0),
timing
timing,
precomputed_distance=pre.get('distance') if pre else None,
precomputed_path_loss=pre.get('path_loss') if pre else None,
)
if point.rsrp >= settings.min_signal:
points.append(point)
@@ -523,17 +567,25 @@ class CoverageService:
vegetation_areas: List[VegetationArea],
site_elevation: float,
point_elevation: float,
timing: dict
timing: dict,
precomputed_distance: Optional[float] = None,
precomputed_path_loss: Optional[float] = None,
) -> CoveragePoint:
"""Fully synchronous point calculation. All terrain tiles must be pre-loaded."""
# Distance
distance = TerrainService.haversine_distance(site.lat, site.lon, lat, lon)
# Distance (use precomputed if available)
if precomputed_distance is not None:
distance = precomputed_distance
else:
distance = TerrainService.haversine_distance(site.lat, site.lon, lat, lon)
if distance < 1:
distance = 1
# Base path loss
path_loss = self._okumura_hata(distance, site.frequency, site.height, 1.5)
# Base path loss (use precomputed if available)
if precomputed_path_loss is not None:
path_loss = precomputed_path_loss
else:
path_loss = self._okumura_hata(distance, site.frequency, site.height, 1.5)
# Antenna pattern
antenna_loss = 0.0