@mytec: iter3.7.0 start, gpu calc int

This commit is contained in:
2026-02-03 22:41:08 +02:00
parent a61753c642
commit 6cd9d869cc
29 changed files with 2288 additions and 28 deletions

View File

@@ -14,6 +14,7 @@ from app.services.coverage_service import (
select_propagation_model,
)
from app.services.parallel_coverage_service import CancellationToken
from app.services.boundary_service import calculate_coverage_boundary
router = APIRouter()
@@ -24,6 +25,12 @@ class CoverageRequest(BaseModel):
settings: CoverageSettings = CoverageSettings()
class BoundaryPoint(BaseModel):
"""Single boundary coordinate"""
lat: float
lon: float
class CoverageResponse(BaseModel):
"""Coverage calculation response"""
points: List[CoveragePoint]
@@ -32,6 +39,7 @@ class CoverageResponse(BaseModel):
stats: dict
computation_time: float # seconds
models_used: List[str] # which models were active
boundary: Optional[List[BoundaryPoint]] = None # coverage boundary polygon
@router.post("/calculate")
@@ -131,13 +139,24 @@ async def calculate_coverage(request: CoverageRequest) -> CoverageResponse:
"points_with_atmospheric_loss": sum(1 for p in points if p.atmospheric_loss > 0),
}
# Calculate coverage boundary
boundary = None
if points:
boundary_coords = calculate_coverage_boundary(
[p.model_dump() for p in points],
threshold_dbm=request.settings.min_signal,
)
if boundary_coords:
boundary = [BoundaryPoint(**c) for c in boundary_coords]
return CoverageResponse(
points=points,
count=len(points),
settings=effective_settings,
stats=stats,
computation_time=round(computation_time, 2),
models_used=models_used
models_used=models_used,
boundary=boundary,
)

View File

@@ -1,12 +1,29 @@
import sys
import platform
from fastapi import APIRouter, Depends
from app.api.deps import get_db
from app.services.gpu_backend import gpu_manager
router = APIRouter()
@router.get("/")
async def health_check():
return {"status": "ok", "service": "rfcp-backend", "version": "1.1.0"}
gpu_info = gpu_manager.get_status()
return {
"status": "ok",
"service": "rfcp-backend",
"version": "3.6.0",
"build": "gpu" if gpu_info.get("gpu_available") else "cpu",
"gpu": {
"available": gpu_info.get("gpu_available", False),
"backend": gpu_info.get("active_backend", "cpu"),
"device": gpu_info.get("active_device", {}).get("name") if gpu_info.get("active_device") else "CPU",
},
"python": sys.version.split()[0],
"platform": platform.system(),
}
@router.get("/db")

View File

@@ -1,4 +1,6 @@
from contextlib import asynccontextmanager
from contextlib import asynccontextmanager
import logging
import platform
from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
@@ -7,9 +9,54 @@ from app.core.database import connect_to_mongo, close_mongo_connection
from app.api.routes import health, projects, terrain, coverage, regions, system, gpu
from app.api.websocket import websocket_endpoint
logger = logging.getLogger("rfcp.startup")
def check_gpu_availability():
"""Log GPU status on startup for debugging."""
is_wsl = "microsoft" in platform.release().lower()
env_note = " (WSL2)" if is_wsl else ""
# Check CuPy / CUDA
try:
import cupy as cp
device_count = cp.cuda.runtime.getDeviceCount()
if device_count > 0:
props = cp.cuda.runtime.getDeviceProperties(0)
name = props["name"]
if isinstance(name, bytes):
name = name.decode()
mem_mb = props["totalGlobalMem"] // (1024 * 1024)
logger.info(f"GPU detected{env_note}: {name} ({mem_mb} MB VRAM)")
logger.info(f"CuPy {cp.__version__}, CUDA devices: {device_count}")
else:
logger.warning(f"CuPy installed but no CUDA devices found{env_note}")
except Exception as e:
logger.warning(f"CuPy FAILED {env_note}: {e}")
if is_wsl:
logger.warning("Install: pip3 install cupy-cuda12x --break-system-packages")
else:
logger.warning("Install: pip install cupy-cuda12x")
except Exception as e:
logger.warning(f"CuPy error{env_note}: {e}")
# Check PyOpenCL
try:
import pyopencl as cl
platforms = cl.get_platforms()
for p in platforms:
for d in p.get_devices():
logger.info(f"OpenCL device: {d.name.strip()}")
except Exception as e:
logger.debug("PyOpenCL not installed (optional)")
except Exception:
pass
@asynccontextmanager
async def lifespan(app: FastAPI):
# Log GPU status on startup
check_gpu_availability()
await connect_to_mongo()
yield
await close_mongo_connection()

View File

@@ -0,0 +1,122 @@
"""
Coverage boundary calculation service.
Computes concave hull (alpha shape) from coverage points to generate
a realistic boundary that follows actual coverage contour.
"""
import logging
from typing import Optional
logger = logging.getLogger(__name__)
def calculate_coverage_boundary(
points: list[dict],
threshold_dbm: float = -100,
simplify_tolerance: float = 0.001,
) -> list[dict]:
"""
Calculate coverage boundary as concave hull of points above threshold.
Args:
points: List of coverage points with 'lat', 'lon', 'rsrp' keys
threshold_dbm: RSRP threshold - points below this are excluded
simplify_tolerance: Simplification tolerance in degrees (~100m per 0.001)
Returns:
List of {'lat': float, 'lon': float} coordinates forming boundary polygon.
Empty list if boundary cannot be computed.
"""
try:
from shapely.geometry import MultiPoint
from shapely import concave_hull
except ImportError:
logger.warning("Shapely not installed - boundary calculation disabled")
return []
# Filter points above threshold
valid_coords = [
(p['lon'], p['lat']) # Shapely uses (x, y) = (lon, lat)
for p in points
if p.get('rsrp', -999) >= threshold_dbm
]
if len(valid_coords) < 3:
logger.debug(f"Not enough points for boundary: {len(valid_coords)}")
return []
try:
# Create MultiPoint geometry
mp = MultiPoint(valid_coords)
# Compute concave hull (alpha shape)
# ratio: 0 = convex hull, 1 = very tight fit
# 0.3-0.5 gives good balance between detail and smoothness
hull = concave_hull(mp, ratio=0.3)
if hull.is_empty:
logger.debug("Concave hull is empty")
return []
# Simplify to reduce points (0.001 deg ≈ 100m)
if simplify_tolerance > 0:
hull = hull.simplify(simplify_tolerance, preserve_topology=True)
# Extract coordinates based on geometry type
if hull.geom_type == 'Polygon':
coords = list(hull.exterior.coords)
return [{'lat': c[1], 'lon': c[0]} for c in coords]
elif hull.geom_type == 'MultiPolygon':
# Return largest polygon's exterior
largest = max(hull.geoms, key=lambda g: g.area)
coords = list(largest.exterior.coords)
return [{'lat': c[1], 'lon': c[0]} for c in coords]
elif hull.geom_type == 'GeometryCollection':
# Find polygons in collection
polygons = [g for g in hull.geoms if g.geom_type == 'Polygon']
if polygons:
largest = max(polygons, key=lambda g: g.area)
coords = list(largest.exterior.coords)
return [{'lat': c[1], 'lon': c[0]} for c in coords]
logger.debug(f"Unexpected hull geometry type: {hull.geom_type}")
return []
except Exception as e:
logger.warning(f"Boundary calculation error: {e}")
return []
def calculate_multi_site_boundaries(
points: list[dict],
threshold_dbm: float = -100,
) -> dict[str, list[dict]]:
"""
Calculate separate boundaries for each site's coverage area.
Args:
points: Coverage points with 'lat', 'lon', 'rsrp', 'site_id' keys
threshold_dbm: RSRP threshold
Returns:
Dict mapping site_id to boundary coordinates list.
"""
# Group points by site_id
by_site: dict[str, list[dict]] = {}
for p in points:
site_id = p.get('site_id', 'default')
if site_id not in by_site:
by_site[site_id] = []
by_site[site_id].append(p)
# Calculate boundary for each site
boundaries = {}
for site_id, site_points in by_site.items():
boundary = calculate_coverage_boundary(site_points, threshold_dbm)
if boundary:
boundaries[site_id] = boundary
return boundaries

View File

@@ -171,17 +171,34 @@ class GPUManager:
"""Full diagnostic info for troubleshooting GPU detection."""
import sys
import platform
import subprocess
is_wsl = "microsoft" in platform.release().lower()
diag = {
"python_version": sys.version,
"python_executable": sys.executable,
"platform": platform.platform(),
"is_wsl": is_wsl,
"numpy": {"version": np.__version__},
"cuda": {},
"opencl": {},
"nvidia_smi": None,
"detected_devices": len(self._devices),
"active_backend": self._active_backend.value,
}
# Check nvidia-smi (works even without CuPy)
try:
result = subprocess.run(
["nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader"],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0 and result.stdout.strip():
diag["nvidia_smi"] = result.stdout.strip()
except Exception:
diag["nvidia_smi"] = "not found or error"
# Check CuPy/CUDA
try:
import cupy as cp
@@ -200,7 +217,10 @@ class GPUManager:
}
except ImportError:
diag["cuda"]["error"] = "CuPy not installed"
diag["cuda"]["install_hint"] = "pip install cupy-cuda12x"
if is_wsl:
diag["cuda"]["install_hint"] = "pip3 install cupy-cuda12x --break-system-packages"
else:
diag["cuda"]["install_hint"] = "pip install cupy-cuda12x"
except Exception as e:
diag["cuda"]["error"] = str(e)
@@ -221,7 +241,10 @@ class GPUManager:
diag["opencl"]["platforms"].append(platform_info)
except ImportError:
diag["opencl"]["error"] = "PyOpenCL not installed"
diag["opencl"]["install_hint"] = "pip install pyopencl"
if is_wsl:
diag["opencl"]["install_hint"] = "pip3 install pyopencl --break-system-packages"
else:
diag["opencl"]["install_hint"] = "pip install pyopencl"
except Exception as e:
diag["opencl"]["error"] = str(e)

View File

@@ -7,6 +7,7 @@ pymongo==4.6.1
pydantic-settings==2.1.0
numpy==1.26.4
scipy==1.12.0
shapely>=2.0.0
requests==2.31.0
httpx==0.27.0
aiosqlite>=0.19.0