@mytec: iter3.7.0 start, gpu calc int

This commit is contained in:
2026-02-03 22:41:08 +02:00
parent a61753c642
commit 6cd9d869cc
29 changed files with 2288 additions and 28 deletions

View File

@@ -45,7 +45,8 @@
"Bash(journalctl:*)",
"Bash(pkill:*)",
"Bash(pip3 list:*)",
"Bash(chmod:*)"
"Bash(chmod:*)",
"Bash(pyinstaller:*)"
]
}
}

4
.gitignore vendored
View File

@@ -24,3 +24,7 @@ installer/dist/
__pycache__/
*.pyc
nul
# PyInstaller build artifacts
backend/build/
backend/dist/

View File

@@ -0,0 +1,130 @@
# RFCP 3.6.0 — Production GPU Build (Claude Code Task)
## Goal
Build `rfcp-server.exe` (PyInstaller) with CuPy GPU support so production RFCP
detects the NVIDIA GPU without manual `pip install`.
Currently production exe shows "CPU (NumPy)" because CuPy is not bundled.
## Current Environment (CONFIRMED WORKING)
```
Windows 10 (10.0.26200)
Python 3.11.8 (C:\Python311)
NVIDIA GeForce RTX 4060 Laptop GPU (8 GB VRAM)
CUDA Toolkit 13.1 (C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1)
CUDA_PATH = C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1
Packages:
cupy-cuda13x 13.6.0 ← NOT cuda12x!
numpy 1.26.4
scipy 1.17.0
fastrlock 0.8.3
pyinstaller 6.18.0
GPU compute verified:
python -c "import cupy; a = cupy.array([1,2,3]); print(a.sum())" → 6 ✅
```
## What We Already Tried (And Why It Failed)
### Attempt 1: ONEFILE spec with collect_all('cupy')
- `collect_all('cupy')` returns 1882 datas, **0 binaries** — CuPy pip doesn't bundle DLLs on Windows
- CUDA DLLs come from two separate sources:
- **nvidia pip packages** (14 DLLs in `C:\Python311\Lib\site-packages\nvidia\*/bin/`)
- **CUDA Toolkit** (13 DLLs in `C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.1\bin\x64\`)
- We manually collected these 27 DLLs in the spec
- Build succeeded (3 GB exe!) but crashed on launch:
```
[PYI-10456:ERROR] Failed to extract cufft64_12.dll: decompression resulted in return code -1!
```
- Root cause: `cufft64_12.dll` is 297 MB — PyInstaller's zlib compression fails on it in ONEFILE mode
### Attempt 2: We were about to try ONEDIR but haven't built it yet
### Key Insight: Duplicate DLLs from two sources
nvidia pip packages have CUDA 12.x DLLs (cublas64_12.dll etc.)
CUDA Toolkit 13.1 has CUDA 13.x DLLs (cublas64_13.dll etc.)
CuPy-cuda13x needs the 13.x versions. The 12.x from pip may conflict.
## What Needs To Happen
1. **Build rfcp-server as ONEDIR** (folder with exe + DLLs, not single exe)
- This avoids the decompression crash with large CUDA DLLs
- Output: `backend/dist/rfcp-server/rfcp-server.exe` + all DLLs alongside
2. **Include ONLY the correct CUDA DLLs**
- Prefer CUDA Toolkit 13.1 DLLs (match cupy-cuda13x)
- The nvidia pip packages have cuda12x DLLs — may cause version conflicts
- Key DLLs needed: cublas, cusparse, cusolver, curand, cufft, nvrtc, cudart
3. **Exclude bloat** — the previous build pulled in tensorflow, grpc, opentelemetry etc.
making it 3 GB. Real size should be ~600-800 MB.
4. **Test the built exe** — run it standalone and verify:
- `curl http://localhost:8090/api/health` returns `"build": "gpu"`
- `curl http://localhost:8090/api/gpu/status` returns `"available": true`
- Or at minimum: the exe starts without errors and CuPy imports successfully
5. **Update Electron integration** if needed:
- Current Electron expects a single `rfcp-server.exe` file
- With ONEDIR, it's a folder `rfcp-server/rfcp-server.exe`
- File: `desktop/main.js` or `desktop/src/main.ts` — look for where it spawns backend
- The path needs to change from `resources/backend/rfcp-server.exe`
to `resources/backend/rfcp-server/rfcp-server.exe`
## File Locations
```
D:\root\rfcp\
├── backend\
│ ├── run_server.py ← PyInstaller entry point
│ ├── app\
│ │ ├── main.py ← FastAPI app
│ │ ├── services\
│ │ │ ├── gpu_backend.py ← GPU detection (CuPy/NumPy fallback)
│ │ │ └── coverage_service.py ← Uses get_array_module()
│ │ └── api\routes\gpu.py ← /api/gpu/status, /api/gpu/diagnostics
│ ├── dist\ ← PyInstaller output goes here
│ └── build\ ← PyInstaller build cache
├── installer\
│ ├── rfcp-server-gpu.spec ← GPU spec (needs fixing)
│ ├── rfcp-server.spec ← CPU spec (working, don't touch)
│ ├── rfcp.ico ← Icon (exists)
│ └── build-gpu.bat ← Build script
├── desktop\
│ ├── main.js or src/main.ts ← Electron main process
│ └── resources\backend\ ← Where production exe lives
└── frontend\ ← React frontend (no changes needed)
```
## Existing CPU spec for reference
The working CPU-only spec is at `installer/rfcp-server.spec`. Use it as the base
and ADD CuPy + CUDA on top. Don't reinvent the wheel.
## Build Command
```powershell
cd D:\root\rfcp\backend
pyinstaller ..\installer\rfcp-server-gpu.spec --clean --noconfirm
```
## Success Criteria
- [ ] `dist/rfcp-server/rfcp-server.exe` starts without errors
- [ ] CuPy imports successfully inside the exe (no missing DLL errors)
- [ ] `/api/gpu/status` returns `"available": true, "device": "RTX 4060"`
- [ ] Total folder size < 1 GB (ideally 600-800 MB)
- [ ] No tensorflow/grpc/opentelemetry bloat
- [ ] Electron can find and launch the backend (path updated if needed)
## Important Notes
- Do NOT use cupy-cuda12x — we migrated to cupy-cuda13x
- Do NOT try ONEFILE mode — cufft64_12.dll (297 MB) crashes decompression
- The nvidia pip packages (nvidia-cublas-cu12, etc.) are still installed but may
conflict with CUDA Toolkit 13.1 — prefer Toolkit DLLs
- `collect_all('cupy')` gives 0 binaries on Windows — DLLs must be manually specified
- gpu_backend.py already handles CuPy absence gracefully (falls back to NumPy)

View File

@@ -0,0 +1,133 @@
# RFCP 3.7.0 — GPU-Accelerated Coverage Calculations
## Context
Iteration 3.6.0 completed: CuPy-cuda13x works in production PyInstaller build,
RTX 4060 detected, ONEDIR build with CUDA DLLs. BUT coverage calculations still
run on CPU because coverage_service.py uses `import numpy as np` directly instead
of the GPU backend.
The GPU infrastructure is ready:
- `app/services/gpu_backend.py` has `GPUManager.get_array_module()` → returns cupy or numpy
- `/api/gpu/status` confirms `"active_backend": "cuda"`
- CuPy is imported and GPU detected in the frozen exe
## Goal
Replace direct `np.` calls in coverage_service.py with `xp = gpu_manager.get_array_module()`
so calculations run on GPU when available, with automatic NumPy fallback.
## Files to Modify
### `app/services/coverage_service.py`
**Line 7**: `import numpy as np` — keep this but also import gpu_manager
Add near top:
```python
from app.services.gpu_backend import gpu_manager
```
**Key sections to GPU-accelerate** (highest impact first):
#### 1. Grid array creation (lines 549-550, 922-923)
```python
# BEFORE:
grid_lats = np.array([lat for lat, lon in grid])
grid_lons = np.array([lon for lat, lon in grid])
# AFTER:
xp = gpu_manager.get_array_module()
grid_lats = xp.array([lat for lat, lon in grid])
grid_lons = xp.array([lon for lat, lon in grid])
```
#### 2. Trig calculations (line 468, 1031, 1408-1415, 1442)
These use np.cos, np.radians, np.sin, np.degrees, np.arctan2 — all have CuPy equivalents.
```python
# BEFORE:
lon_delta = settings.radius / (111000 * np.cos(np.radians(center_lat)))
cos_lat = np.cos(np.radians(center_lat))
# AFTER:
xp = gpu_manager.get_array_module()
lon_delta = settings.radius / (111000 * float(xp.cos(xp.radians(center_lat))))
cos_lat = float(xp.cos(xp.radians(center_lat)))
```
#### 3. The heavy calculation loop — `_run_point_loop` (line 1070) and `_calculate_point_sync` (line 1112)
This is where 90% of time is spent. Currently processes points one-by-one.
The GPU win comes from vectorizing the path loss calculation across ALL grid points at once.
**Strategy**: Instead of looping through points, create arrays of all distances/angles
and compute path loss for all points in one vectorized operation.
#### 4. `_calculate_bearing` (line 1402) — already vectorizable
```python
# All np.* functions here have direct CuPy equivalents
# Just replace np → xp
```
## Important Rules
1. **Always get xp at function scope**, not module scope:
```python
def my_function(self, ...):
xp = gpu_manager.get_array_module()
# use xp instead of np
```
2. **Convert GPU arrays back to CPU** before returning to non-GPU code:
```python
if hasattr(result, 'get'): # CuPy array
result = result.get() # → numpy array
```
3. **Keep np for small/scalar operations** — GPU overhead isn't worth it for single values.
Only use xp for array operations on 100+ elements.
4. **Don't break the fallback** — if CuPy isn't available, `get_array_module()` returns numpy,
so `xp.array()` etc. work identically.
5. **Test both paths** — run with GPU and verify same results as CPU.
## Testing
After changes:
```powershell
# Rebuild
cd D:\root\rfcp\backend
pyinstaller ..\installer\rfcp-server-gpu.spec --noconfirm
# Run
.\dist\rfcp-server\rfcp-server.exe
# Test calculation via frontend — watch Task Manager GPU utilization
# Should see GPU Compute spike during coverage calculation
# Time should be significantly faster than 10s for 1254 points
```
Compare before/after:
- Current (CPU): ~10s for 1254 points, 5km radius
- Expected (GPU): 1-3s for same calculation
Also test GPU diagnostics:
```
curl http://localhost:8888/api/gpu/diagnostics
```
## What NOT to Change
- Don't modify gpu_backend.py — it's working correctly
- Don't change the API endpoints or response format
- Don't remove the NumPy import — keep it for non-array operations
- Don't change propagation model math — only the array operations
- Don't change _filter_buildings_to_bbox or OSM functions — they use lists not arrays
## Success Criteria
- [ ] Coverage calculation uses GPU (visible in Task Manager)
- [ ] Calculation time reduced for 1000+ point grids
- [ ] CPU fallback still works (test by setting active_backend to cpu via API)
- [ ] Same coverage results (heatmap should look identical)
- [ ] No regression in tiled processing mode

View File

@@ -0,0 +1,556 @@
# RFCP — Iteration 3.6.0: Production GPU Build
## Overview
Enable GPU acceleration in the production PyInstaller build. Currently production
runs CPU-only (NumPy) because CuPy is not included in rfcp-server.exe.
**Goal:** User with NVIDIA GPU installs RFCP → GPU detected automatically →
coverage calculations use CUDA acceleration. No manual pip install required.
**Context from diagnostics screenshot:**
```json
{
"python_executable": "C:\\Users\\Administrator\\AppData\\Local\\Programs\\RFCP\\resources\\backend\\rfcp-server.exe",
"platform": "Windows-10-10.0.26288-SP0",
"is_wsl": false,
"numpy": { "version": "1.26.4" },
"cuda": {
"error": "CuPy not installed",
"install_hint": "pip install cupy-cuda12x"
}
}
```
**Architecture:** Production uses PyInstaller-bundled rfcp-server.exe (self-contained).
CuPy not included → GPU not available for end users.
---
## Strategy: Two-Tier Build
Instead of one massive binary, produce two builds:
```
RFCP-Setup-{version}.exe (~150 MB) — CPU-only, works everywhere
RFCP-Setup-{version}-GPU.exe (~700 MB) — includes CuPy + CUDA runtime
```
**Why not dynamic loading?**
PyInstaller bundles everything at build time. CuPy can't be pip-installed
into a frozen exe at runtime. Options are:
1. **Bundle CuPy in PyInstaller** ← cleanest, what we'll do
2. Side-load CuPy DLLs (fragile, version-sensitive)
3. Hybrid: unfrozen Python + CuPy installed separately (defeats purpose of exe)
---
## Task 1: PyInstaller Spec with CuPy (Priority 1 — 30 min)
### File: `installer/rfcp-server-gpu.spec`
Create a separate .spec file that includes CuPy:
```python
# rfcp-server-gpu.spec — GPU-enabled build
import os
import sys
from PyInstaller.utils.hooks import collect_all, collect_dynamic_libs
backend_path = os.path.abspath(os.path.join(os.path.dirname(SPEC), '..', 'backend'))
# Collect CuPy and its CUDA dependencies
cupy_datas, cupy_binaries, cupy_hiddenimports = collect_all('cupy')
# Also collect cupy_backends
cupyb_datas, cupyb_binaries, cupyb_hiddenimports = collect_all('cupy_backends')
# CUDA runtime libraries that CuPy needs
cuda_binaries = collect_dynamic_libs('cupy')
a = Analysis(
[os.path.join(backend_path, 'run_server.py')],
pathex=[backend_path],
binaries=cupy_binaries + cupyb_binaries + cuda_binaries,
datas=[
(os.path.join(backend_path, 'data', 'terrain'), 'data/terrain'),
] + cupy_datas + cupyb_datas,
hiddenimports=[
# Existing imports from rfcp-server.spec
'uvicorn.logging',
'uvicorn.loops',
'uvicorn.loops.auto',
'uvicorn.protocols',
'uvicorn.protocols.http',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto',
'uvicorn.lifespan',
'uvicorn.lifespan.on',
'motor',
'pymongo',
'numpy',
'scipy',
'shapely',
'shapely.geometry',
'shapely.ops',
# CuPy-specific
'cupy',
'cupy.cuda',
'cupy.cuda.runtime',
'cupy.cuda.driver',
'cupy.cuda.memory',
'cupy.cuda.stream',
'cupy._core',
'cupy._core.core',
'cupy._core._routines_math',
'cupy.fft',
'cupy.linalg',
'fastrlock',
] + cupy_hiddenimports + cupyb_hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='rfcp-server',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False, # Don't compress CUDA libs — they need fast loading
console=True,
icon=os.path.join(os.path.dirname(SPEC), 'rfcp.ico'),
)
```
### Key Points:
- `collect_all('cupy')` grabs all CuPy submodules + CUDA DLLs
- `fastrlock` is a CuPy dependency (must be in hiddenimports)
- `upx=False` — don't compress CUDA binaries (breaks them)
- One-file mode (`a.binaries + a.datas` in EXE) for single exe
---
## Task 2: Build Script for GPU Variant (Priority 1 — 15 min)
### File: `installer/build-gpu.bat` (Windows)
```batch
@echo off
echo ========================================
echo RFCP GPU Build — rfcp-server-gpu.exe
echo ========================================
REM Ensure CuPy is installed in build environment
echo Checking CuPy installation...
python -c "import cupy; print(f'CuPy {cupy.__version__} with CUDA {cupy.cuda.runtime.runtimeGetVersion()}')"
if errorlevel 1 (
echo ERROR: CuPy not installed. Run: pip install cupy-cuda12x
exit /b 1
)
REM Build with GPU spec
echo Building rfcp-server with GPU support...
cd /d %~dp0\..\backend
pyinstaller ..\installer\rfcp-server-gpu.spec --clean --noconfirm
echo.
echo Build complete! Output: dist\rfcp-server.exe
echo Size:
dir dist\rfcp-server.exe
REM Optional: copy to Electron resources
if exist "..\desktop\resources" (
copy /y dist\rfcp-server.exe ..\desktop\resources\rfcp-server.exe
echo Copied to desktop\resources\
)
pause
```
### File: `installer/build-gpu.sh` (WSL/Linux)
```bash
#!/bin/bash
set -e
echo "========================================"
echo " RFCP GPU Build — rfcp-server (GPU)"
echo "========================================"
# Check CuPy
python3 -c "import cupy; print(f'CuPy {cupy.__version__}')" 2>/dev/null || {
echo "ERROR: CuPy not installed. Run: pip install cupy-cuda12x"
exit 1
}
cd "$(dirname "$0")/../backend"
pyinstaller ../installer/rfcp-server-gpu.spec --clean --noconfirm
echo ""
echo "Build complete!"
ls -lh dist/rfcp-server*
```
---
## Task 3: GPU Backend — Graceful CuPy Detection (Priority 1 — 15 min)
### File: `backend/app/services/gpu_backend.py`
The existing gpu_backend.py should already handle CuPy absence gracefully.
Verify and fix if needed:
```python
# gpu_backend.py — must work in BOTH CPU and GPU builds
import numpy as np
# Try importing CuPy — this is the key detection
_cupy_available = False
_gpu_device_name = None
_gpu_memory_mb = 0
try:
import cupy as cp
# Verify we can actually use it (not just import)
device = cp.cuda.Device(0)
_gpu_device_name = device.attributes.get('name', f'CUDA Device {device.id}')
# Try to get name via runtime
try:
props = cp.cuda.runtime.getDeviceProperties(0)
_gpu_device_name = props.get('name', _gpu_device_name)
if isinstance(_gpu_device_name, bytes):
_gpu_device_name = _gpu_device_name.decode('utf-8').strip('\x00')
except Exception:
pass
_gpu_memory_mb = device.mem_info[1] // (1024 * 1024)
_cupy_available = True
except ImportError:
cp = None # CuPy not installed (CPU build)
except Exception as e:
cp = None # CuPy installed but CUDA not available
print(f"[GPU] CuPy found but CUDA unavailable: {e}")
def is_gpu_available() -> bool:
return _cupy_available
def get_gpu_info() -> dict:
if _cupy_available:
return {
"available": True,
"backend": "CuPy (CUDA)",
"device": _gpu_device_name,
"memory_mb": _gpu_memory_mb,
}
return {
"available": False,
"backend": "NumPy (CPU)",
"device": "CPU",
"memory_mb": 0,
}
def get_array_module():
"""Return cupy if available, otherwise numpy."""
if _cupy_available:
return cp
return np
```
### Usage in coverage_service.py:
```python
from app.services.gpu_backend import get_array_module, is_gpu_available
xp = get_array_module() # cupy or numpy — same API
# All calculations use xp instead of np:
distances = xp.sqrt(dx**2 + dy**2)
path_loss = 20 * xp.log10(distances) + 20 * xp.log10(freq_mhz) - 27.55
# If using cupy, results need to come back to CPU for JSON serialization:
if is_gpu_available():
results = xp.asnumpy(path_loss)
else:
results = path_loss
```
---
## Task 4: GPU Status in Frontend Header (Priority 2 — 10 min)
### Update GPUIndicator.tsx
When GPU is detected, the badge should clearly show it:
```
CPU build: [⚙ CPU] (gray badge)
GPU detected: [⚡ RTX 4060] (green badge)
```
The existing GPUIndicator already does this. Just verify:
1. Badge color changes from gray → green when GPU available
2. Dropdown shows "Active: GPU (CUDA)" not just "CPU (NumPy)"
3. No install hints shown when CuPy IS available
---
## Task 5: Build Environment Setup (Priority 1 — Manual by Олег)
### Prerequisites for GPU build:
```powershell
# 1. Install CuPy in Windows Python (NOT WSL)
pip install cupy-cuda12x
# 2. Verify CuPy works
python -c "import cupy; print(cupy.cuda.runtime.runtimeGetVersion())"
# Should print: 12000 or similar
# 3. Install PyInstaller if not present
pip install pyinstaller
# 4. Verify fastrlock (CuPy dependency)
pip install fastrlock
```
### Build commands:
```powershell
# CPU-only build (existing)
cd D:\root\rfcp\backend
pyinstaller ..\installer\rfcp-server.spec --clean --noconfirm
# GPU build (new)
cd D:\root\rfcp\backend
pyinstaller ..\installer\rfcp-server-gpu.spec --clean --noconfirm
```
### Expected output sizes:
```
rfcp-server.exe (CPU): ~80 MB
rfcp-server.exe (GPU): ~600-800 MB (CuPy bundles CUDA runtime libs)
```
---
## Task 6: Electron — Detect Build Variant (Priority 2 — 10 min)
### File: `desktop/main.js` or `desktop/src/main.ts`
Add version detection so UI knows which build it's running:
```javascript
// After backend starts, check GPU status
async function checkBackendCapabilities() {
try {
const response = await fetch('http://127.0.0.1:8090/api/gpu/status');
const data = await response.json();
// Send to renderer
mainWindow.webContents.send('gpu-status', data);
if (data.available) {
console.log(`[RFCP] GPU: ${data.device} (${data.memory_mb} MB)`);
} else {
console.log('[RFCP] Running in CPU mode');
}
} catch (e) {
console.log('[RFCP] Backend not ready for GPU check');
}
}
```
---
## Task 7: About / Version Info (Priority 3 — 5 min)
### Add build info to `/api/health` response:
```python
@app.get("/api/health")
async def health():
gpu_info = get_gpu_info()
return {
"status": "ok",
"version": "3.6.0",
"build": "gpu" if gpu_info["available"] else "cpu",
"gpu": gpu_info,
"python": sys.version,
"platform": platform.platform(),
}
```
---
## Build & Test Procedure
### Step 1: Setup Build Environment
```powershell
# Windows PowerShell (NOT WSL)
cd D:\root\rfcp
# Verify Python environment
python --version # Should be 3.11.x
pip list | findstr cupy # Should show cupy-cuda12x
# If CuPy not installed:
pip install cupy-cuda12x fastrlock
```
### Step 2: Build GPU Variant
```powershell
cd D:\root\rfcp\backend
pyinstaller ..\installer\rfcp-server-gpu.spec --clean --noconfirm
```
### Step 3: Test Standalone
```powershell
# Run the built exe directly
.\dist\rfcp-server.exe
# In another terminal:
curl http://localhost:8090/api/health
curl http://localhost:8090/api/gpu/status
curl http://localhost:8090/api/gpu/diagnostics
```
### Step 4: Verify GPU Detection
Expected `/api/gpu/status` response:
```json
{
"available": true,
"backend": "CuPy (CUDA)",
"device": "NVIDIA GeForce RTX 4060 Laptop GPU",
"memory_mb": 8188
}
```
### Step 5: Run Coverage Calculation
- Place a site on map
- Calculate coverage (10km, 200m resolution)
- Check logs for: `[GPU] Using CUDA: RTX 4060 (8188 MB)`
- Compare performance: should be 5-10x faster than CPU
### Step 6: Full Electron Build
```powershell
# Copy GPU server to Electron resources
copy backend\dist\rfcp-server.exe desktop\resources\
# Build Electron installer
cd installer
.\build-win.sh # or equivalent Windows script
```
---
## Risk Assessment
### Size Concern
CuPy bundles CUDA runtime (~500MB). Total GPU installer ~700-800MB.
**Mitigation:** This is acceptable for a professional RF planning tool.
AutoCAD is 7GB. QGIS is 1.5GB. Atoll is 3GB+.
### CUDA Version Compatibility
CuPy-cuda12x requires CUDA 12.x compatible driver.
RTX 4060 with Driver 581.42 → CUDA 13.0 → backward compatible ✅
**Mitigation:** gpu_backend.py already falls back to NumPy gracefully.
### PyInstaller + CuPy Issues
Known issues:
- CuPy uses many .so/.dll files that PyInstaller might miss
- `collect_all('cupy')` should catch them, but test thoroughly
- If missing DLLs → add them manually to `binaries` list
**Mitigation:** Test the standalone exe on a clean machine (no Python installed).
### Antivirus False Positives
Larger exe = more AV suspicion. PyInstaller exes already trigger some AV.
**Mitigation:** Code-sign the exe (future task), submit to AV vendors for whitelisting.
---
## Success Criteria
- [ ] `rfcp-server-gpu.spec` created and builds successfully
- [ ] Built exe detects RTX 4060 on startup
- [ ] `/api/gpu/status` returns `"available": true`
- [ ] Coverage calculation uses CuPy (check logs)
- [ ] GPU badge shows "⚡ RTX 4060" (green) in header
- [ ] Fallback to NumPy works if CUDA unavailable
- [ ] CPU-only spec (`rfcp-server.spec`) still builds and works
- [ ] Build time < 10 minutes
- [ ] GPU exe size < 1 GB
---
## Commit Message
```
feat(build): add GPU-enabled PyInstaller build with CuPy + CUDA
- New rfcp-server-gpu.spec with CuPy/CUDA collection
- Build scripts: build-gpu.bat, build-gpu.sh
- Graceful GPU detection in gpu_backend.py
- Two-tier build: CPU (~80MB) and GPU (~700MB) variants
- Auto-detection: RTX 4060 → CuPy acceleration
- Fallback: no CUDA → NumPy (CPU mode)
Iteration 3.6.0 — Production GPU Build
```
---
## Files Summary
### New Files:
| File | Purpose |
|------|---------|
| `installer/rfcp-server-gpu.spec` | PyInstaller config with CuPy |
| `installer/build-gpu.bat` | Windows GPU build script |
| `installer/build-gpu.sh` | Linux/WSL GPU build script |
### Modified Files:
| File | Changes |
|------|---------|
| `backend/app/services/gpu_backend.py` | Verify graceful detection |
| `backend/app/main.py` | Health endpoint with build info |
| `desktop/main.js` or `main.ts` | GPU status check after backend start |
| `frontend/src/components/ui/GPUIndicator.tsx` | Verify badge shows GPU |
### No Changes Needed:
| File | Reason |
|------|--------|
| `installer/rfcp-server.spec` | CPU build stays as-is |
| `backend/app/services/coverage_service.py` | Already uses get_array_module() |
| `installer/build-win.sh` | Existing CPU build unchanged |
---
## Timeline
| Phase | Task | Time |
|-------|------|------|
| **P1** | Create rfcp-server-gpu.spec | 30 min |
| **P1** | Build scripts | 15 min |
| **P1** | Verify gpu_backend.py | 15 min |
| **P2** | Frontend badge verification | 10 min |
| **P2** | Electron GPU status | 10 min |
| **P3** | Health endpoint update | 5 min |
| **Test** | Build + test standalone | 20 min |
| **Test** | Full Electron build | 15 min |
| | **Total** | **~2 hours** |
**Claude Code estimated time: 10-15 min** (spec + scripts + backend changes)
**Manual testing by Олег: 30-45 min** (building + verifying)

352
UMTC-Wiki-MEGA-TASK.md Normal file
View File

@@ -0,0 +1,352 @@
# UMTC Wiki v2.0 — MEGA TASK: Integration & Polish
Read UMTC-Wiki-v2.0-REFACTOR.md and UMTC-Wiki-v2.0-ROADMAP.md for full context.
This is a comprehensive task covering all remaining fixes and integration work.
Take your time, think hard, work through each section systematically.
Report after completing each major section.
---
## SECTION A: Fix Critical Tauri 404 Bug
The sidebar loads the full content tree correctly but clicking ANY article shows 404.
### Debug steps:
1. In `frontend/src/lib/api.ts` — find where getPage is called with a slug
Add `console.log('[WIKI] getPage called with slug:', slug)`
2. In `frontend/src/lib/utils/backend.ts` — in the tauriGetPage function
Add `console.log('[WIKI] Tauri invoke get_page with:', slug)`
3. In `desktop/src-tauri/src/commands/content.rs` — in the get_page handler
Add `eprintln!("[WIKI] get_page received slug: {}", slug)`
Add `eprintln!("[WIKI] trying path: {:?}", resolved_path)`
4. Check the Sidebar.svelte component — what href/slug does it generate when user clicks?
The web version uses `/api/pages/{slug}` — in desktop mode it should invoke with just the slug part.
5. Common mismatches to check:
- Leading slash: sidebar sends `/lte/bbu` but Rust expects `lte/bbu`
- File extension: Rust looks for `lte/bbu.md` but file is `lte/bbu/index.md`
- URL encoding: Ukrainian characters in slugs
- The SvelteKit catch-all route `[...slug]` may pass the slug differently
6. Fix the mismatch. Test navigation to at least 10 different pages including:
- Root sections (lte/, ran/, mikrotik/)
- Nested pages (lte/bbu, ran/srsenb-config)
- Glossary terms (glossary/prb)
- Deep nesting if any
---
## SECTION B: Fix Web Deployment
The web version must keep working. Test and fix:
1. Check that `backend/content.py` imports work:
- `from wiki_frontmatter import ArticleFrontmatter`
- `from wikilinks import WikiLinksExtension`
- `from backlinks import BacklinksIndex`
- `from admonitions import AdmonitionsExtension`
If any import fails, fix the module.
2. Add the admonitions extension to the markdown pipeline in content.py
(wikilinks was already integrated, verify admonitions too)
3. Make sure the backlinks API endpoint in main.py works:
- GET /api/pages/{slug:path}/backlinks
- Should return { "slug": "...", "backlinks": [...], "count": N }
4. Add grade/status/category to the page API response:
- GET /api/pages/{slug} should now include grade, status, category fields
5. Create a simple test script `scripts/test_web.py`:
```python
# Test that backend starts and key endpoints work
import requests
BASE = "http://localhost:8000"
# Test navigation
r = requests.get(f"{BASE}/api/navigation")
assert r.status_code == 200
nav = r.json()
print(f"Navigation: {len(nav)} sections")
# Test page load
r = requests.get(f"{BASE}/api/pages/index")
assert r.status_code == 200
print(f"Home page: {r.json().get('title', 'OK')}")
# Test search
r = requests.get(f"{BASE}/api/search?q=LTE")
assert r.status_code == 200
print(f"Search 'LTE': {len(r.json())} results")
# Test backlinks
r = requests.get(f"{BASE}/api/pages/glossary/enb/backlinks")
print(f"Backlinks for eNB: {r.status_code}")
print("\nAll tests passed!")
```
---
## SECTION C: Frontend Wiki Components — Full Integration
### C.1: Article Grade Badge on Pages
In the wiki page view (`frontend/src/routes/[...slug]/+page.svelte` or equivalent):
- Import ArticleGrade component
- Display the grade badge next to the page title
- The grade comes from the page API response (field: `grade`)
- If no grade, don't show badge
- Style: small badge inline with title, not a separate block
### C.2: Breadcrumbs Component
Create/update `frontend/src/lib/components/wiki/Breadcrumbs.svelte`:
```svelte
<!-- Example: Головна > LTE > BBU Setup -->
<nav class="breadcrumbs">
<a href="/">Головна</a>
<span class="separator">/</span>
<a href="/lte">LTE</a>
<span class="separator">/</span>
<span class="current">BBU Setup</span>
</nav>
```
- Generate from current page slug
- Each segment is a link except the last
- Use titles from navigation tree if available, otherwise humanize slug
- Works in both web and desktop mode
- Integrate into the page layout — show above article title
### C.3: Admonition CSS
Add styles for admonition boxes to the global CSS or a component:
```css
.admonition {
border-left: 4px solid;
border-radius: 4px;
padding: 12px 16px;
margin: 16px 0;
}
.admonition-note { border-color: #3b82f6; background: rgba(59,130,246,0.1); }
.admonition-warning { border-color: #f59e0b; background: rgba(245,158,11,0.1); }
.admonition-tip { border-color: #10b981; background: rgba(16,185,129,0.1); }
.admonition-danger { border-color: #ef4444; background: rgba(239,68,68,0.1); }
/* Dark mode */
:global(.dark) .admonition-note { background: rgba(59,130,246,0.15); }
:global(.dark) .admonition-warning { background: rgba(245,158,11,0.15); }
:global(.dark) .admonition-tip { background: rgba(16,185,129,0.15); }
:global(.dark) .admonition-danger { background: rgba(239,68,68,0.15); }
.admonition-title {
font-weight: 600;
margin-bottom: 4px;
}
.admonition-icon {
margin-right: 8px;
}
```
### C.4: Wiki-Link CSS
Add styles for wiki-links:
```css
.wiki-link {
color: #3b82f6;
text-decoration: none;
border-bottom: 1px dotted #3b82f6;
}
.wiki-link:hover {
border-bottom-style: solid;
}
.red-link {
color: #ef4444;
border-bottom-color: #ef4444;
}
.red-link:hover::after {
content: " (сторінку не знайдено)";
font-size: 0.75em;
color: #9ca3af;
}
```
### C.5: Backlinks Panel Integration
In the page view, after the article content:
- Show BacklinksPanel component
- Pass current page slug
- Works in both web (API) and desktop (Tauri IPC)
- Only show if there are backlinks (don't show empty panel)
### C.6: Table of Contents (sidebar)
If the page has headings, generate a table of contents:
- Extract h2/h3 from rendered HTML or use TOC data from backend
- Show as a floating sidebar on wide screens (>1200px)
- Collapsible on smaller screens
- Highlight current section on scroll (intersection observer)
- Works in both modes
---
## SECTION D: Search Integration for Desktop
1. Test Tantivy search in Tauri:
- The search command should be wired to the Search component
- Type in search bar → results appear
- Cyrillic text should work (test: "мережа", "антена", "LTE")
2. If search doesn't work, debug:
- Is the search index built on startup? Check Rust logs
- Are content files found? Check content path resolution
- Is the query reaching the search command?
3. Search results should show:
- Page title
- Brief excerpt (first 150 chars of content)
- Click navigates to page
4. Keyboard shortcut: Ctrl+K should focus the search bar (already exists in web, verify in desktop)
---
## SECTION E: Content Quality Pass
### E.1: Content Audit Script
Create `scripts/analyze_content.py`:
- Scan all .md files in content/
- For each file report: has_frontmatter, word_count, has_code_blocks, grade, broken_wiki_links
- Summary: total articles, by grade, articles needing work
- Print actionable output
### E.2: Add More Glossary Terms (20 more)
Create glossary entries with proper frontmatter (grade: B, category: glossary):
**Radio/RF terms:**
- SGW (Serving Gateway)
- PGW (PDN Gateway)
- HSS (Home Subscriber Server)
- RSRP (Reference Signal Received Power)
- RSRQ (Reference Signal Received Quality)
- SINR (Signal to Interference plus Noise Ratio)
- EARFCN (E-UTRA Absolute Radio Frequency Channel Number)
- OFDM (Orthogonal Frequency Division Multiplexing)
- MIMO (Multiple Input Multiple Output)
- QoS (Quality of Service)
**Infrastructure terms:**
- WireGuard
- MikroTik
- Mesh Network
- VLAN (Virtual LAN)
- BGP (Border Gateway Protocol)
- mTLS (Mutual TLS)
- Caddy (Web Server)
**Protocol terms:**
- S1AP (S1 Application Protocol)
- GTP (GPRS Tunnelling Protocol)
- SCTP (Stream Control Transmission Protocol)
Each glossary term should:
- Have title in English with Ukrainian description
- Use [[wiki-links]] to cross-reference other terms
- Include: what it is, why it matters for UMTC, key parameters
- Be 100-300 words
### E.3: Upgrade 5 Key Articles to Grade B
Pick the 5 most important articles and upgrade them:
- Add proper frontmatter with all fields
- Add :::note and :::warning admonitions where useful
- Add [[wiki-links]] to glossary terms
- Add "Див. також" (See also) section with related articles
- Verify technical accuracy
- Set grade: B
Good candidates:
- Main LTE overview
- srsENB configuration
- WireGuard setup
- Open5GS overview
- MikroTik basics
---
## SECTION F: Desktop App Polish
### F.1: Window Title
Show current page title in the window title bar:
`UMTC Wiki — {Page Title}`
### F.2: Keyboard Navigation
- Arrow keys in sidebar to navigate
- Enter to open selected item
- Backspace to go back
- Ctrl+K for search (verify)
### F.3: Error Handling
- If page not found, show a friendly Ukrainian message instead of generic 404
- If content directory is missing, show setup instructions
- If search index fails to build, log error but don't crash
### F.4: About Dialog
Add a simple about/info accessible from a gear icon or Help menu:
- UMTC Wiki v2.0
- Built with Tauri + SvelteKit + Rust
- Content articles count
- "Офлайн документація для УМТЗ"
---
## SECTION G: Production Builds
### G.1: Web Docker Build Test
Update docker-compose.yml if needed to include new backend modules.
Make sure Dockerfile copies:
- backend/wiki_frontmatter.py
- backend/wikilinks.py
- backend/backlinks.py
- backend/admonitions.py
- All content/ files
### G.2: Tauri Production Build
Run `npx tauri build` and fix any remaining compilation errors.
Report the output binary size and location.
---
## Order of Operations
Do these in order — each section builds on the previous:
1. **SECTION A** — Fix 404 bug (CRITICAL, everything depends on this)
2. **SECTION B** — Verify web backend
3. **SECTION C** — Frontend components
4. **SECTION D** — Search
5. **SECTION E** — Content
6. **SECTION F** — Desktop polish
7. **SECTION G** — Production builds
Report after each section with:
- What was done
- What files were changed
- Any issues found
- Ready for next section?
Think hard about edge cases. Don't break existing functionality.
Good luck! 🚀

View File

@@ -14,6 +14,7 @@ from app.services.coverage_service import (
select_propagation_model,
)
from app.services.parallel_coverage_service import CancellationToken
from app.services.boundary_service import calculate_coverage_boundary
router = APIRouter()
@@ -24,6 +25,12 @@ class CoverageRequest(BaseModel):
settings: CoverageSettings = CoverageSettings()
class BoundaryPoint(BaseModel):
"""Single boundary coordinate"""
lat: float
lon: float
class CoverageResponse(BaseModel):
"""Coverage calculation response"""
points: List[CoveragePoint]
@@ -32,6 +39,7 @@ class CoverageResponse(BaseModel):
stats: dict
computation_time: float # seconds
models_used: List[str] # which models were active
boundary: Optional[List[BoundaryPoint]] = None # coverage boundary polygon
@router.post("/calculate")
@@ -131,13 +139,24 @@ async def calculate_coverage(request: CoverageRequest) -> CoverageResponse:
"points_with_atmospheric_loss": sum(1 for p in points if p.atmospheric_loss > 0),
}
# Calculate coverage boundary
boundary = None
if points:
boundary_coords = calculate_coverage_boundary(
[p.model_dump() for p in points],
threshold_dbm=request.settings.min_signal,
)
if boundary_coords:
boundary = [BoundaryPoint(**c) for c in boundary_coords]
return CoverageResponse(
points=points,
count=len(points),
settings=effective_settings,
stats=stats,
computation_time=round(computation_time, 2),
models_used=models_used
models_used=models_used,
boundary=boundary,
)

View File

@@ -1,12 +1,29 @@
import sys
import platform
from fastapi import APIRouter, Depends
from app.api.deps import get_db
from app.services.gpu_backend import gpu_manager
router = APIRouter()
@router.get("/")
async def health_check():
return {"status": "ok", "service": "rfcp-backend", "version": "1.1.0"}
gpu_info = gpu_manager.get_status()
return {
"status": "ok",
"service": "rfcp-backend",
"version": "3.6.0",
"build": "gpu" if gpu_info.get("gpu_available") else "cpu",
"gpu": {
"available": gpu_info.get("gpu_available", False),
"backend": gpu_info.get("active_backend", "cpu"),
"device": gpu_info.get("active_device", {}).get("name") if gpu_info.get("active_device") else "CPU",
},
"python": sys.version.split()[0],
"platform": platform.system(),
}
@router.get("/db")

View File

@@ -1,4 +1,6 @@
from contextlib import asynccontextmanager
from contextlib import asynccontextmanager
import logging
import platform
from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
@@ -7,9 +9,54 @@ from app.core.database import connect_to_mongo, close_mongo_connection
from app.api.routes import health, projects, terrain, coverage, regions, system, gpu
from app.api.websocket import websocket_endpoint
logger = logging.getLogger("rfcp.startup")
def check_gpu_availability():
"""Log GPU status on startup for debugging."""
is_wsl = "microsoft" in platform.release().lower()
env_note = " (WSL2)" if is_wsl else ""
# Check CuPy / CUDA
try:
import cupy as cp
device_count = cp.cuda.runtime.getDeviceCount()
if device_count > 0:
props = cp.cuda.runtime.getDeviceProperties(0)
name = props["name"]
if isinstance(name, bytes):
name = name.decode()
mem_mb = props["totalGlobalMem"] // (1024 * 1024)
logger.info(f"GPU detected{env_note}: {name} ({mem_mb} MB VRAM)")
logger.info(f"CuPy {cp.__version__}, CUDA devices: {device_count}")
else:
logger.warning(f"CuPy installed but no CUDA devices found{env_note}")
except Exception as e:
logger.warning(f"CuPy FAILED {env_note}: {e}")
if is_wsl:
logger.warning("Install: pip3 install cupy-cuda12x --break-system-packages")
else:
logger.warning("Install: pip install cupy-cuda12x")
except Exception as e:
logger.warning(f"CuPy error{env_note}: {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 Exception as e:
logger.debug("PyOpenCL not installed (optional)")
except Exception:
pass
@asynccontextmanager
async def lifespan(app: FastAPI):
# Log GPU status on startup
check_gpu_availability()
await connect_to_mongo()
yield
await close_mongo_connection()

View File

@@ -0,0 +1,122 @@
"""
Coverage boundary calculation service.
Computes concave hull (alpha shape) from coverage points to generate
a realistic boundary that follows actual coverage contour.
"""
import logging
from typing import Optional
logger = logging.getLogger(__name__)
def calculate_coverage_boundary(
points: list[dict],
threshold_dbm: float = -100,
simplify_tolerance: float = 0.001,
) -> list[dict]:
"""
Calculate coverage boundary as concave hull of points above threshold.
Args:
points: List of coverage points with 'lat', 'lon', 'rsrp' keys
threshold_dbm: RSRP threshold - points below this are excluded
simplify_tolerance: Simplification tolerance in degrees (~100m per 0.001)
Returns:
List of {'lat': float, 'lon': float} coordinates forming boundary polygon.
Empty list if boundary cannot be computed.
"""
try:
from shapely.geometry import MultiPoint
from shapely import concave_hull
except ImportError:
logger.warning("Shapely not installed - boundary calculation disabled")
return []
# Filter points above threshold
valid_coords = [
(p['lon'], p['lat']) # Shapely uses (x, y) = (lon, lat)
for p in points
if p.get('rsrp', -999) >= threshold_dbm
]
if len(valid_coords) < 3:
logger.debug(f"Not enough points for boundary: {len(valid_coords)}")
return []
try:
# Create MultiPoint geometry
mp = MultiPoint(valid_coords)
# Compute concave hull (alpha shape)
# ratio: 0 = convex hull, 1 = very tight fit
# 0.3-0.5 gives good balance between detail and smoothness
hull = concave_hull(mp, ratio=0.3)
if hull.is_empty:
logger.debug("Concave hull is empty")
return []
# Simplify to reduce points (0.001 deg ≈ 100m)
if simplify_tolerance > 0:
hull = hull.simplify(simplify_tolerance, preserve_topology=True)
# Extract coordinates based on geometry type
if hull.geom_type == 'Polygon':
coords = list(hull.exterior.coords)
return [{'lat': c[1], 'lon': c[0]} for c in coords]
elif hull.geom_type == 'MultiPolygon':
# Return largest polygon's exterior
largest = max(hull.geoms, key=lambda g: g.area)
coords = list(largest.exterior.coords)
return [{'lat': c[1], 'lon': c[0]} for c in coords]
elif hull.geom_type == 'GeometryCollection':
# Find polygons in collection
polygons = [g for g in hull.geoms if g.geom_type == 'Polygon']
if polygons:
largest = max(polygons, key=lambda g: g.area)
coords = list(largest.exterior.coords)
return [{'lat': c[1], 'lon': c[0]} for c in coords]
logger.debug(f"Unexpected hull geometry type: {hull.geom_type}")
return []
except Exception as e:
logger.warning(f"Boundary calculation error: {e}")
return []
def calculate_multi_site_boundaries(
points: list[dict],
threshold_dbm: float = -100,
) -> dict[str, list[dict]]:
"""
Calculate separate boundaries for each site's coverage area.
Args:
points: Coverage points with 'lat', 'lon', 'rsrp', 'site_id' keys
threshold_dbm: RSRP threshold
Returns:
Dict mapping site_id to boundary coordinates list.
"""
# Group points by site_id
by_site: dict[str, list[dict]] = {}
for p in points:
site_id = p.get('site_id', 'default')
if site_id not in by_site:
by_site[site_id] = []
by_site[site_id].append(p)
# Calculate boundary for each site
boundaries = {}
for site_id, site_points in by_site.items():
boundary = calculate_coverage_boundary(site_points, threshold_dbm)
if boundary:
boundaries[site_id] = boundary
return boundaries

View File

@@ -171,17 +171,34 @@ class GPUManager:
"""Full diagnostic info for troubleshooting GPU detection."""
import sys
import platform
import subprocess
is_wsl = "microsoft" in platform.release().lower()
diag = {
"python_version": sys.version,
"python_executable": sys.executable,
"platform": platform.platform(),
"is_wsl": is_wsl,
"numpy": {"version": np.__version__},
"cuda": {},
"opencl": {},
"nvidia_smi": None,
"detected_devices": len(self._devices),
"active_backend": self._active_backend.value,
}
# Check nvidia-smi (works even without CuPy)
try:
result = subprocess.run(
["nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader"],
capture_output=True, text=True, timeout=5
)
if result.returncode == 0 and result.stdout.strip():
diag["nvidia_smi"] = result.stdout.strip()
except Exception:
diag["nvidia_smi"] = "not found or error"
# Check CuPy/CUDA
try:
import cupy as cp
@@ -200,7 +217,10 @@ class GPUManager:
}
except ImportError:
diag["cuda"]["error"] = "CuPy not installed"
diag["cuda"]["install_hint"] = "pip install cupy-cuda12x"
if is_wsl:
diag["cuda"]["install_hint"] = "pip3 install cupy-cuda12x --break-system-packages"
else:
diag["cuda"]["install_hint"] = "pip install cupy-cuda12x"
except Exception as e:
diag["cuda"]["error"] = str(e)
@@ -221,7 +241,10 @@ class GPUManager:
diag["opencl"]["platforms"].append(platform_info)
except ImportError:
diag["opencl"]["error"] = "PyOpenCL not installed"
diag["opencl"]["install_hint"] = "pip install pyopencl"
if is_wsl:
diag["opencl"]["install_hint"] = "pip3 install pyopencl --break-system-packages"
else:
diag["opencl"]["install_hint"] = "pip install pyopencl"
except Exception as e:
diag["opencl"]["error"] = str(e)

View File

@@ -7,6 +7,7 @@ pymongo==4.6.1
pydantic-settings==2.1.0
numpy==1.26.4
scipy==1.12.0
shapely>=2.0.0
requests==2.31.0
httpx==0.27.0
aiosqlite>=0.19.0

View File

@@ -52,9 +52,11 @@ const getLogPath = () => {
const getBackendExePath = () => {
const exeName = process.platform === 'win32' ? 'rfcp-server.exe' : 'rfcp-server';
if (isDev) {
return path.join(__dirname, '..', 'backend', exeName);
// Dev: use the ONEDIR build output
return path.join(__dirname, '..', 'backend', 'dist', 'rfcp-server', exeName);
}
return getResourcePath('backend', exeName);
// Production: ONEDIR structure - backend/rfcp-server/rfcp-server.exe
return getResourcePath('backend', 'rfcp-server', exeName);
};
/** Frontend index.html path (production only) */

View File

@@ -0,0 +1,233 @@
# RFCP Native Backend Research
## Executive Summary
**Finding:** The production Electron app already supports native Windows operation without WSL2.
The production build uses PyInstaller to bundle the Python backend as a standalone Windows executable (`rfcp-server.exe`). WSL2 is only used during development. No migration is needed for end users.
---
## Current Architecture
### Development Mode
```
RFCP (Electron dev)
└── Spawns: python -m uvicorn app.main:app --host 127.0.0.1 --port 8090
└── Uses system Python (Windows or WSL2)
└── Requires venv with dependencies
```
### Production Mode (Already Implemented)
```
RFCP.exe (Electron packaged)
└── Spawns: rfcp-server.exe (bundled PyInstaller binary)
└── Self-contained Python + all dependencies
└── No WSL2 required
└── No system Python required
```
---
## Evidence from Codebase
### desktop/main.js (Lines 120-145)
```javascript
function startBackend() {
// Production: use bundled executable
if (isProduction) {
const serverPath = path.join(process.resourcesPath, 'rfcp-server.exe');
if (fs.existsSync(serverPath)) {
backendProcess = spawn(serverPath, [], { ... });
return;
}
}
// Development: use system Python
backendProcess = spawn('python', ['-m', 'uvicorn', 'app.main:app', ...]);
}
```
### installer/rfcp-server.spec (PyInstaller Config)
```python
# Key configuration
a = Analysis(
['run_server.py'],
pathex=[backend_path],
binaries=[],
datas=[
('data/terrain', 'data/terrain'), # Terrain data bundled
],
hiddenimports=[
'uvicorn.logging', 'uvicorn.loops', 'uvicorn.protocols',
'motor', 'pymongo', 'numpy', 'scipy', 'shapely',
# Full list of dependencies
],
)
exe = EXE(
pyz,
a.scripts,
name='rfcp-server',
console=True, # Shows console for debugging
icon='rfcp.ico',
)
```
---
## GPU Acceleration in Production
### Current Status
The PyInstaller bundle **does not include CuPy** by default because:
1. CuPy requires CUDA runtime (large, ~500MB)
2. Not all users have NVIDIA GPUs
3. Binary would be too large for distribution
### Solution Options
**Option A: Ship CPU-only (Current)**
- Production build uses NumPy (CPU) for calculations
- GPU acceleration available only in dev mode or manual install
- Smallest download size (~100MB)
**Option B: Separate GPU Installer**
- Main installer: CPU-only (~100MB)
- Optional GPU addon: Downloads CuPy + CUDA runtime (~600MB)
- Implemented via install_rfcp.py dependency installer
**Option C: CUDA Toolkit Detection**
- Detect if CUDA is already installed on user's system
- If yes, attempt to load CuPy dynamically
- Graceful fallback to NumPy if not available
### Recommendation
Keep Option A (CPU-only production) with Option B available for power users:
1. Default production build works everywhere
2. Users with NVIDIA GPUs can run `install_rfcp.py` to enable GPU acceleration
3. No WSL2 required for either path
---
## Terrain Data Handling
### Current Implementation
Terrain data (SRTM .hgt files) is bundled inside the PyInstaller executable:
```python
datas=[
('data/terrain', 'data/terrain'),
]
```
### Considerations
- Bundled terrain data increases exe size significantly
- Alternative: Download terrain on first use (like current region download system)
- For initial release, bundling common regions is acceptable
---
## Database (MongoDB)
### Production Architecture
The Electron app embeds MongoDB or requires MongoDB to be installed separately.
Options:
1. **Embedded MongoDB** - Ships mongod.exe with the app
2. **MongoDB Atlas** - Cloud database (requires internet)
3. **SQLite** - Switch to file-based database (significant refactor)
4. **In-memory + file persistence** - No MongoDB required (significant refactor)
Current implementation uses Motor (async MongoDB driver). For true standalone operation, consider SQLite migration in future iteration.
---
## Build Process
### Current Build Commands
```bash
# Build backend executable
cd /mnt/d/root/rfcp/backend
pyinstaller ../installer/rfcp-server.spec
# Build Electron app with bundled backend
cd /mnt/d/root/rfcp/installer
./build-win.sh
```
### Output
- `rfcp-server.exe` - Standalone backend (~80MB)
- `RFCP-Setup-{version}.exe` - Full installer with Electron + backend (~150MB)
---
## Testing Native Build
### Test Procedure
1. Build `rfcp-server.exe` via PyInstaller
2. Run directly: `./rfcp-server.exe`
3. Verify API responds: `curl http://localhost:8090/api/health`
4. Verify coverage calculation works
5. Check GPU detection in logs
### Known Issues
1. **First launch slow**: PyInstaller extracts on first run (~5-10 seconds)
2. **Antivirus false positives**: Some AV flags PyInstaller executables
3. **Console window**: Shows black console (use `console=False` for windowless)
---
## Conclusions
### No Migration Needed
The production Electron app already works without WSL2. The current architecture is:
- ✅ Native Windows executable
- ✅ No Python installation required
- ✅ No WSL2 required
- ✅ Self-contained dependencies
### Development vs Production
| Aspect | Development | Production |
|--------|-------------|------------|
| Python | System Python / venv | Bundled via PyInstaller |
| WSL2 | Optional (for testing) | Not required |
| GPU | CuPy if installed | CPU-only (NumPy) |
| MongoDB | Local instance | Embedded or Atlas |
| Terrain | Local data/ folder | Bundled in exe |
### Remaining Work
1. **GPU for production**: Implement Optional GPU addon installer
2. **Smaller package**: On-demand terrain download instead of bundling
3. **Database portability**: Consider SQLite migration for offline-first
4. **Installer polish**: Signed executables, auto-update support
---
## Appendix: Full PyInstaller Hidden Imports
From `installer/rfcp-server.spec`:
```python
hiddenimports=[
'uvicorn.logging',
'uvicorn.loops',
'uvicorn.loops.auto',
'uvicorn.protocols',
'uvicorn.protocols.http',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto',
'uvicorn.lifespan',
'uvicorn.lifespan.on',
'motor',
'pymongo',
'numpy',
'scipy',
'shapely',
'shapely.geometry',
'shapely.ops',
# ... additional imports
]
```

View File

@@ -444,11 +444,14 @@ export default function App() {
);
} else {
const timeStr = result.calculationTime.toFixed(1);
const firstSite = sites.find((s) => s.visible);
const freqStr = firstSite ? ` \u2022 ${firstSite.frequency} MHz` : '';
const presetStr = settings.preset ? ` \u2022 ${settings.preset}` : '';
const modelsStr = result.modelsUsed?.length
? ` ${result.modelsUsed.length} models`
? ` \u2022 ${result.modelsUsed.length} models`
: '';
addToast(
`Calculated ${result.totalPoints.toLocaleString()} points in ${timeStr}s${modelsStr}`,
`${result.totalPoints.toLocaleString()} pts \u2022 ${timeStr}s${presetStr}${freqStr}${modelsStr}`,
'success'
);
}
@@ -481,7 +484,7 @@ export default function App() {
return (
<div className="h-screen w-screen flex flex-col bg-gray-100 dark:bg-dark-bg">
{/* Header */}
<header className="bg-slate-800 dark:bg-slate-900 text-white px-4 py-2 flex items-center justify-between flex-shrink-0 z-10">
<header className="bg-slate-800 dark:bg-slate-900 text-white px-4 py-2 flex items-center justify-between flex-shrink-0 z-[1010]">
<div className="flex items-center gap-2">
<span className="text-base font-bold">RFCP</span>
<span className="text-xs text-slate-400 hidden sm:inline">
@@ -684,6 +687,7 @@ export default function App() {
points={coverageResult.points.filter(p => p.rsrp >= settings.rsrpThreshold)}
visible={showBoundary}
resolution={settings.resolution}
boundary={coverageResult.boundary}
/>
)}
</>

View File

@@ -1,8 +1,8 @@
/**
* Renders a dashed polyline around the coverage zone boundary.
*
* Uses @turf/concave to compute a concave hull (alpha shape) per site,
* which correctly follows sector/wedge shapes — not just convex circles.
* Prefers server-computed boundary if available (shapely concave_hull).
* Falls back to client-side @turf/concave computation.
*
* Performance: ~20-50ms for 10k points (runs once per coverage change).
*/
@@ -12,7 +12,7 @@ import { useMap } from 'react-leaflet';
import L from 'leaflet';
import concave from '@turf/concave';
import { featureCollection, point } from '@turf/helpers';
import type { CoveragePoint } from '@/types/index.ts';
import type { CoveragePoint, BoundaryPoint } from '@/types/index.ts';
import { logger } from '@/utils/logger.ts';
interface CoverageBoundaryProps {
@@ -21,6 +21,7 @@ interface CoverageBoundaryProps {
resolution: number; // meters — controls concave hull detail
color?: string;
weight?: number;
boundary?: BoundaryPoint[]; // server-provided boundary (preferred)
}
export default function CoverageBoundary({
@@ -29,13 +30,25 @@ export default function CoverageBoundary({
resolution,
color = '#ffffff', // white — visible against red-to-blue gradient
weight = 2,
boundary,
}: CoverageBoundaryProps) {
const map = useMap();
const layerRef = useRef<L.LayerGroup | null>(null);
// Compute boundary paths grouped by site
// Compute boundary paths - prefer server boundary, fallback to client-side
const boundaryPaths = useMemo(() => {
if (!visible || points.length === 0) return [];
if (!visible) return [];
// Use server-provided boundary if available
if (boundary && boundary.length >= 3) {
const serverPath: L.LatLngExpression[] = boundary.map(
(p) => [p.lat, p.lon] as L.LatLngExpression
);
return [serverPath];
}
// Fallback to client-side computation
if (points.length === 0) return [];
// Group points by siteId (fallback to 'all' when siteId not available from API)
const bySite = new Map<string, CoveragePoint[]>();
@@ -61,7 +74,7 @@ export default function CoverageBoundary({
}
return paths;
}, [points, visible, resolution]);
}, [points, visible, resolution, boundary]);
// Render / cleanup polylines
useEffect(() => {

View File

@@ -121,6 +121,7 @@ export default function MeasurementTool({ enabled, onComplete, onProfileRequest
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onProfileRequest(points[0], points[points.length - 1]);
}}
style={{

View File

@@ -33,6 +33,13 @@ export default function GPUIndicator() {
return () => document.removeEventListener('mousedown', handler);
}, [open]);
// Auto-fetch diagnostics when dropdown opens and only CPU available
useEffect(() => {
if (open && status?.active_backend === 'cpu' && !diagnostics) {
api.getGPUDiagnostics().then(setDiagnostics).catch(() => {});
}
}, [open, status?.active_backend, diagnostics]);
if (!status) return null;
const isGPU = status.active_backend !== 'cpu';
@@ -119,15 +126,30 @@ export default function GPUIndicator() {
<div className="text-[10px] text-yellow-600 dark:text-yellow-400 mb-2">
No GPU detected. For faster calculations:
</div>
<div className="text-[10px] text-gray-500 dark:text-dark-muted space-y-0.5">
<div>NVIDIA: <code className="bg-gray-100 dark:bg-dark-border px-1 rounded">pip install cupy-cuda12x</code></div>
<div>Intel/AMD: <code className="bg-gray-100 dark:bg-dark-border px-1 rounded">pip install pyopencl</code></div>
</div>
{diagnostics?.is_wsl ? (
<div className="text-[10px] text-gray-500 dark:text-dark-muted space-y-1">
<div className="text-[9px] text-gray-400 dark:text-dark-muted mb-1">WSL2 detected - use pip3:</div>
<div className="bg-gray-100 dark:bg-dark-border px-2 py-1 rounded font-mono text-[9px] break-all">
pip3 install cupy-cuda12x --break-system-packages
</div>
<div className="text-[9px] text-gray-400 dark:text-dark-muted mt-1">Then restart RFCP</div>
</div>
) : (
<div className="text-[10px] text-gray-500 dark:text-dark-muted space-y-0.5">
<div>NVIDIA: <code className="bg-gray-100 dark:bg-dark-border px-1 rounded">pip install cupy-cuda12x</code></div>
<div>Intel/AMD: <code className="bg-gray-100 dark:bg-dark-border px-1 rounded">pip install pyopencl</code></div>
</div>
)}
{typeof diagnostics?.nvidia_smi === 'string' && diagnostics.nvidia_smi !== 'not found or error' && (
<div className="mt-2 text-[9px] text-green-600 dark:text-green-400">
GPU hardware found: {diagnostics.nvidia_smi.split(',')[0]}
</div>
)}
<button
onClick={handleRunDiagnostics}
className="mt-2 w-full text-[10px] text-blue-600 dark:text-blue-400 hover:underline text-left"
>
Run Diagnostics
{diagnostics ? 'Refresh Diagnostics' : 'Run Diagnostics'}
</button>
</div>
)}

View File

@@ -75,6 +75,11 @@ export interface ApiCoverageStats {
points_with_atmospheric_loss: number;
}
export interface ApiBoundaryPoint {
lat: number;
lon: number;
}
export interface CoverageResponse {
points: ApiCoveragePoint[];
count: number;
@@ -82,6 +87,7 @@ export interface CoverageResponse {
stats: ApiCoverageStats;
computation_time: number;
models_used: string[];
boundary?: ApiBoundaryPoint[];
}
export interface Preset {

View File

@@ -98,6 +98,7 @@ function responseToResult(response: CoverageResponse, settings: CoverageSettings
settings: settings,
stats: response.stats as CoverageApiStats,
modelsUsed: response.models_used,
boundary: response.boundary,
};
}
@@ -251,11 +252,14 @@ export const useCoverageStore = create<CoverageState>((set, get) => ({
addToast('No coverage points. Try increasing radius.', 'warning');
} else {
const timeStr = result.calculationTime.toFixed(1);
const firstSite = useSitesStore.getState().sites.find((s) => s.visible);
const freqStr = firstSite ? ` \u2022 ${firstSite.frequency} MHz` : '';
const presetStr = settings.preset ? ` \u2022 ${settings.preset}` : '';
const modelsStr = result.modelsUsed?.length
? ` \u2022 ${result.modelsUsed.length} models`
: '';
addToast(
`Calculated ${result.totalPoints.toLocaleString()} points in ${timeStr}s${modelsStr}`,
`${result.totalPoints.toLocaleString()} pts \u2022 ${timeStr}s${presetStr}${freqStr}${modelsStr}`,
'success'
);
}

View File

@@ -15,6 +15,11 @@ export interface CoveragePoint {
atmospheric_loss?: number; // dB atmospheric absorption
}
export interface BoundaryPoint {
lat: number;
lon: number;
}
export interface CoverageResult {
points: CoveragePoint[];
calculationTime: number; // seconds (was ms for browser calc)
@@ -23,6 +28,7 @@ export interface CoverageResult {
// API-provided fields
stats?: CoverageApiStats;
modelsUsed?: string[];
boundary?: BoundaryPoint[]; // server-computed coverage boundary
}
export interface CoverageApiStats {

View File

@@ -5,5 +5,6 @@ export type {
CoverageSettings,
CoverageApiStats,
GridPoint,
BoundaryPoint,
} from './coverage.ts';
export type { FrequencyBand } from './frequency.ts';

70
installer/build-gpu.bat Normal file
View File

@@ -0,0 +1,70 @@
@echo off
echo ========================================
echo RFCP GPU Build — ONEDIR mode
echo CuPy-cuda13x + CUDA Toolkit 13.x
echo ========================================
echo.
REM ── Check CuPy ──
echo [1/5] Checking CuPy installation...
python -c "import cupy; print(f' CuPy {cupy.__version__}')" 2>nul
if errorlevel 1 (
echo ERROR: CuPy not installed.
echo Run: pip install cupy-cuda13x
exit /b 1
)
REM ── Check CUDA compute ──
echo [2/5] Testing GPU compute...
python -c "import cupy; a = cupy.array([1,2,3]); assert a.sum() == 6; print(' GPU compute: OK')" 2>nul
if errorlevel 1 (
echo ERROR: CuPy installed but GPU compute failed.
echo Check: CUDA Toolkit installed? nvidia-smi works?
exit /b 1
)
REM ── Check CUDA_PATH ──
echo [3/5] Checking CUDA Toolkit...
if defined CUDA_PATH (
echo CUDA_PATH: %CUDA_PATH%
) else (
echo WARNING: CUDA_PATH not set
)
REM ── Check nvidia pip DLLs ──
echo [4/5] Checking nvidia pip packages...
python -c "import nvidia; import os; base=os.path.dirname(nvidia.__file__); dlls=[f for d in os.listdir(base) if os.path.isdir(os.path.join(base,d,'bin')) for f in os.listdir(os.path.join(base,d,'bin')) if f.endswith('.dll')]; print(f' nvidia pip DLLs: {len(dlls)}')" 2>nul
if errorlevel 1 (
echo No nvidia pip packages (will use CUDA Toolkit)
)
REM ── Build ──
echo.
echo [5/5] Building rfcp-server (ONEDIR mode)...
echo This may take 3-5 minutes...
echo.
cd /d "%~dp0\..\backend"
pyinstaller "..\installer\rfcp-server-gpu.spec" --clean --noconfirm
echo.
echo ========================================
if exist "dist\rfcp-server\rfcp-server.exe" (
echo BUILD COMPLETE! (ONEDIR mode)
echo.
echo Output: backend\dist\rfcp-server\
dir /b dist\rfcp-server\*.exe dist\rfcp-server\*.dll 2>nul | find /c /v "" > nul
echo.
echo Test commands:
echo cd dist\rfcp-server
echo rfcp-server.exe
echo curl http://localhost:8090/api/health
echo curl http://localhost:8090/api/gpu/status
echo ========================================
) else (
echo BUILD FAILED — check errors above
echo ========================================
exit /b 1
)
pause

84
installer/build-gpu.sh Normal file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
set -e
echo "========================================"
echo " RFCP GPU Build — ONEDIR mode"
echo " CuPy-cuda13x + CUDA Toolkit 13.x"
echo "========================================"
echo ""
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BACKEND_DIR="$SCRIPT_DIR/../backend"
# Check backend exists
if [ ! -f "$BACKEND_DIR/run_server.py" ]; then
echo "ERROR: Backend not found at $BACKEND_DIR"
exit 1
fi
# Check Python
echo "[1/5] Checking Python..."
python3 --version || { echo "ERROR: Python3 not found"; exit 1; }
# Check CuPy
echo ""
echo "[2/5] Checking CuPy installation..."
if ! python3 -c "import cupy; print(f' CuPy {cupy.__version__}')" 2>/dev/null; then
echo "ERROR: CuPy not installed"
echo ""
echo "Install CuPy:"
echo " pip3 install cupy-cuda13x"
echo " # or for WSL2:"
echo " pip3 install cupy-cuda13x --break-system-packages"
exit 1
fi
# Check GPU compute
echo ""
echo "[3/5] Testing GPU compute..."
if python3 -c "import cupy; a = cupy.array([1,2,3]); assert a.sum() == 6; print(' GPU compute: OK')" 2>/dev/null; then
:
else
echo "WARNING: GPU compute test failed (may still work)"
fi
# Check CUDA
echo ""
echo "[4/5] Checking CUDA..."
if [ -n "$CUDA_PATH" ]; then
echo " CUDA_PATH: $CUDA_PATH"
else
echo " CUDA_PATH not set (relying on nvidia pip packages)"
fi
# Check nvidia pip packages
echo ""
echo "[5/5] Checking nvidia pip packages..."
python3 -c "import nvidia; print(' nvidia packages found')" 2>/dev/null || echo " No nvidia pip packages"
# Build
echo ""
echo "Building rfcp-server (ONEDIR mode)..."
echo ""
cd "$BACKEND_DIR"
pyinstaller "$SCRIPT_DIR/rfcp-server-gpu.spec" --clean --noconfirm
echo ""
echo "========================================"
if [ -f "dist/rfcp-server/rfcp-server" ] || [ -f "dist/rfcp-server/rfcp-server.exe" ]; then
echo " BUILD COMPLETE! (ONEDIR mode)"
echo ""
echo " Output: backend/dist/rfcp-server/"
ls -lh dist/rfcp-server/ | head -20
echo ""
echo " Test:"
echo " cd dist/rfcp-server"
echo " ./rfcp-server"
echo " curl http://localhost:8090/api/health"
echo "========================================"
else
echo " BUILD FAILED — check errors above"
echo "========================================"
exit 1
fi

View File

@@ -3,6 +3,7 @@ set -e
echo "========================================="
echo " RFCP Desktop Build (Windows)"
echo " GPU-enabled ONEDIR build"
echo "========================================="
cd "$(dirname "$0")/.."
@@ -14,15 +15,30 @@ npm ci
npm run build
cd ..
# 2. Build backend with PyInstaller
echo "[2/4] Building backend..."
# 2. Build backend with PyInstaller (GPU ONEDIR mode)
echo "[2/4] Building backend (GPU)..."
cd backend
# Check CuPy is available
if ! python -c "import cupy" 2>/dev/null; then
echo "WARNING: CuPy not installed - GPU acceleration will not be available"
echo " Install with: pip install cupy-cuda13x"
fi
python -m pip install -r requirements.txt
python -m pip install pyinstaller
cd ../installer
python -m PyInstaller rfcp-server.spec --clean --noconfirm
# Build using GPU spec (ONEDIR output)
python -m PyInstaller ../installer/rfcp-server-gpu.spec --clean --noconfirm
# Copy ONEDIR folder to desktop staging area
# Result: desktop/backend-dist/win/rfcp-server/rfcp-server.exe + _internal/
mkdir -p ../desktop/backend-dist/win
cp dist/rfcp-server.exe ../desktop/backend-dist/win/
rm -rf ../desktop/backend-dist/win/rfcp-server # Clean old build
cp -r dist/rfcp-server ../desktop/backend-dist/win/rfcp-server
echo " Backend copied to: desktop/backend-dist/win/rfcp-server/"
ls -la ../desktop/backend-dist/win/rfcp-server/*.exe 2>/dev/null || true
cd ..
# 3. Build Electron app

View File

@@ -0,0 +1,305 @@
# rfcp-server-gpu.spec — GPU-enabled build (CuPy + CUDA 13.x)
# RFCP Iteration 3.6.0
#
# Mode: ONEDIR (directory output, not single exe)
# This is better for CUDA — DLLs load directly without temp extraction
#
# Requirements:
# pip install cupy-cuda13x fastrlock pyinstaller
# CUDA Toolkit 13.x installed (winget install Nvidia.CUDA)
#
# Build:
# cd backend && pyinstaller ../installer/rfcp-server-gpu.spec --clean --noconfirm
#
# Output:
# backend/dist/rfcp-server/rfcp-server.exe (+ DLLs in same folder)
import os
import sys
import glob
from PyInstaller.utils.hooks import collect_all, collect_dynamic_libs
backend_path = os.path.abspath(os.path.join(os.path.dirname(SPEC), '..', 'backend'))
print(f"[GPU SPEC] Backend path: {backend_path}")
# ═══════════════════════════════════════════
# Collect CuPy packages
# ═══════════════════════════════════════════
cupy_datas = []
cupy_binaries = []
cupy_hiddenimports = []
cupyb_datas = []
cupyb_binaries = []
cupyb_hiddenimports = []
try:
cupy_datas, cupy_binaries, cupy_hiddenimports = collect_all('cupy')
cupyb_datas, cupyb_binaries, cupyb_hiddenimports = collect_all('cupy_backends')
print(f"[GPU SPEC] CuPy: {len(cupy_binaries)} binaries, {len(cupy_datas)} data files")
except Exception as e:
print(f"[GPU SPEC] WARNING: CuPy collection failed: {e}")
# NOTE: nvidia pip packages REMOVED - they have cuda12 DLLs that conflict with cupy-cuda13x
# We use CUDA Toolkit 13.x DLLs only
# ═══════════════════════════════════════════
# Collect CUDA Toolkit DLLs (system install)
# ═══════════════════════════════════════════
# Installed via: winget install Nvidia.CUDA
cuda_toolkit_binaries = []
cuda_path = os.environ.get('CUDA_PATH', '')
if cuda_path:
# Scan BOTH bin\ and bin\x64\ directories
cuda_bin_dirs = [
os.path.join(cuda_path, 'bin'),
os.path.join(cuda_path, 'bin', 'x64'),
]
# Only essential CUDA runtime DLLs (exclude NPP, nvjpeg, nvblas, nvfatbin)
cuda_dll_patterns = [
'cublas64_*.dll',
'cublasLt64_*.dll',
'cudart64_*.dll',
'cufft64_*.dll',
'cufftw64_*.dll',
'curand64_*.dll',
'cusolver64_*.dll',
'cusolverMg64_*.dll',
'cusparse64_*.dll',
'nvrtc64_*.dll',
'nvrtc-builtins64_*.dll',
'nvJitLink_*.dll',
'nvjitlink_*.dll',
]
collected_dlls = set() # Avoid duplicates
for cuda_bin in cuda_bin_dirs:
if os.path.isdir(cuda_bin):
for pattern in cuda_dll_patterns:
for dll in glob.glob(os.path.join(cuda_bin, pattern)):
dll_name = os.path.basename(dll)
if dll_name not in collected_dlls:
cuda_toolkit_binaries.append((dll, '.'))
collected_dlls.add(dll_name)
print(f"[GPU SPEC] Scanned: {cuda_bin}")
print(f"[GPU SPEC] CUDA Toolkit ({cuda_path}): {len(cuda_toolkit_binaries)} DLLs")
for dll, _ in cuda_toolkit_binaries:
print(f"[GPU SPEC] {os.path.basename(dll)}")
else:
print("[GPU SPEC] ERROR: CUDA_PATH not set!")
print("[GPU SPEC] Install: winget install Nvidia.CUDA")
# All GPU binaries (CUDA Toolkit only, no nvidia pip packages)
all_gpu_binaries = cuda_toolkit_binaries
if len(all_gpu_binaries) == 0:
print("[GPU SPEC] NO CUDA DLLs FOUND!")
print("[GPU SPEC] Install CUDA Toolkit: winget install Nvidia.CUDA")
else:
print(f"[GPU SPEC] Total GPU DLLs: {len(all_gpu_binaries)}")
# ═══════════════════════════════════════════
# Collect fastrlock (CuPy dependency)
# ═══════════════════════════════════════════
fl_datas = []
fl_binaries = []
fl_hiddenimports = []
try:
fl_datas, fl_binaries, fl_hiddenimports = collect_all('fastrlock')
print(f"[GPU SPEC] fastrlock: {len(fl_binaries)} binaries")
except Exception:
print("[GPU SPEC] fastrlock not found (optional)")
# ═══════════════════════════════════════════
# PyInstaller Analysis
# ═══════════════════════════════════════════
a = Analysis(
[os.path.join(backend_path, 'run_server.py')],
pathex=[backend_path],
binaries=(
cupy_binaries + cupyb_binaries +
fl_binaries + all_gpu_binaries
),
datas=[
# Include app/ source code
(os.path.join(backend_path, 'app'), 'app'),
] + cupy_datas + cupyb_datas + fl_datas,
hiddenimports=[
# ── Uvicorn internals ──
'uvicorn.logging',
'uvicorn.loops',
'uvicorn.loops.auto',
'uvicorn.loops.asyncio',
'uvicorn.protocols',
'uvicorn.protocols.http',
'uvicorn.protocols.http.auto',
'uvicorn.protocols.http.h11_impl',
'uvicorn.protocols.http.httptools_impl',
'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto',
'uvicorn.protocols.websockets.wsproto_impl',
'uvicorn.lifespan',
'uvicorn.lifespan.on',
'uvicorn.lifespan.off',
# ── FastAPI / Starlette ──
'fastapi',
'fastapi.middleware',
'fastapi.middleware.cors',
'fastapi.routing',
'fastapi.responses',
'fastapi.exceptions',
'starlette',
'starlette.routing',
'starlette.middleware',
'starlette.middleware.cors',
'starlette.responses',
'starlette.requests',
'starlette.concurrency',
'starlette.formparsers',
'starlette.staticfiles',
# ── Pydantic ──
'pydantic',
'pydantic.fields',
'pydantic_settings',
'pydantic_core',
# ── HTTP / networking ──
'httpx',
'httpcore',
'h11',
'httptools',
'anyio',
'anyio._backends',
'anyio._backends._asyncio',
'sniffio',
# ── MongoDB (motor/pymongo) ──
'motor',
'motor.motor_asyncio',
'pymongo',
'pymongo.errors',
'pymongo.collection',
'pymongo.database',
'pymongo.mongo_client',
# ── Async I/O ──
'aiofiles',
'aiofiles.os',
'aiofiles.ospath',
# ── Scientific ──
'numpy',
'numpy.core',
'scipy',
'scipy.special',
'scipy.interpolate',
'shapely',
'shapely.geometry',
'shapely.ops',
# ── Multipart ──
'multipart',
'python_multipart',
# ── Encoding ──
'email.mime',
'email.mime.multipart',
# ── Multiprocessing ──
'multiprocessing',
'multiprocessing.pool',
'multiprocessing.queues',
'concurrent.futures',
# ── CuPy + CUDA ──
'cupy',
'cupy.cuda',
'cupy.cuda.runtime',
'cupy.cuda.driver',
'cupy.cuda.memory',
'cupy.cuda.stream',
'cupy.cuda.device',
'cupy._core',
'cupy._core.core',
'cupy._core._routines_math',
'cupy._core._routines_logic',
'cupy._core._routines_manipulation',
'cupy._core._routines_sorting',
'cupy._core._routines_statistics',
'cupy._core._cub_reduction',
'cupy.fft',
'cupy.linalg',
'cupy.random',
'cupy_backends',
'cupy_backends.cuda',
'cupy_backends.cuda.api',
'cupy_backends.cuda.libs',
'fastrlock',
'fastrlock.rlock',
] + cupy_hiddenimports + cupyb_hiddenimports + fl_hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[os.path.join(os.path.dirname(SPEC), 'rthook_cuda_dlls.py')],
# ── Exclude bloat ──
excludes=[
# GUI
'tkinter',
'matplotlib',
'PIL',
'IPython',
# Data science bloat
'pandas',
'tensorflow',
'torch',
'keras',
# Testing
'pytest',
# Jupyter
'jupyter',
'notebook',
'ipykernel',
# gRPC / telemetry (often pulled in by dependencies)
'grpc',
'grpcio',
'google.protobuf',
'opentelemetry',
'opentelemetry.sdk',
'opentelemetry.instrumentation',
# Ray (too heavy, we use multiprocessing)
'ray',
# Other
'cv2',
'sklearn',
'sympy',
],
noarchive=False,
)
pyz = PYZ(a.pure)
# ═══════════════════════════════════════════
# ONEDIR mode: EXE + COLLECT
# ═══════════════════════════════════════════
# Creates: dist/rfcp-server/rfcp-server.exe + all DLLs in same folder
# Better for CUDA — no temp extraction needed
exe = EXE(
pyz,
a.scripts,
[], # No binaries/datas in EXE — they go in COLLECT
exclude_binaries=True, # ONEDIR mode
name='rfcp-server',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False, # Don't compress — CUDA libs need fast loading
console=True,
icon=os.path.join(os.path.dirname(SPEC), 'rfcp.ico') if os.path.exists(os.path.join(os.path.dirname(SPEC), 'rfcp.ico')) else None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name='rfcp-server',
)

BIN
installer/rfcp.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,24 @@
# PyInstaller runtime hook for CUDA DLL loading
# Must run BEFORE any CuPy import
#
# Problem: Windows Python 3.8+ requires os.add_dll_directory() for DLL search
# PyInstaller ONEDIR mode puts DLLs in _internal/ which isn't in the search path
import os
import sys
if sys.platform == 'win32' and getattr(sys, 'frozen', False):
# _MEIPASS points to _internal/ in ONEDIR mode
base = getattr(sys, '_MEIPASS', None)
if base and os.path.isdir(base):
os.add_dll_directory(base)
print(f"[CUDA DLL Hook] Added DLL directory: {base}")
# Also add CUDA_PATH if available (fallback to system CUDA)
cuda_path = os.environ.get('CUDA_PATH', '')
if cuda_path:
for subdir in ['bin', os.path.join('bin', 'x64')]:
d = os.path.join(cuda_path, subdir)
if os.path.isdir(d):
os.add_dll_directory(d)
print(f"[CUDA DLL Hook] Added CUDA_PATH: {d}")

64
rfcp-gpu-preflight.bat Normal file
View File

@@ -0,0 +1,64 @@
@echo off
echo ========================================
echo RFCP GPU Build — Pre-flight Check
echo ========================================
echo.
echo [1] Python version:
python --version
echo.
echo [2] CuPy status:
python -c "import cupy; print(f' CuPy {cupy.__version__}')"
python -c "import cupy; d=cupy.cuda.Device(0); print(f' Device: {d.id}'); print(f' Memory: {d.mem_info[1]//1024//1024} MB')"
echo.
echo [3] CUDA runtime version:
python -c "import cupy; v=cupy.cuda.runtime.runtimeGetVersion(); print(f' CUDA Runtime: {v}')"
echo.
echo [4] CUDA_PATH environment:
if defined CUDA_PATH (
echo CUDA_PATH = %CUDA_PATH%
) else (
echo WARNING: CUDA_PATH not set!
echo.
echo Checking common locations...
if exist "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA" (
for /d %%i in ("C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v*") do (
echo Found: %%i
echo.
echo To fix, run:
echo setx CUDA_PATH "%%i"
echo Then restart terminal.
)
) else (
echo No CUDA Toolkit found in default location.
echo CuPy bundles its own CUDA runtime, so this may be OK.
echo But PyInstaller build might need it.
)
)
echo.
echo [5] nvidia-smi:
nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader 2>nul
if errorlevel 1 echo nvidia-smi not found in PATH
echo.
echo [6] CuPy CUDA libs location:
python -c "import cupy; import os; print(f' {os.path.dirname(cupy.__file__)}')"
python -c "import cupy._core.core" 2>nul && echo cupy._core.core: OK || echo cupy._core.core: FAILED
echo.
echo [7] fastrlock:
python -c "import fastrlock; print(f' fastrlock {fastrlock.__version__}')"
echo.
echo [8] PyInstaller:
python -c "import PyInstaller; print(f' PyInstaller {PyInstaller.__version__}')" 2>nul || echo PyInstaller NOT installed! Run: pip install pyinstaller
echo.
echo ========================================
echo Pre-flight complete
echo ========================================
pause