@mytec: iter3.7.0 start, gpu calc int
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
122
backend/app/services/boundary_service.py
Normal file
122
backend/app/services/boundary_service.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user