Files
rfcp/docs/devlog/gpu_supp/RFCP-Iteration-3.5.0-GPU-Acceleration.md
2026-02-03 10:51:26 +02:00

1097 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RFCP Iteration 3.5.0 — GPU Acceleration & UI Polish
## Overview
Major performance upgrade with GPU acceleration support and UI/UX improvements.
**Key Goals:**
- GPU acceleration for 10-50x speedup
- Fix timeout for large radius calculations
- Terrain Profile Viewer
- UI polish and fixes
---
## Phase 1: Critical Fixes
### 1.1 Timeout Fix for Tiled Calculations
**Problem:** 5 minute timeout kills calculations > 20km even though they're working correctly.
**Solution:**
```python
# backend/app/api/websocket.py
# Per-tile timeout instead of total timeout
TILE_TIMEOUT_SECONDS = 120 # 2 min per tile (generous)
TOTAL_TIMEOUT_SECONDS = 1800 # 30 min max for entire calculation
# For tiled calculations:
if is_tiled_calculation(radius):
timeout = TOTAL_TIMEOUT_SECONDS
else:
timeout = 300 # 5 min for small calculations
```
**Files:**
- `backend/app/api/websocket.py`
- `backend/app/services/coverage_service.py`
---
### 1.2 Coverage Boundary Fix
**Problem:** White dashed boundary doesn't work correctly for multi-site/tiled calculations.
**Solution:**
1. Fix boundary to use convex hull of ALL points > threshold
2. Add toggle button in legend: "Show Boundary"
3. Default: OFF (less visual clutter)
```typescript
// frontend/src/components/map/HeatmapLegend.tsx
// Add toggle
const [showBoundary, setShowBoundary] = useState(false);
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showBoundary}
onChange={(e) => setShowBoundary(e.target.checked)}
/>
<span className="text-xs">Show coverage boundary</span>
</label>
```
```typescript
// frontend/src/components/map/CoverageBoundary.tsx
// Calculate convex hull of all points above threshold
function calculateBoundary(points: CoveragePoint[], threshold: number) {
const validPoints = points.filter(p => p.rsrp >= threshold);
if (validPoints.length < 3) return null;
// Use convex hull algorithm (Graham scan or gift wrapping)
return convexHull(validPoints.map(p => [p.lat, p.lon]));
}
```
**Files:**
- `frontend/src/components/map/CoverageBoundary.tsx`
- `frontend/src/components/map/HeatmapLegend.tsx`
- `frontend/src/store/settings.ts` (add showBoundary state)
---
## Phase 2: GPU Acceleration
### 2.1 GPU Backend Detection
**Support hierarchy:**
1. NVIDIA CUDA (fastest) — CuPy
2. OpenCL (any GPU) — PyOpenCL
3. CPU fallback — NumPy
```python
# backend/app/services/gpu_backend.py (NEW)
from typing import Tuple, Optional
from enum import Enum
class ComputeBackend(Enum):
CUDA = "cuda"
OPENCL = "opencl"
CPU = "cpu"
class GPUManager:
_instance = None
_backend: ComputeBackend = ComputeBackend.CPU
_device_name: str = "CPU (NumPy)"
_devices: list = []
@classmethod
def detect_backends(cls) -> list:
"""Detect all available compute backends."""
backends = []
# Check NVIDIA CUDA
try:
import cupy as cp
for i in range(cp.cuda.runtime.getDeviceCount()):
device = cp.cuda.Device(i)
backends.append({
"type": ComputeBackend.CUDA,
"id": i,
"name": device.name,
"memory": device.mem_info[1], # total memory
"priority": 1 # highest
})
except Exception:
pass
# Check OpenCL (AMD, Intel, NVIDIA)
try:
import pyopencl as cl
for platform in cl.get_platforms():
for device in platform.get_devices():
# Skip if already have CUDA version of same GPU
if ComputeBackend.CUDA in [b["type"] for b in backends]:
if "NVIDIA" in device.name:
continue
backends.append({
"type": ComputeBackend.OPENCL,
"id": f"{platform.name}:{device.name}",
"name": device.name,
"memory": device.global_mem_size,
"priority": 2
})
except Exception:
pass
# CPU always available
import multiprocessing
backends.append({
"type": ComputeBackend.CPU,
"id": "cpu",
"name": f"CPU ({multiprocessing.cpu_count()} cores)",
"memory": None,
"priority": 3
})
cls._devices = sorted(backends, key=lambda x: x["priority"])
return cls._devices
@classmethod
def get_devices(cls) -> list:
"""Get list of available compute devices."""
if not cls._devices:
cls.detect_backends()
return cls._devices
@classmethod
def set_backend(cls, backend_type: ComputeBackend, device_id: Optional[str] = None):
"""Set active compute backend."""
cls._backend = backend_type
if backend_type == ComputeBackend.CUDA:
import cupy as cp
device_idx = int(device_id) if device_id else 0
cp.cuda.Device(device_idx).use()
cls._device_name = cp.cuda.Device(device_idx).name
elif backend_type == ComputeBackend.OPENCL:
# Store for later use in calculations
cls._opencl_device_id = device_id
cls._device_name = device_id.split(":")[-1] if device_id else "OpenCL"
else:
cls._device_name = "CPU (NumPy)"
@classmethod
def get_array_module(cls):
"""Get numpy-compatible array module for current backend."""
if cls._backend == ComputeBackend.CUDA:
import cupy as cp
return cp
else:
import numpy as np
return np
@classmethod
def get_status(cls) -> dict:
"""Get current GPU status for UI."""
return {
"backend": cls._backend.value,
"device_name": cls._device_name,
"available_devices": cls.get_devices()
}
```
### 2.2 GPU-Accelerated Path Loss Calculation
```python
# backend/app/services/propagation_gpu.py (NEW)
from .gpu_backend import GPUManager, ComputeBackend
def calculate_path_loss_batch_gpu(
site_lat: float,
site_lon: float,
site_height: float,
points_lat: np.ndarray, # Can be large array
points_lon: np.ndarray,
frequency_mhz: float,
environment: str = "suburban"
) -> np.ndarray:
"""
Calculate path loss for ALL points at once using GPU.
Returns array of path loss values in dB.
"""
xp = GPUManager.get_array_module() # numpy or cupy
# Transfer to GPU if using CUDA
if GPUManager._backend == ComputeBackend.CUDA:
lats = xp.asarray(points_lat)
lons = xp.asarray(points_lon)
else:
lats = points_lat
lons = points_lon
# Vectorized distance calculation (Haversine)
R = 6371000 # Earth radius in meters
lat1 = xp.radians(site_lat)
lat2 = xp.radians(lats)
dlat = lat2 - lat1
dlon = xp.radians(lons - site_lon)
a = xp.sin(dlat/2)**2 + xp.cos(lat1) * xp.cos(lat2) * xp.sin(dlon/2)**2
c = 2 * xp.arctan2(xp.sqrt(a), xp.sqrt(1-a))
distances_m = R * c
distances_km = distances_m / 1000.0
# Avoid log(0)
distances_km = xp.maximum(distances_km, 0.01)
# Okumura-Hata model (vectorized)
f = frequency_mhz
hb = site_height
# Base formula
A = 69.55 + 26.16 * xp.log10(f) - 13.82 * xp.log10(hb)
B = 44.9 - 6.55 * xp.log10(hb)
path_loss = A + B * xp.log10(distances_km)
# Environment corrections
if environment == "urban":
pass # Base formula
elif environment == "suburban":
path_loss = path_loss - 2 * (xp.log10(f/28))**2 - 5.4
elif environment == "rural":
path_loss = path_loss - 4.78 * (xp.log10(f))**2 + 18.33 * xp.log10(f) - 40.94
# Transfer back to CPU if needed
if GPUManager._backend == ComputeBackend.CUDA:
return path_loss.get() # cupy → numpy
return path_loss
def calculate_rsrp_batch_gpu(
tx_power_dbm: float,
antenna_gain_dbi: float,
cable_loss_db: float,
path_loss_db: np.ndarray,
additional_loss_db: np.ndarray = None
) -> np.ndarray:
"""
Calculate RSRP for all points at once.
RSRP = TX Power + Antenna Gain - Cable Loss - Path Loss - Additional Loss
"""
xp = GPUManager.get_array_module()
if GPUManager._backend == ComputeBackend.CUDA:
path_loss = xp.asarray(path_loss_db)
add_loss = xp.asarray(additional_loss_db) if additional_loss_db is not None else 0
else:
path_loss = path_loss_db
add_loss = additional_loss_db if additional_loss_db is not None else 0
eirp = tx_power_dbm + antenna_gain_dbi - cable_loss_db
rsrp = eirp - path_loss - add_loss
if GPUManager._backend == ComputeBackend.CUDA:
return rsrp.get()
return rsrp
```
### 2.3 GPU Terrain Interpolation
```python
# backend/app/services/terrain_gpu.py (NEW)
def interpolate_terrain_batch_gpu(
terrain_data: np.ndarray,
terrain_bounds: tuple, # (min_lat, min_lon, max_lat, max_lon)
points_lat: np.ndarray,
points_lon: np.ndarray
) -> np.ndarray:
"""
Bilinear interpolation of terrain heights for all points.
GPU version uses texture memory for fast 2D lookups.
"""
xp = GPUManager.get_array_module()
min_lat, min_lon, max_lat, max_lon = terrain_bounds
rows, cols = terrain_data.shape
if GPUManager._backend == ComputeBackend.CUDA:
# Upload terrain as texture (cached on GPU)
terrain_gpu = xp.asarray(terrain_data)
lats = xp.asarray(points_lat)
lons = xp.asarray(points_lon)
else:
terrain_gpu = terrain_data
lats = points_lat
lons = points_lon
# Normalize coordinates to [0, 1]
lat_norm = (lats - min_lat) / (max_lat - min_lat)
lon_norm = (lons - min_lon) / (max_lon - min_lon)
# Convert to pixel coordinates
y = lat_norm * (rows - 1)
x = lon_norm * (cols - 1)
# Bilinear interpolation indices
x0 = xp.floor(x).astype(int)
x1 = xp.minimum(x0 + 1, cols - 1)
y0 = xp.floor(y).astype(int)
y1 = xp.minimum(y0 + 1, rows - 1)
# Clamp to valid range
x0 = xp.clip(x0, 0, cols - 1)
y0 = xp.clip(y0, 0, rows - 1)
# Interpolation weights
wx = x - x0
wy = y - y0
# Bilinear interpolation
heights = (
terrain_gpu[y0, x0] * (1 - wx) * (1 - wy) +
terrain_gpu[y0, x1] * wx * (1 - wy) +
terrain_gpu[y1, x0] * (1 - wx) * wy +
terrain_gpu[y1, x1] * wx * wy
)
if GPUManager._backend == ComputeBackend.CUDA:
return heights.get()
return heights
```
### 2.4 API Endpoint for GPU Status
```python
# backend/app/api/gpu.py (NEW)
from fastapi import APIRouter
from ..services.gpu_backend import GPUManager, ComputeBackend
router = APIRouter(prefix="/api/gpu", tags=["GPU"])
@router.get("/status")
async def get_gpu_status():
"""Get current GPU acceleration status."""
return GPUManager.get_status()
@router.get("/devices")
async def get_available_devices():
"""List all available compute devices."""
return {"devices": GPUManager.get_devices()}
@router.post("/set")
async def set_compute_backend(backend: str, device_id: str = None):
"""Set active compute backend."""
backend_enum = ComputeBackend(backend)
GPUManager.set_backend(backend_enum, device_id)
return {"status": "ok", "backend": backend, "device": GPUManager._device_name}
```
### 2.5 Frontend GPU Settings UI
```typescript
// frontend/src/components/panels/GPUSettings.tsx (NEW)
import { useState, useEffect } from 'react';
import { Gpu, Cpu, Zap } from 'lucide-react';
interface Device {
type: 'cuda' | 'opencl' | 'cpu';
id: string;
name: string;
memory: number | null;
}
export function GPUSettings() {
const [devices, setDevices] = useState<Device[]>([]);
const [activeBackend, setActiveBackend] = useState<string>('cpu');
const [activeDevice, setActiveDevice] = useState<string>('');
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchGPUStatus();
}, []);
const fetchGPUStatus = async () => {
try {
const res = await fetch('/api/gpu/devices');
const data = await res.json();
setDevices(data.devices);
const status = await fetch('/api/gpu/status');
const statusData = await status.json();
setActiveBackend(statusData.backend);
setActiveDevice(statusData.device_name);
} finally {
setLoading(false);
}
};
const setBackend = async (type: string, deviceId?: string) => {
await fetch('/api/gpu/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ backend: type, device_id: deviceId })
});
fetchGPUStatus();
};
const getIcon = (type: string) => {
if (type === 'cuda') return <Gpu className="w-4 h-4 text-green-500" />;
if (type === 'opencl') return <Gpu className="w-4 h-4 text-blue-500" />;
return <Cpu className="w-4 h-4 text-gray-500" />;
};
const formatMemory = (bytes: number | null) => {
if (!bytes) return '';
const gb = bytes / 1024 / 1024 / 1024;
return `${gb.toFixed(1)} GB`;
};
return (
<div className="p-4 bg-slate-800 rounded-lg">
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
<Zap className="w-4 h-4" />
Compute Acceleration
</h3>
{/* Current status */}
<div className="mb-3 p-2 bg-slate-700 rounded text-xs">
<span className="text-gray-400">Active: </span>
<span className="text-white font-medium">{activeDevice}</span>
</div>
{/* Device list */}
<div className="space-y-2">
{devices.map((device) => (
<label
key={device.id}
className={`
flex items-center gap-3 p-2 rounded cursor-pointer
${activeBackend === device.type ? 'bg-blue-600/30 border border-blue-500' : 'bg-slate-700 hover:bg-slate-600'}
`}
>
<input
type="radio"
name="compute-backend"
checked={activeBackend === device.type}
onChange={() => setBackend(device.type, device.id)}
className="sr-only"
/>
{getIcon(device.type)}
<div className="flex-1">
<div className="text-sm text-white">{device.name}</div>
<div className="text-xs text-gray-400">
{device.type.toUpperCase()}
{device.memory && `${formatMemory(device.memory)}`}
</div>
</div>
{activeBackend === device.type && (
<span className="text-xs text-green-400"> Active</span>
)}
</label>
))}
</div>
{/* Info */}
<p className="mt-3 text-xs text-gray-500">
GPU acceleration provides 10-50x speedup for large calculations.
NVIDIA CUDA is fastest, OpenCL works with AMD/Intel GPUs.
</p>
</div>
);
}
```
### 2.6 Status Bar GPU Indicator
```typescript
// frontend/src/components/StatusBar.tsx (add to existing or create)
// Add GPU indicator to status bar / toolbar
<div className="flex items-center gap-1 text-xs">
{gpuStatus.backend === 'cuda' && (
<>
<Gpu className="w-3 h-3 text-green-500" />
<span className="text-green-400">GPU: {gpuStatus.device_name}</span>
</>
)}
{gpuStatus.backend === 'opencl' && (
<>
<Gpu className="w-3 h-3 text-blue-500" />
<span className="text-blue-400">OpenCL: {gpuStatus.device_name}</span>
</>
)}
{gpuStatus.backend === 'cpu' && (
<>
<Cpu className="w-3 h-3 text-gray-500" />
<span className="text-gray-400">CPU mode</span>
</>
)}
</div>
```
---
## Phase 3: Terrain Profile Viewer
### 3.1 Backend Endpoint
```python
# backend/app/api/terrain.py (NEW or add to existing)
@router.post("/profile")
async def get_terrain_profile(
start_lat: float,
start_lon: float,
end_lat: float,
end_lon: float,
samples: int = 100
):
"""
Get elevation profile between two points.
Returns array of {distance, elevation, lat, lon} objects.
"""
from ..services.terrain_service import TerrainService
terrain = TerrainService()
# Generate sample points along the line
lats = np.linspace(start_lat, end_lat, samples)
lons = np.linspace(start_lon, end_lon, samples)
# Get elevations
elevations = []
total_distance = 0
prev_lat, prev_lon = start_lat, start_lon
for i, (lat, lon) in enumerate(zip(lats, lons)):
elev = terrain.get_elevation(lat, lon)
if i > 0:
# Calculate distance from previous point
dist = haversine_distance(prev_lat, prev_lon, lat, lon)
total_distance += dist
elevations.append({
"index": i,
"lat": lat,
"lon": lon,
"elevation": elev,
"distance": total_distance
})
prev_lat, prev_lon = lat, lon
return {
"profile": elevations,
"total_distance": total_distance,
"min_elevation": min(p["elevation"] for p in elevations),
"max_elevation": max(p["elevation"] for p in elevations)
}
```
### 3.2 Frontend Profile Component
```typescript
// frontend/src/components/panels/TerrainProfile.tsx (NEW)
import { useEffect, useState } from 'react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Area, ReferenceLine } from 'recharts';
import { Mountain, Radio, Eye } from 'lucide-react';
interface ProfilePoint {
distance: number;
elevation: number;
lat: number;
lon: number;
}
interface TerrainProfileProps {
startPoint: [number, number]; // [lat, lon]
endPoint: [number, number];
antennaHeight?: number; // Height above ground at start point
onClose: () => void;
}
export function TerrainProfile({ startPoint, endPoint, antennaHeight = 10, onClose }: TerrainProfileProps) {
const [profile, setProfile] = useState<ProfilePoint[]>([]);
const [loading, setLoading] = useState(true);
const [losStatus, setLosStatus] = useState<'clear' | 'obstructed' | 'partial'>('clear');
useEffect(() => {
fetchProfile();
}, [startPoint, endPoint]);
const fetchProfile = async () => {
setLoading(true);
try {
const res = await fetch('/api/terrain/profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
start_lat: startPoint[0],
start_lon: startPoint[1],
end_lat: endPoint[0],
end_lon: endPoint[1],
samples: 100
})
});
const data = await res.json();
setProfile(data.profile);
calculateLOS(data.profile);
} finally {
setLoading(false);
}
};
const calculateLOS = (profileData: ProfilePoint[]) => {
if (profileData.length < 2) return;
const startElev = profileData[0].elevation + antennaHeight;
const endElev = profileData[profileData.length - 1].elevation + 1.5; // UE height
const totalDist = profileData[profileData.length - 1].distance;
let obstructionCount = 0;
for (const point of profileData) {
// Calculate expected LOS height at this distance
const ratio = point.distance / totalDist;
const losHeight = startElev + (endElev - startElev) * ratio;
if (point.elevation > losHeight) {
obstructionCount++;
}
}
if (obstructionCount === 0) {
setLosStatus('clear');
} else if (obstructionCount > profileData.length * 0.3) {
setLosStatus('obstructed');
} else {
setLosStatus('partial');
}
};
// Calculate LOS line data
const losLine = profile.length > 0 ? [
{
distance: 0,
los: profile[0].elevation + antennaHeight
},
{
distance: profile[profile.length - 1].distance,
los: profile[profile.length - 1].elevation + 1.5
}
] : [];
const totalDistanceKm = profile.length > 0
? (profile[profile.length - 1].distance / 1000).toFixed(2)
: '0';
return (
<div className="absolute left-4 bottom-20 w-[500px] bg-slate-800 rounded-lg shadow-xl border border-slate-600 z-50">
{/* Header */}
<div className="flex items-center justify-between p-3 border-b border-slate-600">
<h3 className="font-semibold flex items-center gap-2">
<Mountain className="w-4 h-4" />
Terrain Profile
</h3>
<button onClick={onClose} className="text-gray-400 hover:text-white">×</button>
</div>
{/* Stats */}
<div className="flex gap-4 p-3 text-xs border-b border-slate-700">
<div>
<span className="text-gray-400">Distance: </span>
<span className="text-white font-medium">{totalDistanceKm} km</span>
</div>
<div>
<span className="text-gray-400">LOS: </span>
<span className={`font-medium ${
losStatus === 'clear' ? 'text-green-400' :
losStatus === 'partial' ? 'text-yellow-400' : 'text-red-400'
}`}>
{losStatus === 'clear' ? '✓ Clear' :
losStatus === 'partial' ? '⚠ Partial' : '✗ Obstructed'}
</span>
</div>
<div>
<span className="text-gray-400">Antenna: </span>
<span className="text-white font-medium">{antennaHeight}m AGL</span>
</div>
</div>
{/* Chart */}
<div className="p-3 h-[200px]">
{loading ? (
<div className="flex items-center justify-center h-full text-gray-400">
Loading terrain data...
</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={profile} margin={{ top: 10, right: 10, bottom: 20, left: 40 }}>
{/* Terrain fill */}
<defs>
<linearGradient id="terrainGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#4ade80" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#4ade80" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis
dataKey="distance"
tickFormatter={(v) => `${(v/1000).toFixed(1)}km`}
tick={{ fontSize: 10, fill: '#9ca3af' }}
/>
<YAxis
tick={{ fontSize: 10, fill: '#9ca3af' }}
tickFormatter={(v) => `${v}m`}
domain={['dataMin - 20', 'dataMax + 50']}
/>
<Tooltip
formatter={(value: number, name: string) => [
`${value.toFixed(0)}m`,
name === 'elevation' ? 'Elevation' : 'LOS'
]}
labelFormatter={(label: number) => `Distance: ${(label/1000).toFixed(2)} km`}
/>
{/* Terrain area */}
<Area
type="monotone"
dataKey="elevation"
stroke="#4ade80"
fill="url(#terrainGradient)"
strokeWidth={2}
/>
{/* LOS line */}
<Line
data={losLine}
type="linear"
dataKey="los"
stroke="#ef4444"
strokeWidth={1}
strokeDasharray="5 5"
dot={false}
/>
{/* Antenna marker */}
<ReferenceLine x={0} stroke="#3b82f6" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
)}
</div>
{/* Legend */}
<div className="flex gap-4 p-3 text-xs border-t border-slate-700">
<div className="flex items-center gap-1">
<div className="w-3 h-0.5 bg-green-400"></div>
<span className="text-gray-400">Terrain</span>
</div>
<div className="flex items-center gap-1">
<div className="w-3 h-0.5 bg-red-400 border-dashed"></div>
<span className="text-gray-400">Line of Sight</span>
</div>
<div className="flex items-center gap-1">
<Radio className="w-3 h-3 text-blue-400" />
<span className="text-gray-400">Antenna</span>
</div>
</div>
</div>
);
}
```
### 3.3 Integration with Ruler Tool
```typescript
// frontend/src/components/map/RulerTool.tsx (modify existing)
// After ruler measurement is complete, show button:
{rulerPoints.length === 2 && (
<button
onClick={() => setShowTerrainProfile(true)}
className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-full mb-2
px-3 py-1 bg-blue-600 text-white text-xs rounded shadow-lg
hover:bg-blue-700 flex items-center gap-1"
>
<Mountain className="w-3 h-3" />
Show Elevation Profile
</button>
)}
{showTerrainProfile && (
<TerrainProfile
startPoint={rulerPoints[0]}
endPoint={rulerPoints[1]}
antennaHeight={activeSite?.antennaHeight || 10}
onClose={() => setShowTerrainProfile(false)}
/>
)}
```
---
## Phase 4: Additional Features
### 4.1 Batch Frequency Change
```typescript
// frontend/src/components/panels/BatchOperations.tsx (NEW)
export function BatchFrequencyChange() {
const { sites, updateAllSectors } = useSiteStore();
const frequencies = [
{ value: 700, label: '700 MHz', band: 'Band 28' },
{ value: 800, label: '800 MHz', band: 'Band 20' },
{ value: 1800, label: '1800 MHz', band: 'Band 3' },
{ value: 2100, label: '2100 MHz', band: 'Band 1' },
{ value: 2600, label: '2600 MHz', band: 'Band 7' },
{ value: 3500, label: '3500 MHz', band: 'n78 (5G)' },
];
const handleBatchChange = (freq: number) => {
updateAllSectors({ frequency: freq });
};
return (
<div className="p-3 border-t border-slate-700">
<h4 className="text-xs font-semibold text-gray-400 mb-2">
BATCH FREQUENCY CHANGE
</h4>
<div className="flex flex-wrap gap-1">
{frequencies.map(f => (
<button
key={f.value}
onClick={() => handleBatchChange(f.value)}
className="px-2 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded"
title={f.band}
>
{f.label}
</button>
))}
</div>
</div>
);
}
```
### 4.2 Fading Margin Setting
```typescript
// Add to Coverage Settings panel
<div className="space-y-2">
<label className="flex items-center justify-between">
<span className="text-sm">Fading Margin</span>
<span className="text-sm text-blue-400">{fadingMargin} dB</span>
</label>
<input
type="range"
min="0"
max="15"
value={fadingMargin}
onChange={(e) => setFadingMargin(Number(e.target.value))}
className="w-full"
/>
<p className="text-xs text-gray-500">
Safety margin for fading (8-10 dB typical for 90% coverage)
</p>
</div>
```
### 4.3 Extended Frequency Bands
```python
# backend/app/models/frequency_bands.py (NEW)
FREQUENCY_BANDS = {
# LTE FDD
"band_28": {"name": "Band 28", "freq_dl": 758, "freq_ul": 703, "bandwidth": 10, "type": "FDD", "region": "APAC/EU"},
"band_20": {"name": "Band 20", "freq_dl": 806, "freq_ul": 847, "bandwidth": 10, "type": "FDD", "region": "EU"},
"band_8": {"name": "Band 8", "freq_dl": 935, "freq_ul": 880, "bandwidth": 10, "type": "FDD", "region": "Global"},
"band_3": {"name": "Band 3", "freq_dl": 1842.5, "freq_ul": 1747.5, "bandwidth": 20, "type": "FDD", "region": "Global"},
"band_1": {"name": "Band 1", "freq_dl": 2140, "freq_ul": 1950, "bandwidth": 20, "type": "FDD", "region": "Global"},
"band_7": {"name": "Band 7", "freq_dl": 2655, "freq_ul": 2535, "bandwidth": 20, "type": "FDD", "region": "Global"},
# LTE TDD
"band_38": {"name": "Band 38", "freq": 2600, "bandwidth": 10, "type": "TDD", "region": "EU/APAC"},
"band_40": {"name": "Band 40", "freq": 2350, "bandwidth": 20, "type": "TDD", "region": "APAC"},
"band_41": {"name": "Band 41", "freq": 2593, "bandwidth": 20, "type": "TDD", "region": "Global"},
# 5G NR
"n77": {"name": "n77", "freq": 3700, "bandwidth": 100, "type": "TDD", "region": "Global", "tech": "5G"},
"n78": {"name": "n78", "freq": 3500, "bandwidth": 100, "type": "TDD", "region": "Global", "tech": "5G"},
"n258": {"name": "n258", "freq": 26500, "bandwidth": 400, "type": "TDD", "region": "Global", "tech": "5G mmWave"},
}
def get_band_info(frequency_mhz: int) -> dict:
"""Get band info for a frequency."""
for band_id, band in FREQUENCY_BANDS.items():
if "freq_dl" in band:
if abs(band["freq_dl"] - frequency_mhz) < 50:
return {"band_id": band_id, **band}
elif abs(band["freq"] - frequency_mhz) < 50:
return {"band_id": band_id, **band}
return None
```
---
## Implementation Order
### Priority 1 — Critical (Day 1)
1. Timeout fix for tiled calculations
2. GPU backend detection & CuPy integration
### Priority 2 — GPU Implementation (Day 2-3)
3. GPU path loss calculation
4. GPU terrain interpolation
5. Frontend GPU settings UI
### Priority 3 — Features (Day 4-5)
6. Coverage boundary fix + toggle
7. Terrain Profile Viewer
8. Batch frequency change
### Priority 4 — Polish (Day 6)
9. Fading margin setting
10. Extended frequency bands
11. UI/UX improvements
---
## Dependencies
### Python (backend)
```
# requirements.txt additions
cupy-cuda12x>=12.0.0 # For CUDA 12
# OR cupy-cuda11x>=11.0.0 # For CUDA 11
pyopencl>=2022.1 # OpenCL fallback
```
### Install CUDA support
```bash
# Check CUDA version
nvidia-smi
# Install matching CuPy
pip install cupy-cuda12x # For CUDA 12.x
# OR
pip install cupy-cuda11x # For CUDA 11.x
```
---
## Expected Performance
| Scenario | Current (CPU) | With GPU (RTX 4060) | Speedup |
|----------|---------------|---------------------|---------|
| 10 km, 100m resolution | 124s | ~12s | **10x** |
| 20 km, 100m resolution | >5 min (timeout) | ~30s | **10x+** |
| 20 km, 200m resolution | ~3 min | ~20s | **9x** |
| 50 km, 500m resolution | N/A (OOM) | ~2 min | **∞** |
---
## Testing
```bash
# Test GPU detection
curl http://localhost:8888/api/gpu/devices
# Test GPU calculation
curl -X POST http://localhost:8888/api/coverage/calculate \
-H "Content-Type: application/json" \
-d '{"use_gpu": true, "radius": 20000, "resolution": 100}'
# Benchmark CPU vs GPU
python -c "
from app.services.gpu_backend import GPUManager
import time
import numpy as np
# Generate test data
n_points = 100000
lats = np.random.uniform(49, 50, n_points)
lons = np.random.uniform(24, 25, n_points)
# CPU
GPUManager.set_backend('cpu')
t0 = time.time()
# ... path loss calc ...
print(f'CPU: {time.time()-t0:.2f}s')
# GPU
GPUManager.set_backend('cuda')
t0 = time.time()
# ... path loss calc ...
print(f'GPU: {time.time()-t0:.2f}s')
"
```
---
## Files Summary
### New Files
- `backend/app/services/gpu_backend.py` — GPU detection & management
- `backend/app/services/propagation_gpu.py` — GPU path loss
- `backend/app/services/terrain_gpu.py` — GPU terrain interpolation
- `backend/app/api/gpu.py` — GPU API endpoints
- `backend/app/models/frequency_bands.py` — Extended band definitions
- `frontend/src/components/panels/GPUSettings.tsx` — GPU UI
- `frontend/src/components/panels/TerrainProfile.tsx` — Profile viewer
- `frontend/src/components/panels/BatchOperations.tsx` — Batch controls
### Modified Files
- `backend/app/api/websocket.py` — Timeout fix
- `backend/app/services/coverage_service.py` — GPU integration
- `backend/requirements.txt` — CuPy dependency
- `frontend/src/components/map/CoverageBoundary.tsx` — Fix + toggle
- `frontend/src/components/map/HeatmapLegend.tsx` — Boundary toggle
- `frontend/src/components/map/RulerTool.tsx` — Profile integration
- `frontend/src/components/panels/CoverageSettings.tsx` — Fading margin
---
## Success Criteria
- [ ] 20 km radius completes without timeout
- [ ] GPU detected and selectable in UI
- [ ] GPU provides 10x+ speedup on RTX 4060
- [ ] OpenCL fallback works for non-NVIDIA
- [ ] Terrain profile shows correct elevation
- [ ] Coverage boundary toggleable and accurate
- [ ] Batch frequency change works
- [ ] Fading margin affects RSRP calculation
---
*"Make it fast, then make it faster"* 🚀