Files
rfcp/RFCP-Iteration-3.5.2-Native-GPU-Polish.md

14 KiB
Raw Blame History

RFCP — Iteration 3.5.2: Native Backend + GPU Fix + UI Polish

Overview

Fix critical architecture issues: GPU indicator dropdown broken, GPU acceleration not working (CuPy in wrong Python environment), and prepare path to remove WSL2 dependency for end users. Plus UI polish items carried over from 3.5.1.

Priority: GPU fixes first, then UI polish, then native Windows exploration.


CRITICAL CONTEXT

Current Architecture Problem

RFCP.exe (Electron, Windows)
  └── launches backend via WSL2:
      python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8090
      └── /usr/bin/python3 (WSL2 system Python 3.12)
          └── NO venv, NO CuPy installed

User installed CuPy in Windows Python → backend doesn't see it.
User installed CuPy in WSL system Python → needs --break-system-packages

GPU Hardware (Confirmed Working)

nvidia-smi output (from WSL2):
  NVIDIA GeForce RTX 4060 Laptop GPU
  Driver: 581.42 (Windows) / 580.95.02 (WSL2)
  CUDA: 13.0
  VRAM: 8188 MiB
  GPU passthrough: WORKING ✅

Files to Reference

backend/app/services/gpu_backend.py   — GPUManager class
backend/app/api/routes/gpu.py         — GPU API endpoints  
frontend/src/components/ui/GPUIndicator.tsx — GPU badge/dropdown
desktop/                              — Electron app source
installer/                            — Build scripts

Task 1: Fix GPU Indicator Dropdown Z-Index (Priority 1 — 10 min)

Problem

GPU dropdown WORKS (opens on click, shows diagnostics, install hints) but renders BEHIND the right sidebar panel. The sidebar (Sites, Coverage Settings) has higher z-index than the GPU dropdown, so the dropdown is invisible/hidden underneath.

See screenshots: dropdown is partially visible only when sidebar is made very narrow. It shows: "COMPUTE DEVICES", "CPU (NumPy)", install hints, "Run Diagnostics", and even diagnostics JSON — all working but hidden behind sidebar.

Root Cause

GPUIndicator dropdown z-index is lower than the right sidebar panel z-index.

Solution

In GPUIndicator.tsx — find the dropdown container div and set z-index higher than the sidebar:

{isOpen && (
  <div 
    className="absolute top-full mt-1 bg-dark-surface border border-dark-border 
               rounded-lg shadow-2xl p-3 min-w-[300px]"
    style={{ zIndex: 9999 }}  // MUST be above sidebar (which is ~z-50 or z-auto)
  >
    ...
  </div>
)}

Key requirements:

  1. z-index: 9999 (or at minimum higher than sidebar)
  2. Position: dropdown should open to the LEFT (toward center of screen) to avoid being cut off by right edge
  3. right-0 on the absolute positioning (anchored to right edge of badge)

Alternative approach — use Tailwind z-index:

className="absolute top-full right-0 mt-1 z-[9999] ..."

Also check: The parent container of GPUIndicator might need position: relative for absolute positioning to work correctly against the right sidebar.

Testing

  • Click "CPU" badge → dropdown appears ABOVE the sidebar
  • Full dropdown visible: devices, install hints, diagnostics
  • Dropdown doesn't get cut off on right side
  • Click outside → dropdown closes
  • Dropdown works at any window width

Task 2: Install CuPy in WSL Backend (Priority 1 — 10 min)

Problem

CuPy installed in Windows Python, but backend runs in WSL2 system Python.

Solution

Add a startup check in the backend that detects missing GPU packages and provides clear instructions. Also, the Electron app should try to install dependencies on first launch.

Step 1: Backend startup GPU check

In backend/app/main.py, add on startup:

@app.on_event("startup")
async def check_gpu_availability():
    """Log GPU status on startup for debugging."""
    import logging
    logger = logging.getLogger("rfcp.gpu")
    
    # Check CuPy
    try:
        import cupy as cp
        device_count = cp.cuda.runtime.getDeviceCount()
        if device_count > 0:
            name = cp.cuda.Device(0).name
            mem = cp.cuda.Device(0).mem_info[1] // 1024 // 1024
            logger.info(f"✅ GPU detected: {name} ({mem} MB VRAM)")
            logger.info(f"   CuPy {cp.__version__}, CUDA devices: {device_count}")
        else:
            logger.warning("⚠️ CuPy installed but no CUDA devices found")
    except ImportError:
        logger.warning("⚠️ CuPy not installed — GPU acceleration disabled")
        logger.warning("   Install: pip install cupy-cuda12x --break-system-packages")
    except Exception as e:
        logger.warning(f"⚠️ CuPy error: {e}")
    
    # Check PyOpenCL
    try:
        import pyopencl as cl
        platforms = cl.get_platforms()
        for p in platforms:
            for d in p.get_devices():
                logger.info(f"✅ OpenCL device: {d.name.strip()}")
    except ImportError:
        logger.info(" PyOpenCL not installed (optional)")
    except Exception:
        pass

Step 2: GPU diagnostics endpoint enhancement

Enhance /api/gpu/diagnostics to return install commands:

@router.get("/diagnostics")
async def gpu_diagnostics():
    import platform, sys
    
    diagnostics = {
        "python": sys.version,
        "platform": platform.platform(),
        "executable": sys.executable,
        "is_wsl": "microsoft" in platform.release().lower(),
        "cuda_available": False,
        "opencl_available": False,
        "install_hint": "",
        "devices": []
    }
    
    # Check nvidia-smi
    try:
        import subprocess
        result = subprocess.run(
            ["nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader"],
            capture_output=True, text=True, timeout=5
        )
        if result.returncode == 0:
            diagnostics["nvidia_smi"] = result.stdout.strip()
    except:
        diagnostics["nvidia_smi"] = "not found"
    
    # Check CuPy
    try:
        import cupy
        diagnostics["cupy_version"] = cupy.__version__
        diagnostics["cuda_available"] = True
        count = cupy.cuda.runtime.getDeviceCount()
        for i in range(count):
            d = cupy.cuda.Device(i)
            diagnostics["devices"].append({
                "id": i,
                "name": d.name,
                "memory_mb": d.mem_info[1] // 1024 // 1024,
                "backend": "CUDA"
            })
    except ImportError:
        if diagnostics.get("is_wsl"):
            diagnostics["install_hint"] = "pip3 install cupy-cuda12x --break-system-packages"
        else:
            diagnostics["install_hint"] = "pip install cupy-cuda12x"
    
    return diagnostics

Step 3: Frontend shows diagnostics clearly

In GPUIndicator dropdown, show:

⚠ No GPU detected

Your system: WSL2 + NVIDIA RTX 4060

To enable GPU acceleration:
┌─────────────────────────────────────────────┐
│ pip3 install cupy-cuda12x                   │
│      --break-system-packages                │
└─────────────────────────────────────────────┘
Then restart RFCP.

[Copy Command]  [Run Diagnostics]

Testing

  • Backend startup logs GPU status
  • /api/gpu/diagnostics returns WSL detection + install hint
  • Frontend shows clear install instructions
  • After installing CuPy in WSL + restart → GPU appears in list

Task 3: Terrain Profile Click Fix (Priority 2 — 5 min)

Problem

Clicking "Terrain Profile" button in ruler measurement also adds a point on the map.

Solution

In the Terrain Profile button handler:

const handleTerrainProfile = (e: React.MouseEvent) => {
  e.stopPropagation();
  e.preventDefault();
  // ... open terrain profile
};

Also check if the button is rendered inside a map click handler area — may need L.DomEvent.disableClickPropagation(container) on the parent.

Testing

  • Click "Terrain Profile" → opens profile, NO new ruler point added
  • Map click still works normally when not clicking the button

Task 4: Coverage Boundary — Real Contour Shape (Priority 2 — 45 min)

Problem

Current boundary is a rough circle/ellipse. Should follow actual coverage contour.

Approaches

Option A: Shapely Alpha Shape (recommended)

# backend/app/services/boundary_service.py

from shapely.geometry import MultiPoint
from shapely.ops import unary_union
import numpy as np

def calculate_coverage_boundary(points: list, threshold_dbm: float = -100) -> list:
    """Calculate concave hull of coverage area above threshold."""
    
    # Filter points above threshold
    valid = [(p['lon'], p['lat']) for p in points if p['rsrp'] >= threshold_dbm]
    
    if len(valid) < 3:
        return []
    
    mp = MultiPoint(valid)
    
    # Use convex hull first, then try concave
    try:
        # Shapely 2.0+ has concave_hull
        from shapely import concave_hull
        hull = concave_hull(mp, ratio=0.3)
    except ImportError:
        # Fallback to convex hull
        hull = mp.convex_hull
    
    # Simplify to reduce points (0.001 deg ≈ 100m)
    simplified = hull.simplify(0.001, preserve_topology=True)
    
    # Extract coordinates
    if simplified.geom_type == 'Polygon':
        coords = list(simplified.exterior.coords)
        return [{'lat': c[1], 'lon': c[0]} for c in coords]
    
    return []

Option B: Grid-based contour (simpler)

def grid_contour_boundary(points: list, threshold_dbm: float, resolution: float):
    """Find boundary by detecting edge cells in grid."""
    
    # Create binary grid: 1 = above threshold, 0 = below
    # Find cells where 1 is adjacent to 0 = boundary
    # Convert cell coords back to lat/lon
    # Return ordered boundary points

API Endpoint

# Add to coverage calculation response
@router.post("/coverage/calculate")
async def calculate_coverage(...):
    result = coverage_service.calculate(...)
    
    # Calculate boundary
    if result.points:
        boundary = calculate_coverage_boundary(
            result.points, 
            threshold_dbm=settings.min_signal
        )
        result.boundary = boundary
    
    return result

Frontend

// CoverageBoundary.tsx — use returned boundary coords
// Instead of calculating alpha shape on frontend

const CoverageBoundary = ({ points, boundary }) => {
  // If server returned boundary, use it
  if (boundary && boundary.length > 0) {
    return <Polygon positions={boundary.map(p => [p.lat, p.lon])} />;
  }
  
  // Fallback to current convex hull implementation
  return <CurrentImplementation points={points} />;
};

Dependencies

Need shapely installed:

pip install shapely  # or pip3 install shapely --break-system-packages

Check if already in requirements.txt.

Testing

  • 5km calculation → boundary follows actual coverage shape
  • 10km calculation → boundary is irregular (terrain-dependent)
  • Toggle boundary on/off works
  • Boundary doesn't crash with < 3 points

Task 5: Results Popup Enhancement (Priority 3 — 15 min)

Problem

Calculation complete toast/popup doesn't show which models were used.

Solution

Enhance the toast message after calculation:

// Current:
toast.success(`Calculated ${points} points in ${time}s`);

// Enhanced:
const modelCount = result.modelsUsed?.length ?? 0;
const freq = sites[0]?.frequency ?? 0;
const presetName = settings.preset ?? 'custom';

toast.success(
  `${points} pts • ${time}s • ${presetName}${freq} MHz • ${modelCount} models`,
  { duration: 5000 }
);

Testing

  • After calculation, toast shows: points, time, preset, frequency, model count

Task 6: Native Windows Backend (Priority 3 — Research/Plan)

Problem

Current setup REQUIRES WSL2. Users without WSL2 can't use RFCP at all.

Current Flow

RFCP.exe (Electron)
  → detects WSL2
  → launches: wsl python3 -m uvicorn ...
  → backend runs in WSL2 Linux

Target Flow

RFCP.exe (Electron)
  → Option A: embedded Python (Windows native)
  → Option B: detect system Python (Windows)
  → Option C: keep WSL2 but with fallback

Research Tasks (don't implement yet, just investigate)

  1. Check how Electron currently launches backend:
# Look at desktop/ directory
cat desktop/src/main.ts   # or main.js
# Find where it spawns python/uvicorn
  1. Check if Windows Python works for backend:
# In Windows PowerShell:
cd D:\root\rfcp\backend
python -m uvicorn app.main:app --host 0.0.0.0 --port 8090
# Does it start? What errors?
  1. Evaluate embedded Python options:

    • python-embedded (official, ~30 MB)
    • PyInstaller (bundle backend as .exe)
    • cx_Freeze
    • Nuitka (compile Python to C)
  2. Document findings — create a brief report:

RFCP-Native-Backend-Research.md
  - Current architecture (WSL2 dependency)
  - Windows Python compatibility test results
  - Recommended approach
  - Migration steps
  - Timeline estimate

Goal

User downloads RFCP.exe → installs → clicks icon → everything works. No WSL2. No manual pip install. GPU auto-detected.


Implementation Order

Priority 1 (30 min total)

  1. Task 1: Fix GPU dropdown — make it clickable again
  2. Task 2: GPU diagnostics + install instructions in UI
  3. Task 3: Terrain Profile click propagation fix

Priority 2 (1 hour)

  1. Task 4: Coverage boundary real contour (shapely)
  2. Task 5: Results popup enhancement

Priority 3 (Research only)

  1. Task 6: Investigate native Windows backend — report only, no implementation

Build & Deploy

# After implementation:
cd /mnt/d/root/rfcp/frontend
npx tsc --noEmit           # TypeScript check
npm run build              # Production build

# Rebuild Electron:
cd /mnt/d/root/rfcp/installer
bash build-win.sh

# Test:
# Install new .exe and verify GPU indicator works

Success Criteria

  • GPU dropdown opens when clicking badge
  • Dropdown shows device list or install instructions
  • After pip3 install cupy-cuda12x --break-system-packages in WSL + restart → GPU visible
  • Terrain Profile click doesn't add ruler points
  • Coverage boundary follows actual signal contour
  • Results toast shows model count and frequency
  • Native Windows backend research document created