# 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:**
1. Check if CuPy is actually installed in the Python environment
2. Check if CUDA toolkit is accessible from the app's runtime
3. Check if PyOpenCL is installed (fallback for Intel GPU)
4. The backend GPU detection may be failing silently
**Debug steps to add:**
```python
# 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:**
```python
# 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:**
```typescript
// In GPUIndicator.tsx — when only CPU detected, show help
{devices.length === 1 && devices[0].type === 'cpu' && (
⚠ No GPU detected.
)}
```
**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):**
```python
# 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.tsx`
- `backend/app/services/gpu_backend.py`
- `backend/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:**
```python
# 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]
```
```python
# 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 calculation
- `frontend/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.
```typescript
// 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:**
```typescript
// Expanded history entry shows propagation details