120 lines
3.5 KiB
Python
120 lines
3.5 KiB
Python
"""
|
|
GPU-accelerated computation service using CuPy.
|
|
Falls back to NumPy when CuPy/CUDA is not available.
|
|
|
|
Provides vectorized batch operations for coverage calculation:
|
|
- Haversine distance (site → all grid points)
|
|
- Okumura-Hata path loss (all distances at once)
|
|
|
|
Usage:
|
|
from app.services.gpu_service import gpu_service, GPU_AVAILABLE
|
|
"""
|
|
|
|
import numpy as np
|
|
from typing import Dict, Any, Optional
|
|
|
|
# ── Try CuPy import ──
|
|
|
|
GPU_AVAILABLE = False
|
|
GPU_INFO: Optional[Dict[str, Any]] = None
|
|
cp = None
|
|
|
|
try:
|
|
import cupy as _cp
|
|
if _cp.cuda.runtime.getDeviceCount() > 0:
|
|
cp = _cp
|
|
GPU_AVAILABLE = True
|
|
props = _cp.cuda.runtime.getDeviceProperties(0)
|
|
GPU_INFO = {
|
|
"name": props["name"].decode() if isinstance(props["name"], bytes) else str(props["name"]),
|
|
"memory_mb": props["totalGlobalMem"] // (1024 * 1024),
|
|
"cuda_version": _cp.cuda.runtime.runtimeGetVersion(),
|
|
}
|
|
print(f"[GPU] CUDA available: {GPU_INFO['name']} ({GPU_INFO['memory_mb']} MB)", flush=True)
|
|
except ImportError:
|
|
print("[GPU] CuPy not installed — using CPU/NumPy", flush=True)
|
|
except Exception as e:
|
|
print(f"[GPU] CUDA check failed: {e} — using CPU/NumPy", flush=True)
|
|
|
|
|
|
# Array module: cupy on GPU, numpy on CPU
|
|
xp = cp if GPU_AVAILABLE else np
|
|
|
|
|
|
def _to_cpu(arr):
|
|
"""Transfer array to CPU numpy if on GPU."""
|
|
if GPU_AVAILABLE and hasattr(arr, 'get'):
|
|
return arr.get()
|
|
return np.asarray(arr)
|
|
|
|
|
|
class GPUService:
|
|
"""GPU-accelerated batch operations for coverage calculation."""
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return GPU_AVAILABLE
|
|
|
|
def get_info(self) -> Dict[str, Any]:
|
|
"""Return GPU info dict for system endpoint."""
|
|
if not GPU_AVAILABLE:
|
|
return {"available": False, "name": None, "memory_mb": None}
|
|
return {"available": True, **GPU_INFO}
|
|
|
|
def precompute_distances(
|
|
self,
|
|
grid_lats: np.ndarray,
|
|
grid_lons: np.ndarray,
|
|
site_lat: float,
|
|
site_lon: float,
|
|
) -> np.ndarray:
|
|
"""Vectorized haversine distance from site to all grid points.
|
|
|
|
Returns distances in meters as a CPU numpy array.
|
|
"""
|
|
lat1 = xp.radians(xp.asarray(grid_lats, dtype=xp.float64))
|
|
lon1 = xp.radians(xp.asarray(grid_lons, dtype=xp.float64))
|
|
lat2 = xp.radians(xp.float64(site_lat))
|
|
lon2 = xp.radians(xp.float64(site_lon))
|
|
|
|
dlat = lat2 - lat1
|
|
dlon = lon2 - lon1
|
|
|
|
a = xp.sin(dlat / 2) ** 2 + xp.cos(lat1) * xp.cos(lat2) * xp.sin(dlon / 2) ** 2
|
|
c = 2 * xp.arcsin(xp.sqrt(a))
|
|
|
|
distances = 6371000.0 * c
|
|
return _to_cpu(distances)
|
|
|
|
def precompute_path_loss(
|
|
self,
|
|
distances: np.ndarray,
|
|
frequency_mhz: float,
|
|
tx_height: float,
|
|
rx_height: float = 1.5,
|
|
) -> np.ndarray:
|
|
"""Vectorized Okumura-Hata path loss for all distances.
|
|
|
|
Returns path loss in dB as a CPU numpy array.
|
|
"""
|
|
d_arr = xp.asarray(distances, dtype=xp.float64)
|
|
d_km = xp.maximum(d_arr / 1000.0, 0.1)
|
|
|
|
freq = float(frequency_mhz)
|
|
h_tx = float(tx_height)
|
|
h_rx = float(rx_height)
|
|
|
|
log_f = xp.log10(xp.float64(freq))
|
|
log_hb = xp.log10(xp.float64(h_tx))
|
|
|
|
a_hm = (1.1 * log_f - 0.7) * h_rx - (1.56 * log_f - 0.8)
|
|
|
|
L = (69.55 + 26.16 * log_f - 13.82 * log_hb - a_hm
|
|
+ (44.9 - 6.55 * log_hb) * xp.log10(d_km))
|
|
|
|
return _to_cpu(L)
|
|
|
|
|
|
# Singleton
|
|
gpu_service = GPUService()
|