@mytec: iter3.5.1 ready for testing
This commit is contained in:
557
RFCP-Iteration-3.5.1-Bugfixes-Polish.md
Normal file
557
RFCP-Iteration-3.5.1-Bugfixes-Polish.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# 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' && (
|
||||
<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):**
|
||||
```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
|
||||
|
||||
<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 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:
|
||||
<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.
|
||||
|
||||
```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 (
|
||||
<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` — 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"* ✨
|
||||
Reference in New Issue
Block a user