@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

@@ -12,6 +12,7 @@ from app.services.coverage_service import (
apply_preset,
PRESETS,
)
from app.services.parallel_coverage_service import CancellationToken
router = APIRouter()
@@ -59,6 +60,7 @@ async def calculate_coverage(request: CoverageRequest) -> CoverageResponse:
# Time the calculation
start_time = time.time()
cancel_token = CancellationToken()
try:
# Calculate with 5-minute timeout
@@ -66,7 +68,8 @@ async def calculate_coverage(request: CoverageRequest) -> CoverageResponse:
points = await asyncio.wait_for(
coverage_service.calculate_coverage(
request.sites[0],
request.settings
request.settings,
cancel_token,
),
timeout=300.0
)
@@ -74,12 +77,17 @@ async def calculate_coverage(request: CoverageRequest) -> CoverageResponse:
points = await asyncio.wait_for(
coverage_service.calculate_multi_site_coverage(
request.sites,
request.settings
request.settings,
cancel_token,
),
timeout=300.0
)
except asyncio.TimeoutError:
cancel_token.cancel()
raise HTTPException(408, "Calculation timeout (5 min) — try smaller radius or lower resolution")
except asyncio.CancelledError:
cancel_token.cancel()
raise HTTPException(499, "Client disconnected")
computation_time = time.time() - start_time

View File

@@ -21,25 +21,24 @@ async def get_system_info():
except Exception:
pass
# Check GPU
gpu_info = None
try:
import cupy as cp
if cp.cuda.runtime.getDeviceCount() > 0:
props = cp.cuda.runtime.getDeviceProperties(0)
gpu_info = {
"name": props["name"].decode(),
"memory_mb": props["totalGlobalMem"] // (1024 * 1024),
}
except Exception:
pass
# Check GPU via gpu_service
from app.services.gpu_service import gpu_service
gpu_info = gpu_service.get_info()
# Determine parallel backend
if ray_available:
parallel_backend = "ray"
elif cpu_cores > 1:
parallel_backend = "process_pool"
else:
parallel_backend = "sequential"
return {
"cpu_cores": cpu_cores,
"parallel_workers": min(cpu_cores, 14),
"parallel_backend": "ray" if ray_available else "sequential",
"parallel_backend": parallel_backend,
"ray_available": ray_available,
"ray_initialized": ray_initialized,
"gpu": gpu_info,
"gpu_enabled": gpu_info is not None,
"gpu_available": gpu_info.get("available", False),
}

View File

@@ -1,4 +1,6 @@
import os
import asyncio
import math
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import FileResponse
@@ -11,6 +13,46 @@ from app.services.los_service import los_service
router = APIRouter()
def _build_elevation_grid(min_lat, max_lat, min_lon, max_lon, resolution):
"""Build a 2D elevation grid. Runs in thread executor (CPU-bound)."""
import numpy as np
rows = min(resolution, 200)
cols = min(resolution, 200)
lats = np.linspace(max_lat, min_lat, rows) # north to south
lons = np.linspace(min_lon, max_lon, cols)
grid = []
min_elev = float('inf')
max_elev = float('-inf')
for lat in lats:
row = []
for lon in lons:
elev = terrain_service.get_elevation_sync(float(lat), float(lon))
row.append(elev)
if elev < min_elev:
min_elev = elev
if elev > max_elev:
max_elev = elev
grid.append(row)
return {
"grid": grid,
"rows": rows,
"cols": cols,
"min_elevation": min_elev if min_elev != float('inf') else 0,
"max_elevation": max_elev if max_elev != float('-inf') else 0,
"bbox": {
"min_lat": min_lat,
"max_lat": max_lat,
"min_lon": min_lon,
"max_lon": max_lon,
},
}
@router.get("/elevation")
async def get_elevation(
lat: float = Query(..., ge=-90, le=90, description="Latitude"),
@@ -26,6 +68,42 @@ async def get_elevation(
}
@router.get("/elevation-grid")
async def get_elevation_grid(
min_lat: float = Query(..., ge=-90, le=90, description="South boundary"),
max_lat: float = Query(..., ge=-90, le=90, description="North boundary"),
min_lon: float = Query(..., ge=-180, le=180, description="West boundary"),
max_lon: float = Query(..., ge=-180, le=180, description="East boundary"),
resolution: int = Query(100, ge=10, le=200, description="Grid size (rows/cols)"),
):
"""Get elevation grid for a bounding box. Returns a 2D array for terrain visualization."""
if max_lat <= min_lat or max_lon <= min_lon:
raise HTTPException(400, "Invalid bbox: max must be greater than min")
if (max_lat - min_lat) > 2.0 or (max_lon - min_lon) > 2.0:
raise HTTPException(400, "Bbox too large (max 2 degrees per axis)")
# Ensure terrain tiles are loaded for this area
await terrain_service.ensure_tiles_for_bbox(min_lat, min_lon, max_lat, max_lon)
# Pre-load all tiles that cover the bbox
lat_start = int(math.floor(min_lat))
lat_end = int(math.floor(max_lat))
lon_start = int(math.floor(min_lon))
lon_end = int(math.floor(max_lon))
for lat_i in range(lat_start, lat_end + 1):
for lon_i in range(lon_start, lon_end + 1):
tile_name = terrain_service.get_tile_name(lat_i + 0.5, lon_i + 0.5)
terrain_service._load_tile(tile_name)
# Build grid in thread executor (CPU-bound sync calls)
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, _build_elevation_grid,
min_lat, max_lat, min_lon, max_lon, resolution,
)
return result
@router.get("/profile")
async def get_elevation_profile(
lat1: float = Query(..., description="Start latitude"),
@@ -87,9 +165,9 @@ async def check_fresnel_clearance(
@router.get("/tiles")
async def list_cached_tiles():
"""List cached SRTM tiles"""
tiles = list(terrain_service.cache_dir.glob("*.hgt"))
tiles = list(terrain_service.terrain_path.glob("*.hgt"))
return {
"cache_dir": str(terrain_service.cache_dir),
"cache_dir": str(terrain_service.terrain_path),
"tiles": [t.stem for t in tiles],
"count": len(tiles)
}