@mytec: iter3.4.0 ready for testing
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import gc
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
@@ -426,18 +427,28 @@ class CoverageService:
|
||||
settings: CoverageSettings,
|
||||
cancel_token: Optional[CancellationToken] = None,
|
||||
progress_fn: Optional[Callable[[str, float], None]] = None,
|
||||
tile_callback: Optional[Callable] = None,
|
||||
) -> List[CoveragePoint]:
|
||||
"""
|
||||
Calculate coverage grid for a single site
|
||||
|
||||
Returns list of CoveragePoint with RSRP values.
|
||||
progress_fn(phase, pct): optional callback for progress updates (0.0-1.0).
|
||||
tile_callback(points, tile_idx, total_tiles): optional callback for per-tile
|
||||
partial results when using tiled processing (radius > 10km).
|
||||
"""
|
||||
calc_start = time.time()
|
||||
|
||||
# Apply preset if specified
|
||||
settings = apply_preset(settings)
|
||||
|
||||
# ── Tiled processing for large radius ──
|
||||
from app.services.tile_processor import TILE_THRESHOLD_M
|
||||
if settings.radius > TILE_THRESHOLD_M:
|
||||
return await self.calculate_coverage_tiled(
|
||||
site, settings, cancel_token, progress_fn, tile_callback
|
||||
)
|
||||
|
||||
points = []
|
||||
|
||||
# Generate grid
|
||||
@@ -660,12 +671,14 @@ class CoverageService:
|
||||
settings: CoverageSettings,
|
||||
cancel_token: Optional[CancellationToken] = None,
|
||||
progress_fn: Optional[Callable[[str, float], None]] = None,
|
||||
tile_callback: Optional[Callable] = None,
|
||||
) -> List[CoveragePoint]:
|
||||
"""
|
||||
Calculate combined coverage from multiple sites
|
||||
Best server (strongest signal) wins at each point
|
||||
|
||||
progress_fn(phase, pct): optional callback for progress updates (0.0-1.0).
|
||||
tile_callback: forwarded to calculate_coverage for progressive results.
|
||||
"""
|
||||
if not sites:
|
||||
return []
|
||||
@@ -691,6 +704,7 @@ class CoverageService:
|
||||
self.calculate_coverage(
|
||||
site, settings, cancel_token,
|
||||
progress_fn=_make_site_progress(i) if progress_fn else None,
|
||||
tile_callback=tile_callback,
|
||||
)
|
||||
for i, site in enumerate(sites)
|
||||
])
|
||||
@@ -707,6 +721,208 @@ class CoverageService:
|
||||
|
||||
return list(point_map.values())
|
||||
|
||||
async def calculate_coverage_tiled(
|
||||
self,
|
||||
site: SiteParams,
|
||||
settings: CoverageSettings,
|
||||
cancel_token: Optional[CancellationToken] = None,
|
||||
progress_fn: Optional[Callable[[str, float], None]] = None,
|
||||
tile_callback: Optional[Callable] = None,
|
||||
) -> List[CoveragePoint]:
|
||||
"""Tile-based coverage for large radius (>10km).
|
||||
|
||||
Splits the coverage area into 5km sub-tiles. Each tile loads its
|
||||
own OSM data and terrain, processes its grid points, then frees
|
||||
memory before moving to the next tile. This keeps peak RAM
|
||||
bounded regardless of total coverage area.
|
||||
|
||||
tile_callback(points, tile_idx, total_tiles): async callback
|
||||
invoked with partial results after each tile completes.
|
||||
"""
|
||||
from app.services.tile_processor import (
|
||||
generate_tile_grid, partition_grid_to_tiles, get_adaptive_worker_count,
|
||||
)
|
||||
|
||||
calc_start = time.time()
|
||||
|
||||
# NOTE: settings already has preset applied by calculate_coverage()
|
||||
|
||||
# Generate full adaptive grid (lightweight — just coordinate tuples)
|
||||
grid = self._generate_grid(
|
||||
site.lat, site.lon, settings.radius, settings.resolution,
|
||||
)
|
||||
_clog(f"Tiled mode: {len(grid)} total grid points, radius={settings.radius}m")
|
||||
|
||||
# Generate tiles and partition grid points
|
||||
tiles = generate_tile_grid(site.lat, site.lon, settings.radius)
|
||||
total_tiles = len(tiles)
|
||||
tile_grids = partition_grid_to_tiles(grid, tiles)
|
||||
_clog(f"Generated {total_tiles} tiles")
|
||||
|
||||
# Free full grid reference
|
||||
del grid
|
||||
|
||||
site_elevation: Optional[float] = None
|
||||
all_points: List[CoveragePoint] = []
|
||||
|
||||
for tile_idx, tile in enumerate(tiles):
|
||||
if cancel_token and cancel_token.is_cancelled:
|
||||
_clog("Tiled calculation cancelled")
|
||||
break
|
||||
|
||||
tile_grid = tile_grids.get(tile.index, [])
|
||||
if not tile_grid:
|
||||
continue
|
||||
|
||||
tile_start = time.time()
|
||||
min_lat, min_lon, max_lat, max_lon = tile.bbox
|
||||
_clog(f"━━━ Tile {tile_idx + 1}/{total_tiles}: "
|
||||
f"{len(tile_grid)} points ━━━")
|
||||
|
||||
# Per-tile progress mapped to overall progress range
|
||||
def _tile_progress(phase: str, pct: float, _idx=tile_idx):
|
||||
if progress_fn:
|
||||
overall = (_idx + pct) / total_tiles
|
||||
progress_fn(
|
||||
f"Tile {_idx + 1}/{total_tiles}: {phase}", overall,
|
||||
)
|
||||
|
||||
# ── 1. Fetch OSM data for this tile ──
|
||||
_tile_progress("Fetching map data", 0.10)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
osm_data = await self._fetch_osm_grid_aligned(
|
||||
min_lat, min_lon, max_lat, max_lon, settings,
|
||||
)
|
||||
|
||||
buildings = _filter_buildings_to_bbox(
|
||||
osm_data["buildings"], min_lat, min_lon, max_lat, max_lon,
|
||||
site.lat, site.lon, _clog,
|
||||
)
|
||||
streets = _filter_osm_list_to_bbox(
|
||||
osm_data["streets"], min_lat, min_lon, max_lat, max_lon,
|
||||
)
|
||||
water_bodies = _filter_osm_list_to_bbox(
|
||||
osm_data["water_bodies"], min_lat, min_lon, max_lat, max_lon,
|
||||
)
|
||||
vegetation_areas = _filter_osm_list_to_bbox(
|
||||
osm_data["vegetation_areas"], min_lat, min_lon, max_lat, max_lon,
|
||||
max_count=5000,
|
||||
)
|
||||
|
||||
spatial_idx: Optional[SpatialIndex] = None
|
||||
if buildings:
|
||||
cache_key = f"tile_{tile_idx}_{min_lat:.3f},{min_lon:.3f}"
|
||||
spatial_idx = get_spatial_index(cache_key, buildings)
|
||||
|
||||
# ── 2. Pre-load terrain for this tile ──
|
||||
_tile_progress("Loading terrain", 0.25)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
tile_names = await self.terrain.ensure_tiles_for_bbox(
|
||||
min_lat, min_lon, max_lat, max_lon,
|
||||
)
|
||||
for tn in tile_names:
|
||||
self.terrain._load_tile(tn)
|
||||
|
||||
if site_elevation is None:
|
||||
site_elevation = self.terrain.get_elevation_sync(
|
||||
site.lat, site.lon,
|
||||
)
|
||||
|
||||
point_elevations = {}
|
||||
for lat, lon in tile_grid:
|
||||
point_elevations[(lat, lon)] = self.terrain.get_elevation_sync(
|
||||
lat, lon,
|
||||
)
|
||||
|
||||
# ── 3. Precompute distances / path loss ──
|
||||
_tile_progress("Pre-computing propagation", 0.35)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
from app.services.gpu_service import gpu_service
|
||||
|
||||
grid_lats = np.array([lat for lat, _lon in tile_grid])
|
||||
grid_lons = np.array([_lon for _lat, _lon in tile_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,
|
||||
environment=getattr(settings, 'environment', 'urban'),
|
||||
)
|
||||
|
||||
precomputed = {}
|
||||
for i, (lat, lon) in enumerate(tile_grid):
|
||||
precomputed[(lat, lon)] = {
|
||||
'distance': float(pre_distances[i]),
|
||||
'path_loss': float(pre_path_loss[i]),
|
||||
}
|
||||
|
||||
# ── 4. Calculate points (parallel with adaptive workers) ──
|
||||
_tile_progress("Calculating coverage", 0.40)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
num_workers = get_adaptive_worker_count(
|
||||
settings.radius, get_cpu_count(),
|
||||
)
|
||||
use_parallel = len(tile_grid) > 100 and num_workers > 1
|
||||
|
||||
if use_parallel:
|
||||
loop = asyncio.get_event_loop()
|
||||
result_dicts, _timing = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: calculate_coverage_parallel(
|
||||
tile_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,
|
||||
),
|
||||
)
|
||||
tile_points = [CoveragePoint(**d) for d in result_dicts]
|
||||
else:
|
||||
loop = asyncio.get_event_loop()
|
||||
tile_points, _timing = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: self._run_point_loop(
|
||||
tile_grid, site, settings, buildings, streets,
|
||||
spatial_idx, water_bodies, vegetation_areas,
|
||||
site_elevation, point_elevations,
|
||||
cancel_token=cancel_token,
|
||||
precomputed=precomputed,
|
||||
),
|
||||
)
|
||||
|
||||
all_points.extend(tile_points)
|
||||
|
||||
# Send partial results via callback
|
||||
if tile_callback and tile_points:
|
||||
await tile_callback(tile_points, tile_idx, total_tiles)
|
||||
|
||||
tile_time = time.time() - tile_start
|
||||
_clog(f"Tile {tile_idx + 1}/{total_tiles} done: "
|
||||
f"{len(tile_points)} points in {tile_time:.1f}s")
|
||||
|
||||
# ── 5. Free memory ──
|
||||
del buildings, streets, water_bodies, vegetation_areas
|
||||
del osm_data, spatial_idx, point_elevations, precomputed
|
||||
del pre_distances, pre_path_loss, grid_lats, grid_lons
|
||||
gc.collect()
|
||||
|
||||
total_time = time.time() - calc_start
|
||||
_clog(f"━━━ Tiled calculation complete: "
|
||||
f"{len(all_points)} points in {total_time:.1f}s ━━━")
|
||||
|
||||
if progress_fn:
|
||||
progress_fn("Finalizing", 0.95)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
return all_points
|
||||
|
||||
# Adaptive resolution zone boundaries (meters)
|
||||
_ADAPTIVE_ZONES = [
|
||||
(0, 2000), # Inner: full user resolution
|
||||
|
||||
Reference in New Issue
Block a user