32 KiB
32 KiB
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:
# 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.pybackend/app/services/coverage_service.py
1.2 Coverage Boundary Fix
Problem: White dashed boundary doesn't work correctly for multi-site/tiled calculations.
Solution:
- Fix boundary to use convex hull of ALL points > threshold
- Add toggle button in legend: "Show Boundary"
- Default: OFF (less visual clutter)
// 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>
// 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.tsxfrontend/src/components/map/HeatmapLegend.tsxfrontend/src/store/settings.ts(add showBoundary state)
Phase 2: GPU Acceleration
2.1 GPU Backend Detection
Support hierarchy:
- NVIDIA CUDA (fastest) — CuPy
- OpenCL (any GPU) — PyOpenCL
- CPU fallback — NumPy
# 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
# 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
# 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
# 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
// 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
// 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
# 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
// 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
// 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
// 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
// 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
# 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)
- Timeout fix for tiled calculations
- GPU backend detection & CuPy integration
Priority 2 — GPU Implementation (Day 2-3)
- GPU path loss calculation
- GPU terrain interpolation
- Frontend GPU settings UI
Priority 3 — Features (Day 4-5)
- Coverage boundary fix + toggle
- Terrain Profile Viewer
- Batch frequency change
Priority 4 — Polish (Day 6)
- Fading margin setting
- Extended frequency bands
- 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
# 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
# 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 & managementbackend/app/services/propagation_gpu.py— GPU path lossbackend/app/services/terrain_gpu.py— GPU terrain interpolationbackend/app/api/gpu.py— GPU API endpointsbackend/app/models/frequency_bands.py— Extended band definitionsfrontend/src/components/panels/GPUSettings.tsx— GPU UIfrontend/src/components/panels/TerrainProfile.tsx— Profile viewerfrontend/src/components/panels/BatchOperations.tsx— Batch controls
Modified Files
backend/app/api/websocket.py— Timeout fixbackend/app/services/coverage_service.py— GPU integrationbackend/requirements.txt— CuPy dependencyfrontend/src/components/map/CoverageBoundary.tsx— Fix + togglefrontend/src/components/map/HeatmapLegend.tsx— Boundary togglefrontend/src/components/map/RulerTool.tsx— Profile integrationfrontend/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" 🚀