# 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
{/* Existing: time, points, coverage bars */} {/* NEW: Propagation summary (collapsed by default) */}
▸ Propagation: {entry.propagation.modelCount} models, {entry.propagation.frequency} MHz
TX: {entry.propagation.txPower} dBm, Gain: {entry.propagation.antennaGain} dBi
Height: {entry.propagation.antennaHeight}m
Environment: {entry.propagation.season}, {entry.propagation.rainConditions}
Indoor: {entry.propagation.indoorCoverage}
{entry.propagation.fadingMargin > 0 && (
Fading margin: {entry.propagation.fadingMargin} dB
)}
{entry.propagation.modelsUsed.map(model => ( {model} ))}
``` **Files:** - `frontend/src/store/calcHistory.ts` — extend HistoryEntry type, save propagation - `frontend/src/components/panels/HistoryPanel.tsx` — show expandable propagation details - `backend/app/api/websocket.py` — include propagation config in result message - `backend/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. ```typescript // frontend/src/components/ui/ResultsPopup.tsx // Add below coverage bars:
{result.modelsUsed?.length || 0} models {result.frequency} MHz {result.fadingMargin > 0 && ( <> FM: {result.fadingMargin} dB )} {result.indoorCoverage && result.indoorCoverage !== 'none' && ( <> Indoor: {result.indoorCoverage} )}
``` **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. ```typescript // 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 (

SET ALL SECTORS

{QUICK_BANDS.map(b => ( ))}
); } ``` **Location:** Below site list, above Coverage Settings. **Files:** - `frontend/src/components/panels/BatchOperations.tsx` (NEW) - `frontend/src/store/sites.ts` — add `setAllSectorsFrequency()` 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/overlap - `frontend/src/components/map/CoverageBoundary.tsx` — real contour - `frontend/src/components/ui/ResultsPopup.tsx` — propagation info - `frontend/src/store/calcHistory.ts` — extended HistoryEntry - `frontend/src/components/panels/HistoryPanel.tsx` — expandable details - `frontend/src/store/sites.ts` — batch frequency action - `backend/app/services/coverage_service.py` — boundary calculation, config snapshot - `backend/app/api/websocket.py` — include config in results --- *"Polish makes the difference between a tool and a product"* ✨