18 KiB
RFCP Iteration 3.5.1 — Bugfixes & Polish
Overview
Focused bugfix and polish release addressing UI issues, coverage boundary accuracy, history improvements, and GPU indicator fixes discovered during 3.5.0 testing.
1. GPU — Detection Not Working + UI Overlap
1A. GPU Not Detected Despite Being Available
Problem: User has a laptop with DUAL GPUs (Intel integrated + NVIDIA discrete) but the app only shows "CPU (NumPy)". GPU acceleration is not working at all — no GPU option available in the device selector.
Root cause investigation needed:
- Check if CuPy is actually installed in the Python environment
- Check if CUDA toolkit is accessible from the app's runtime
- Check if PyOpenCL is installed (fallback for Intel GPU)
- The backend GPU detection may be failing silently
Debug steps to add:
# backend/app/services/gpu_backend.py — improve detection with logging
import logging
logger = logging.getLogger(__name__)
@classmethod
def detect_backends(cls) -> list:
backends = []
# Check NVIDIA CUDA
try:
import cupy as cp
count = cp.cuda.runtime.getDeviceCount()
logger.info(f"CUDA detected: {count} device(s)")
for i in range(count):
device = cp.cuda.Device(i)
backends.append({...})
except ImportError:
logger.warning("CuPy not installed — run: pip install cupy-cuda12x")
except Exception as e:
logger.warning(f"CUDA detection failed: {e}")
# Check OpenCL (works with Intel, AMD, AND NVIDIA)
try:
import pyopencl as cl
platforms = cl.get_platforms()
logger.info(f"OpenCL detected: {len(platforms)} platform(s)")
for platform in platforms:
for device in platform.get_devices():
logger.info(f" OpenCL device: {device.name}")
backends.append({...})
except ImportError:
logger.warning("PyOpenCL not installed — run: pip install pyopencl")
except Exception as e:
logger.warning(f"OpenCL detection failed: {e}")
# Always log what was found
logger.info(f"Total compute backends: {len(backends)} "
f"({sum(1 for b in backends if b['type'] == 'cuda')} CUDA, "
f"{sum(1 for b in backends if b['type'] == 'opencl')} OpenCL)")
# CPU always available
backends.append({...cpu...})
return backends
Installation check endpoint:
# backend/app/api/routes/gpu.py — add diagnostic endpoint
@router.get("/diagnostics")
async def gpu_diagnostics():
"""Full GPU diagnostic info for troubleshooting."""
diag = {
"python_version": sys.version,
"platform": platform.platform(),
"cuda": {},
"opencl": {},
"numpy": {}
}
# Check CuPy/CUDA
try:
import cupy
diag["cuda"]["cupy_version"] = cupy.__version__
diag["cuda"]["cuda_version"] = cupy.cuda.runtime.runtimeGetVersion()
diag["cuda"]["device_count"] = cupy.cuda.runtime.getDeviceCount()
for i in range(diag["cuda"]["device_count"]):
d = cupy.cuda.Device(i)
diag["cuda"][f"device_{i}"] = {
"name": d.name,
"compute_capability": d.compute_capability,
"total_memory_mb": d.mem_info[1] // 1024 // 1024
}
except ImportError:
diag["cuda"]["error"] = "CuPy not installed"
diag["cuda"]["install_hint"] = "pip install cupy-cuda12x --break-system-packages"
except Exception as e:
diag["cuda"]["error"] = str(e)
# Check PyOpenCL
try:
import pyopencl as cl
diag["opencl"]["pyopencl_version"] = cl.VERSION_TEXT
for p in cl.get_platforms():
platform_info = {"name": p.name, "devices": []}
for d in p.get_devices():
platform_info["devices"].append({
"name": d.name,
"type": cl.device_type.to_string(d.type),
"memory_mb": d.global_mem_size // 1024 // 1024,
"compute_units": d.max_compute_units
})
diag["opencl"][p.name] = platform_info
except ImportError:
diag["opencl"]["error"] = "PyOpenCL not installed"
diag["opencl"]["install_hint"] = "pip install pyopencl"
except Exception as e:
diag["opencl"]["error"] = str(e)
# Check NumPy
import numpy as np
diag["numpy"]["version"] = np.__version__
return diag
Frontend — show diagnostic info:
// In GPUIndicator.tsx — when only CPU detected, show help
{devices.length === 1 && devices[0].type === 'cpu' && (
<div className="text-xs text-yellow-400 mt-2 p-2 bg-yellow-900/20 rounded">
⚠ No GPU detected.
<button
onClick={() => fetchDiagnostics()}
className="underline ml-1"
>
Run diagnostics
</button>
</div>
)}
Auto-install hint in UI:
⚠ No GPU detected
For NVIDIA GPU: pip install cupy-cuda12x
For Intel/AMD GPU: pip install pyopencl
[Run Diagnostics] [Install Guide]
Dual GPU handling (Intel + NVIDIA laptop):
# When both Intel (OpenCL) and NVIDIA (CUDA) found:
# - List both in device selector
# - Default to NVIDIA CUDA (faster)
# - Allow user to switch
# - Intel iGPU via OpenCL is still ~3-5x faster than CPU
# Example device list for dual GPU laptop:
# 1. ⚡ NVIDIA GeForce RTX 4060 (CUDA) — 8 GB [DEFAULT]
# 2. ⚡ Intel UHD Graphics 770 (OpenCL) — shared memory
# 3. 💻 CPU (16 cores)
1B. GPU Indicator UI — Fix Overlap with Fit Button
Problem: GPU device dropdown overlaps with the "Fit" button in top-right corner.
Solution:
- Keep compact "⚡ CPU" badge in header
- Dropdown opens to the LEFT or DOWNWARD, not overlapping map controls
- Proper z-index and positioning
- Shorter labels: "CPU" not "CPU (NumPy)"
Files:
frontend/src/components/ui/GPUIndicator.tsxbackend/app/services/gpu_backend.pybackend/app/api/routes/gpu.py
2. Coverage Boundary — Improve Accuracy
Problem: Current boundary shows a rough circle/ellipse shape that doesn't follow actual coverage contour.
Current behavior: Boundary seems to be based on simple distance radius rather than actual RSRP threshold contour.
Expected behavior: Boundary should follow the actual -100 dBm (or configured threshold) contour line — an irregular shape that follows terrain, buildings, vegetation shadows.
Solution:
# Backend approach: Generate contour from actual RSRP grid
import numpy as np
from scipy.ndimage import binary_dilation, binary_erosion
from shapely.geometry import MultiPoint
from shapely.ops import unary_union
def calculate_coverage_boundary(points: list, threshold_dbm: float = -100) -> list:
"""
Calculate coverage boundary as convex hull of points above threshold.
Returns list of [lat, lon] coordinates forming the boundary polygon.
"""
# Filter points above threshold
valid_points = [(p['lat'], p['lon']) for p in points if p['rsrp'] >= threshold_dbm]
if len(valid_points) < 3:
return []
# Create concave hull (alpha shape) for realistic boundary
# Concave hull follows the actual shape better than convex hull
from shapely.geometry import MultiPoint
multi_point = MultiPoint(valid_points)
# Alpha shape — adjust alpha for detail level
# Higher alpha = more detailed (but slower)
boundary = concave_hull(multi_point, ratio=0.3)
if boundary.is_empty:
return []
# Simplify to reduce points (tolerance in degrees ≈ 100m)
simplified = boundary.simplify(0.001)
# Return as coordinate list
coords = list(simplified.exterior.coords)
return [[lat, lon] for lat, lon in coords]
# Alternative: Grid-based contour approach
def calculate_boundary_from_grid(
grid_points: list,
threshold_dbm: float,
grid_resolution_m: float
) -> list:
"""
Create boundary by finding edge cells of coverage area.
More accurate than hull — follows actual coverage gaps.
"""
import numpy as np
# Build 2D RSRP grid
lats = sorted(set(p['lat'] for p in grid_points))
lons = sorted(set(p['lon'] for p in grid_points))
grid = np.full((len(lats), len(lons)), np.nan)
lat_idx = {lat: i for i, lat in enumerate(lats)}
lon_idx = {lon: i for i, lon in enumerate(lons)}
for p in grid_points:
i = lat_idx[p['lat']]
j = lon_idx[p['lon']]
grid[i, j] = p['rsrp']
# Binary mask: above threshold
mask = grid >= threshold_dbm
# Find boundary: dilate - original = edge cells
dilated = binary_dilation(mask)
boundary_mask = dilated & ~mask
# Extract boundary coordinates
boundary_coords = []
for i in range(len(lats)):
for j in range(len(lons)):
if boundary_mask[i, j]:
boundary_coords.append([lats[i], lons[j]])
# Order points for polygon (traveling salesman approximate)
if len(boundary_coords) > 2:
ordered = order_boundary_points(boundary_coords)
return ordered
return boundary_coords
Frontend changes:
- Receive boundary polygon from backend (already calculated with results)
- Or calculate client-side from grid points
- Render as Leaflet polygon with dashed white stroke
- Should follow actual coverage shape, not circular approximation
Files:
backend/app/services/coverage_service.py— add boundary calculationfrontend/src/components/map/CoverageBoundary.tsx— render real contour
3. Session History — Show Propagation Parameters
Problem: History entries only show preset, points, radius, resolution. Missing propagation settings used.
Solution: Save full propagation config snapshot with each history entry.
// frontend/src/store/calcHistory.ts
interface HistoryEntry {
id: string;
timestamp: Date;
computationTime: number;
preset: string;
radius: number;
resolution: number;
totalPoints: number;
// Coverage results
coverage: {
excellent: number; // percentage
good: number;
fair: number;
weak: number;
};
avgRsrp: number;
rangeMin: number;
rangeMax: number;
// NEW: Propagation parameters snapshot
propagation: {
modelsUsed: string[]; // ["Free-Space", "terrain_los", ...]
modelCount: number; // 12
frequency: number; // 2100 MHz
txPower: number; // 46 dBm
antennaGain: number; // 15 dBi
antennaHeight: number; // 10 m
// Environment
season: string; // "Winter (30%)"
temperature: string; // "15°C (mild)"
humidity: string; // "50% (normal)"
rainConditions: string; // "Light Rain"
indoorCoverage: string; // "Medium Building (brick)"
// Margins
fadingMargin: number; // 0 dB
// Atmospheric
atmosphericAbsorption: boolean;
};
// Site config
sites: number; // 2
sectors: number; // total sectors
}
Display in History panel:
// Expanded history entry shows propagation details
<div className="history-entry-expanded">
{/* Existing: time, points, coverage bars */}
{/* NEW: Propagation summary (collapsed by default) */}
<details className="mt-2">
<summary className="text-xs text-gray-400 cursor-pointer hover:text-gray-300">
▸ Propagation: {entry.propagation.modelCount} models, {entry.propagation.frequency} MHz
</summary>
<div className="mt-1 pl-3 text-xs text-gray-500 space-y-0.5">
<div>TX: {entry.propagation.txPower} dBm, Gain: {entry.propagation.antennaGain} dBi</div>
<div>Height: {entry.propagation.antennaHeight}m</div>
<div>Environment: {entry.propagation.season}, {entry.propagation.rainConditions}</div>
<div>Indoor: {entry.propagation.indoorCoverage}</div>
{entry.propagation.fadingMargin > 0 && (
<div>Fading margin: {entry.propagation.fadingMargin} dB</div>
)}
<div className="flex flex-wrap gap-1 mt-1">
{entry.propagation.modelsUsed.map(model => (
<span key={model} className="px-1 py-0.5 bg-slate-700 rounded text-[10px]">
{model}
</span>
))}
</div>
</div>
</details>
</div>
Files:
frontend/src/store/calcHistory.ts— extend HistoryEntry type, save propagationfrontend/src/components/panels/HistoryPanel.tsx— show expandable propagation detailsbackend/app/api/websocket.py— include propagation config in result messagebackend/app/services/coverage_service.py— return config snapshot with results
4. Results Popup — Show Propagation Summary
Problem: Calculation Complete popup shows time, points, coverage bars — but not which models were used.
Solution: Add compact propagation info to results popup.
// frontend/src/components/ui/ResultsPopup.tsx
// Add below coverage bars:
<div className="mt-2 text-xs text-gray-400">
<span>{result.modelsUsed?.length || 0} models</span>
<span className="mx-1">•</span>
<span>{result.frequency} MHz</span>
{result.fadingMargin > 0 && (
<>
<span className="mx-1">•</span>
<span>FM: {result.fadingMargin} dB</span>
</>
)}
{result.indoorCoverage && result.indoorCoverage !== 'none' && (
<>
<span className="mx-1">•</span>
<span>Indoor: {result.indoorCoverage}</span>
</>
)}
</div>
Files:
frontend/src/components/ui/ResultsPopup.tsx
5. Batch Frequency Change (from 3.5.0 backlog)
Problem: To compare coverage at different frequencies, user must edit each sector manually.
Solution: Quick-change buttons in toolbar or Coverage Settings.
// frontend/src/components/panels/BatchOperations.tsx (NEW)
const QUICK_BANDS = [
{ freq: 700, label: '700', band: 'B28', color: 'text-red-400' },
{ freq: 800, label: '800', band: 'B20', color: 'text-orange-400' },
{ freq: 900, label: '900', band: 'B8', color: 'text-yellow-400' },
{ freq: 1800, label: '1800', band: 'B3', color: 'text-green-400' },
{ freq: 2100, label: '2100', band: 'B1', color: 'text-blue-400' },
{ freq: 2600, label: '2600', band: 'B7', color: 'text-purple-400' },
{ freq: 3500, label: '3500', band: 'n78', color: 'text-pink-400' },
];
export function BatchFrequencyChange() {
return (
<div className="p-3 border-t border-slate-700">
<h4 className="text-xs font-semibold text-gray-400 mb-2">
SET ALL SECTORS
</h4>
<div className="flex flex-wrap gap-1">
{QUICK_BANDS.map(b => (
<button
key={b.freq}
onClick={() => setAllSectorsFrequency(b.freq)}
className="px-2 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded"
title={`${b.band} — ${b.freq} MHz`}
>
<span className={b.color}>{b.label}</span>
</button>
))}
</div>
</div>
);
}
Location: Below site list, above Coverage Settings.
Files:
frontend/src/components/panels/BatchOperations.tsx(NEW)frontend/src/store/sites.ts— addsetAllSectorsFrequency()action
6. Minor UI Fixes
6.1 Terrain Profile — Click Propagation (verify fix)
- Verify that clicking "Terrain Profile" button no longer adds ruler point
- If still broken: ensure e.stopPropagation() AND e.preventDefault() on button
6.2 GPU Indicator — Shorter Label
- Current: "CPU (NumPy)" — too long
- Should be: "CPU" or "⚡ CPU"
- When GPU active: "⚡ RTX 4060" (short device name)
6.3 Coordinate Display — Show Elevation ✅ WORKS
- Elevation loads on hover with delay — NOT a bug
- Shows "Elev: 380m ASL" after holding cursor on map
- No fix needed
Implementation Order
Priority 1 — Quick Fixes (30 min)
- GPU indicator positioning (no overlap with Fit)
- GPU detection — install CuPy/PyOpenCL, diagnostics endpoint
- Terrain Profile click fix (verify)
Priority 2 — History Enhancement (1 hour)
- Extend HistoryEntry with propagation params
- Save propagation snapshot on calculation complete
- Expandable propagation details in History panel
- Results popup — show model count + frequency
Priority 3 — Coverage Boundary (1-2 hours)
- Implement contour-based boundary from actual RSRP grid
- Replace circular approximation with real coverage shape
- Test with multi-site calculations
- Smooth boundary line (simplify polygon)
Priority 4 — Batch Frequency (30 min)
- BatchOperations component
- setAllSectorsFrequency store action
- Wire into sidebar panel
Success Criteria
- GPU indicator does not overlap with any map controls
- Coverage boundary follows actual coverage shape (not circular)
- History entries show expandable propagation parameters
- Results popup shows model count and frequency
- Batch frequency change updates all sectors at once
- Terrain Profile button click doesn't add ruler point
- Elevation displays correctly in bottom-left
Files Summary
New Files
frontend/src/components/panels/BatchOperations.tsx
Modified Files
frontend/src/components/ui/GPUIndicator.tsx— fix position/overlapfrontend/src/components/map/CoverageBoundary.tsx— real contourfrontend/src/components/ui/ResultsPopup.tsx— propagation infofrontend/src/store/calcHistory.ts— extended HistoryEntryfrontend/src/components/panels/HistoryPanel.tsx— expandable detailsfrontend/src/store/sites.ts— batch frequency actionbackend/app/services/coverage_service.py— boundary calculation, config snapshotbackend/app/api/websocket.py— include config in results
"Polish makes the difference between a tool and a product" ✨