@mytec: iter2.4 ready for testing

This commit is contained in:
2026-02-01 10:48:23 +02:00
parent 7893c57bc9
commit 5488633e43
19 changed files with 1448 additions and 69 deletions

1254
docs/RFCP-ARCHITECTURE.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,703 @@
# RFCP Project Roadmap - Complete History & Future Plans
**Project:** RFCP (RF Coverage Planning) for Ukrainian Military Tactical Communications
**Developer:** Олег
**Started:** January 30, 2025 (this morning!)
**Current Status:** Frontend 95% Complete, Backend 0% Started
**Last Updated:** January 30, 2025 (end of day)
---
## 📊 Project Overview
**Vision:** Professional RF coverage planning tool for tactical military communications
**Tech Stack:**
- **Frontend:** React 18 + TypeScript + Vite + Leaflet
- **Backend:** Python FastAPI + MongoDB (planned)
- **Infrastructure:** VPS + WireGuard mesh + Caddy reverse proxy
- **Methodology:** RFCP (Recursive Feedback Collaborative Process)
**Current Deployment:**
- URL: https://rfcp.eliah.one
- Status: Production (functional with minor UX iteration needed)
---
## ✅ Completed Phases
### Phase 0: Foundation (December 2024)
**Goal:** Project setup and initial prototype
**Achievements:**
- ✅ React + TypeScript + Vite setup
- ✅ Leaflet map integration
- ✅ Basic site management (CRUD)
- ✅ Simple coverage calculation
- ✅ VPS deployment with Caddy
**Key Decisions:**
- Chose Leaflet over Mapbox (open-source, no API keys)
- TypeScript strict mode from day 1
- Zustand for state management (simpler than Redux)
---
### Iteration 1-7: Core Features (December 2024 - January 2025)
**Iteration 1: Basic RF Calculations**
- Path loss models (Free Space, Okumura-Hata, Egli)
- RSRP calculation
- Simple heatmap overlay
**Iteration 2: Multi-Site Support**
- Multiple sites on map
- Site selection/editing
- Aggregate coverage (strongest signal wins)
**Iteration 3: Antenna Patterns**
- Omnidirectional vs Directional
- Azimuth and beamwidth controls
- Antenna gain calculations
**Iteration 4: Terrain Integration**
- Elevation data (planned, not implemented)
- Line-of-sight checks (basic)
- Fresnel zone clearance (basic)
**Iteration 5: Performance Optimization**
- Web Workers for parallel calculation
- Tile-based rendering
- Coverage cache
**Iteration 6: UI/UX Improvements**
- Side panels (Sites, Settings, Stats)
- Dark mode
- Responsive layout
**Iteration 7: Settings & Controls**
- Coverage radius control
- Resolution settings
- RSRP threshold controls
- Opacity sliders
---
### Iteration 8: Geographic Heatmap (Major Milestone) ⭐
**Date:** Mid-January 2025
**Problem Solved:** Previous heatmap scaled with zoom level (bad!)
**Achievement:** Custom geographic-scale canvas renderer
- True 400m radius constant across all zoom levels
- 256×256 tile rendering with cache
- Replaced leaflet.heat dependency entirely
**Technical Highlight:**
```typescript
// Custom GeographicHeatmap component
// Calculates exact geographic coordinates for each pixel
// Renders to canvas tiles that match Leaflet's tile system
```
**Impact:** Professional-grade heatmap visualization! 🎉
---
### Iteration 8.1: Multi-Sector Sites ⭐
**Date:** Late January 2025
**Achievement:** Sites can have multiple sectors (Alpha, Beta, Gamma)
- Greek alphabet naming (Alpha, Beta, Gamma, Delta...)
- Clone sector button (+ Sector) with 120° azimuth offset
- Each sector = independent antenna with own parameters
**UI Pattern:**
```
Site "Vyshka_1"
├─ Alpha (azimuth: 0°)
├─ Beta (azimuth: 120°)
└─ Gamma (azimuth: 240°)
```
**Use Case:** Real-world base stations have 3 sectors for 360° coverage
---
### Iteration 9: Keyboard Shortcuts ⚡
**Date:** Late January 2025
**Added Shortcuts:**
- `N` - New Site
- `S` - Save Project
- `C` - Clone Selected Sector
- `Delete` - Delete Selected Site
- `Ctrl+Z` - Undo
- `Ctrl+Shift+Z` - Redo
- `Ctrl+F` - Search Sites
- `Esc` - Close Dialogs
**Pattern:** useKeyboardShortcuts hook with callback interface
---
### Iteration 9.1: NumberInput Components
**Date:** Late January 2025
**Replaced:** All `<input type="number">` with custom NumberInput component
**Features:**
- Integrated slider below input
- Real-time value validation
- Consistent styling
- Better UX for numeric settings
**Applied to:**
- Transmit Power (1-100W)
- Antenna Gain (0-30 dBi)
- Antenna Height (1-500m)
- Frequency (700-3800 MHz)
- Azimuth (0-359°)
- Beamwidth (10-360°)
---
### Iteration 9.1.1: Delete Confirmation & Undo
**Date:** Late January 2025
**Added:**
- ConfirmDialog component (reusable)
- Delete confirmation with site name
- Danger variant styling (red)
- 10-second undo toast after delete
**UX Pattern:**
```
Delete button → ConfirmDialog → Delete + Undo Toast (10s)
```
---
### Iteration 10: Final Frontend Audit ⭐ (Major Milestone)
**Date:** January 29-30, 2025
**Goal:** Production-ready frontend
**Phase 1: Pre-Audit Fixes**
- ✅ Fixed potential crashes
- ✅ Cleaned up console logs
**Phase 2: Code Audit & Refactor**
- ✅ TypeScript strict mode: 0 errors
- ✅ Dead code removal (3 unused files)
- ✅ Created logger utility (dev/prod separation)
- ✅ Console cleanup (6 files migrated to logger)
- ✅ React.memo optimization (SiteMarker, CoverageStats)
**Phase 3: UX Polish**
- ✅ Tooltips on Coverage Settings (5 NumberInputs)
- ✅ Improved empty states (CoverageStats, ExportPanel)
- ✅ ESLint cleanup (0 errors, 1 acceptable warning)
**Phase 4: Final Audit**
- ✅ TypeScript: 0 errors
- ✅ ESLint: 0 errors
- ✅ Bundle: 535KB / 163KB gzipped (under 500KB target!)
- ✅ Production build: Success
**Build Time:** 1.49s (excellent!)
---
### Iteration 10.1: Critical Bugfixes ✅ (Just Completed!)
**Date:** January 30, 2025
**Bug 1: Stack Overflow at 50m Resolution**
- Root cause: `Math.min(...150k_array)` exceeded argument limit
- Fix: Replaced with `for...of` loop (faster + no limits)
- File: `CoverageStats.tsx`
**Bug 2: No Confirmation on Delete Key**
- Root cause: Keyboard handler bypassed ConfirmDialog
- Fix: Added `onDeleteRequest` callback pattern
- Files: `useKeyboardShortcuts.ts`, `App.tsx`
**Bug 3: Cyan/Green Coverage Zone**
- Root cause: RSRP gradient used cyan/green colors (conflicted with map)
- Fix: Replaced with warm-only palette (maroon → red → orange → yellow)
- Files: `colorGradient.ts`, `rsrp-thresholds.ts`
**Status:** All bugs fixed, deployed, tested ✅
**New Issue Discovered:** Gradient aesthetics - yellow (good) → red (bad) feels counter-intuitive to users. Needs UX decision in next session.
---
## 📅 Timeline Summary
```
January 30, 2025
Morning ──────────────────────────────────────────────── Evening
| |
v v
[Planning]──[Iteration 10]──[Bugs Found]──[Iteration 10.1]──[Complete!]
(Frontend Audit) (Critical Fixes) ✅
```
**Total Development Time:** ~1 day (!)
**Sessions:** 2 (morning planning + afternoon implementation)
**Major Milestones:** Iteration 10 (Audit), Iteration 10.1 (Bugfixes)
**Note:** RFCP already had ~8 iterations of prior work. Today we audited, found bugs, and fixed them all! 🚀
---
## 🎯 Current Status (End of Iteration 10.1)
### ✅ What's Working
**Core Functionality:**
- ✅ Multi-site RF coverage planning
- ✅ Multi-sector sites (Alpha, Beta, Gamma...)
- ✅ Geographic-scale heatmap visualization
- ✅ RSRP gradient overlay
- ✅ Site CRUD operations
- ✅ Coverage calculation (Web Workers)
- ✅ Keyboard shortcuts
- ✅ Delete confirmation + undo
**Code Quality:**
- ✅ TypeScript strict mode (0 errors)
- ✅ ESLint clean (0 errors)
- ✅ Production build optimized
- ✅ Console logs cleaned up
- ✅ Performance optimized
**Deployment:**
- ✅ Live at https://rfcp.eliah.one
- ✅ SSL via Caddy
- ✅ VPN-only access (security)
### ⚠️ Known Issues
**Aesthetic (P2):**
- Gradient color palette feels counter-intuitive
- Yellow = good signal (technically correct)
- Red = bad signal (technically correct)
- But users expect red = danger/bad
- **Action:** Need UX decision on color scheme
### 🚧 Not Yet Implemented
**Frontend Features (Nice-to-Have):**
- Keyboard shortcuts help modal (press `?`)
- Project export/import (JSON)
- Settings persistence (localStorage)
- Input validation with error messages
- Mobile optimization
- PWA offline mode
- Print stylesheet
**Backend (Not Started):**
- API endpoints (FastAPI)
- Database (MongoDB)
- User authentication
- Project storage
- Multi-user support
**Advanced RF Features (Future):**
- Real terrain data (SRTM/ASTER)
- Accurate line-of-sight
- Fresnel zone calculation
- Link budget analysis
- Interference modeling
- 3D visualization
---
## 🔜 Next Steps
### Immediate (Next Session)
**1. Gradient Color Decision** (15-30 min)
- Review current palette: Yellow (good) → Red (bad)
- Discuss 3 options:
- **Option A:** Reverse gradient (red = good, yellow = bad)
- **Option B:** Purple → Orange palette (purple = bad, orange = good)
- **Option C:** Grayscale → Orange (gray = bad, orange = good)
- Implement chosen palette
- Test and validate with user
**2. Optional UX Polish** (if time allows)
- Keyboard shortcuts help modal
- Project export/import
- Settings persistence
---
### Short Term (Next 1-2 Weeks)
**Frontend Polish Phase:**
- ✅ Gradient color finalized
- Add keyboard shortcuts help (`?` key)
- Implement export/import (JSON format)
- Add settings persistence (localStorage)
- Input validation with error messages
- Mobile responsive improvements
**Estimated Time:** 4-6 hours total
---
### Medium Term (Next 1-2 Months)
**Backend Development Phase:**
**Week 1-2: Backend Setup**
- FastAPI project structure
- MongoDB connection
- User authentication (JWT)
- Basic CRUD API endpoints
**Week 3-4: Backend Features**
- Project storage (save/load)
- User projects (multi-user support)
- Sharing & collaboration
- API documentation (Swagger)
**Week 5-6: Integration**
- Connect frontend to backend
- Replace localStorage with API
- Testing & debugging
- Deployment
**Week 7-8: Hardening**
- API rate limiting
- Input validation
- Error handling
- Logging & monitoring
- Security audit
**Estimated Time:** 40-60 hours total
---
### Long Term (3-6 Months)
**Advanced Features Phase:**
**Terrain Integration:**
- SRTM/ASTER elevation data
- Accurate line-of-sight calculation
- Fresnel zone clearance
- Terrain-based path loss
**RF Enhancements:**
- Link budget calculator
- Interference modeling
- Multiple frequency bands
- Custom propagation models
**Visualization:**
- 3D terrain view
- Signal strength profiles
- Coverage comparison mode
- Time-based analysis
**Collaboration:**
- Real-time multi-user editing
- Comments & annotations
- Version history
- Export to professional formats (KML, GeoJSON)
**Estimated Time:** 80-120 hours total
---
## 🎓 Lessons Learned
### Technical Insights
**1. Spread Operator Limits**
- JavaScript engines have argument limits (~65k-125k)
- Never use spread with large arrays (>10k elements)
- Use `reduce()` or `for...of` loops instead
**2. Geographic Calculations are Hard**
- Need precise lat/lon → pixel conversions
- Must account for map projection (Web Mercator)
- Tile-based rendering is complex but worth it
**3. React Performance Optimization**
- React.memo for components with stable props
- useMemo for expensive calculations
- useCallback for stable callbacks
- Web Workers for heavy computation
**4. TypeScript Strict Mode**
- Worth enabling from day 1
- Catches bugs early
- Forces better code design
- Minor performance overhead acceptable
### UX Insights
**1. Color Psychology Matters**
- Users have strong expectations (red = bad, green = good)
- Map features have colors too (blue = water, green = vegetation)
- Gradient must balance data accuracy + user intuition + map conflicts
**2. Confirmation Dialogs are Critical**
- Destructive actions MUST have confirmation
- Keyboard shortcuts must follow same rules as UI buttons
- Undo toast = great safety net (10s duration)
**3. Progressive Disclosure Works**
- Start with simple controls (power, frequency)
- Advanced features in collapsible sections
- Tooltips for complex settings
- Keyboard shortcuts for power users
### Process Insights
**1. RFCP Method is Effective**
- Iterative development with clear goals
- Each iteration = working feature
- Document everything (specs, decisions, learnings)
- Easy to resume after breaks
**2. User Feedback is Gold**
- Real usage reveals issues (e.g., gradient colors)
- Stack traces help find root causes
- Users know their domain (military comms)
- Trust technical decisions, guide UX
**3. Code Quality Pays Off**
- TypeScript strict mode caught bugs
- ESLint enforced consistency
- Logger utility simplified debugging
- Clean code = faster iteration
---
## 📊 Project Metrics
### Code Statistics
**Lines of Code:**
- Frontend: ~8,000 lines (TypeScript + TSX)
- Backend: 0 lines (not started)
**Files:**
- Components: ~25 files
- Utils/Lib: ~15 files
- Stores: ~5 files
- Total: ~50+ files
**Dependencies:**
- Production: ~15 packages (React, Leaflet, Zustand, etc.)
- Development: ~20 packages (Vite, TypeScript, ESLint, etc.)
**Bundle Size:**
- Uncompressed: 536KB
- Gzipped: 163KB
- Target: <500KB gzipped ✅
### Development Statistics
**Today's Session (Jan 30, 2025):**
- Morning: Iteration 10 planning & spec
- Afternoon: Claude Code implementation
- Evening: Bug discovery & Iteration 10.1 fixes
- Total: ~8-10 hours productive work
**Prior Work (Iterations 1-9):**
- Completed before today
- Frontend already functional
- Geographic heatmap, multi-sector, keyboard shortcuts all working
**Today's Impact:**
- Fixed 3 critical bugs
- Production-ready code quality
- Full audit complete
- Zero TypeScript/ESLint errors
---
## 🎯 Success Criteria
### Frontend (Current Phase)
**Must Have (✅ Complete):**
- Multi-site coverage planning
- Geographic-scale heatmap
- Professional UI/UX
- Production-ready code
- Zero critical bugs
**Should Have (⚠️ Partial):**
- Keyboard shortcuts ✅
- Delete confirmation ✅
- Code quality ✅
- Color palette ⚠️ (needs UX decision)
**Nice to Have (❌ Not Done):**
- Shortcuts help modal
- Export/import
- Settings persistence
- Mobile optimization
- PWA features
### Backend (Not Started)
**Must Have:**
- User authentication
- Project storage
- CRUD API endpoints
- MongoDB integration
- Deployment
**Should Have:**
- Multi-user support
- Sharing & collaboration
- API documentation
- Security hardening
**Nice to Have:**
- Real-time collaboration
- Version history
- Advanced analytics
### Overall Project
**Current Status:** Frontend 95%, Backend 0%
**Target:** Frontend 100%, Backend 80% (MVP)
**Timeline:** ~2-3 months to MVP
---
## 📁 Project Structure
```
rfcp/
├── frontend/ # React + TypeScript + Vite
│ ├── src/
│ │ ├── components/ # UI components
│ │ │ ├── map/ # Map-related
│ │ │ ├── panels/ # Side panels
│ │ │ ├── ui/ # Reusable UI
│ │ │ └── common/ # Common components
│ │ ├── store/ # Zustand stores
│ │ ├── lib/ # Business logic
│ │ ├── utils/ # Utilities
│ │ ├── hooks/ # Custom hooks
│ │ ├── types/ # TypeScript types
│ │ └── constants/ # Constants
│ ├── public/ # Static assets
│ │ └── workers/ # Web Workers
│ └── dist/ # Production build
├── backend/ # FastAPI + MongoDB (planned)
│ ├── app/
│ │ ├── api/ # API routes
│ │ ├── models/ # Data models
│ │ ├── services/ # Business logic
│ │ └── utils/ # Utilities
│ └── tests/ # Backend tests
└── docs/ # Documentation
├── SESSION-*.md # Session summaries
├── RFCP-Iteration*.md # Iteration specs
├── ARCHITECTURE.md # Architecture decisions
└── DEPLOYMENT.md # Deployment guide
```
---
## 🚀 Quick Start (For New Developers)
### Getting Started
**1. Read Documentation:**
```bash
# Start with latest session
cat docs/SESSION-2025-01-30-Iteration-10.1-Complete.md
# Then read iterations in order
cat docs/RFCP-Iteration8-Geographic-Heatmap.md
cat docs/RFCP-Iteration10-Final-Audit.md
cat docs/RFCP-Iteration10.1-Critical-Bugfixes.md
```
**2. Setup Development Environment:**
```bash
cd /opt/rfcp/frontend
npm install
npm run dev
```
**3. Understand Architecture:**
- Read `ARCHITECTURE.md` (to be created)
- Review component structure
- Study state management (Zustand stores)
**4. Make First Contribution:**
- Pick a "Nice to Have" feature
- Create new iteration spec
- Implement and test
- Document learnings
---
## 🎬 Next Session Checklist
**Before Starting:**
- [ ] Read `SESSION-2025-01-30-Iteration-10.1-Complete.md`
- [ ] Review this roadmap
- [ ] Check current deployment at https://rfcp.eliah.one
**First Task:**
- [ ] Discuss gradient color palette with Олег
- [ ] Choose option (A, B, or C)
- [ ] Implement and test
- [ ] Deploy if satisfied
**Then:**
- [ ] Continue with UX polish (shortcuts help, export/import)
- [ ] Or start backend planning
**Document:**
- [ ] Create new session summary
- [ ] Update roadmap with new decisions
- [ ] Plan next iteration
---
## 📞 Contact & Resources
**Project Owner:** Олег
**Project Repository:** `/opt/rfcp/` on VPS (10.10.10.1)
**Live Site:** https://rfcp.eliah.one (VPN required)
**Documentation:** `/mnt/project/` and `/home/claude/`
**Key Documents:**
- `RFCP-Method-Collaboration-Framework.md` - Work methodology
- `SESSION-*.md` - Session summaries
- `RFCP-Iteration*.md` - Iteration specs
---
**Status:** ✅ ROADMAP COMPLETE
**Last Updated:** 2025-01-30
**Next Update:** After gradient color decision + next iteration
**Total Roadmap Length:** ~600 lines of comprehensive project history and future plans! 🎉

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,543 @@
# RFCP Phase 2.3: Performance Optimization
**Date:** January 31, 2025
**Type:** Performance & Parallelization
**Estimated:** 8-12 hours
**Priority:** HIGH — enables practical use of Detailed preset
**Depends on:** Phase 2.2 (Offline Caching)
---
## 🎯 Goal
Make Detailed preset usable by parallelizing calculations across CPU cores and optionally GPU. Target: **10-50x speedup**.
---
## 📊 Current Performance
| Preset | Points | Current Time | Target Time |
|--------|--------|--------------|-------------|
| Fast | 868 | 0.03s | 0.03s ✅ |
| Standard | 868 | 13s | 5s |
| Detailed | 868 | 300s+ (timeout) | 30s |
**Bottleneck Analysis:**
```
[DOMINANT_PATH] Point #1: line_bldgs=646, refl_bldgs=302
- 868 points × 700 buildings × geometry = millions of operations
- Single-threaded Python
- 2 sec/point → 868 × 2 = 1736 sec theoretical
```
---
## 🏗️ Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Coverage Calculation │
├─────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: OSM Fetch (async, cached) → unchanged │
│ Phase 2: Terrain Pre-load (async) → unchanged │
│ Phase 3: Point Calculation → PARALLELIZE │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ProcessPoolExecutor │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Core 1 │ │ Core 2 │ │ Core 3 │ │ Core N │ │ │
│ │ │ pts 0-61│ │pts 62-123│ │pts 124..│ │ pts ... │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Optional: GPU Acceleration │ │
│ │ - Path loss matrix calculation (NumPy → CuPy) │ │
│ │ - Batch terrain lookups │ │
│ │ - Vectorized distance calculations │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## ✅ Tasks
### Task 2.3.1: Multiprocessing Infrastructure (3-4 hours)
**Problem:** Python GIL prevents true parallelism with threads. Need processes.
**Create `backend/app/services/parallel_coverage_service.py`:**
```python
import os
import multiprocessing as mp
from concurrent.futures import ProcessPoolExecutor, as_completed
from typing import List, Dict, Any, Tuple
import time
# Shared data for worker processes (loaded once per process)
_worker_data = {}
def _init_worker(terrain_cache: Dict, buildings: List, spatial_index_data: Dict, settings_dict: Dict):
"""Initialize worker process with shared data."""
global _worker_data
_worker_data = {
'terrain_cache': terrain_cache,
'buildings': buildings,
'spatial_index': rebuild_spatial_index(spatial_index_data),
'settings': settings_dict,
}
# Import heavy modules inside worker to avoid pickle issues
from app.services.terrain_service import TerrainService
from app.services.los_service import LOSService
from app.services.dominant_path_service import DominantPathService
_worker_data['terrain_service'] = TerrainService()
_worker_data['terrain_service']._tile_cache = terrain_cache
_worker_data['los_service'] = LOSService(_worker_data['terrain_service'])
_worker_data['dominant_path_service'] = DominantPathService(
_worker_data['terrain_service'],
_worker_data['los_service']
)
def _calculate_point_worker(args: Tuple) -> Dict:
"""Worker function for single point calculation."""
global _worker_data
lat, lon, site_lat, site_lon, site_elevation, point_elevation = args
# Use pre-initialized services
terrain = _worker_data['terrain_service']
los = _worker_data['los_service']
dominant = _worker_data['dominant_path_service']
settings = _worker_data['settings']
buildings = _worker_data['buildings']
spatial_idx = _worker_data['spatial_index']
# ... calculation logic (copy from _calculate_point_sync)
return {
'lat': lat,
'lon': lon,
'rsrp': rsrp,
'distance': distance,
# ... other fields
}
class ParallelCoverageService:
"""Coverage calculation with multiprocessing."""
def __init__(self):
# Detect available cores
self.num_workers = min(mp.cpu_count(), 14) # Cap at 14
print(f"[Coverage] Parallel mode: {self.num_workers} workers")
async def calculate_parallel(
self,
sites: List,
settings: CoverageSettings,
terrain_cache: Dict,
buildings: List,
spatial_index_data: Dict,
) -> List[Dict]:
"""Calculate coverage using multiple processes."""
# Prepare grid
grid = self._generate_grid(sites, settings)
total_points = len(grid)
print(f"[Coverage] Starting parallel calculation: {total_points} points, {self.num_workers} workers")
# Pre-compute point elevations
point_elevations = {(lat, lon): elev for lat, lon, elev in grid_with_elevations}
# Prepare arguments for workers
work_items = [
(lat, lon, site.lat, site.lon, site_elevation, point_elevations.get((lat, lon), 0))
for lat, lon in grid
]
# Run in process pool
results = []
start_time = time.time()
with ProcessPoolExecutor(
max_workers=self.num_workers,
initializer=_init_worker,
initargs=(terrain_cache, buildings, spatial_index_data, settings.dict())
) as executor:
# Submit all tasks
futures = {executor.submit(_calculate_point_worker, item): i
for i, item in enumerate(work_items)}
# Collect results with progress
completed = 0
for future in as_completed(futures):
result = future.result()
results.append(result)
completed += 1
if completed % (total_points // 10) == 0:
elapsed = time.time() - start_time
rate = completed / elapsed
eta = (total_points - completed) / rate
print(f"[Coverage] Progress: {completed}/{total_points} ({100*completed//total_points}%) - ETA: {eta:.1f}s")
elapsed = time.time() - start_time
print(f"[Coverage] Parallel calculation done: {elapsed:.1f}s ({elapsed/total_points*1000:.1f}ms/point)")
return results
```
---
### Task 2.3.2: Data Serialization for Workers (2-3 hours)
**Problem:** Each worker process needs access to terrain cache, buildings, spatial index. Can't share directly.
**Solutions:**
1. **Shared Memory (Python 3.8+):**
```python
from multiprocessing import shared_memory
import numpy as np
# Create shared terrain cache
terrain_shm = shared_memory.SharedMemory(create=True, size=terrain_array.nbytes)
terrain_shared = np.ndarray(terrain_array.shape, dtype=terrain_array.dtype, buffer=terrain_shm.buf)
terrain_shared[:] = terrain_array[:]
```
2. **Memory-mapped files:**
```python
import mmap
import numpy as np
# Save terrain to mmap file
terrain_mmap = np.memmap('terrain_cache.dat', dtype='int16', mode='w+', shape=(3601, 3601))
terrain_mmap[:] = terrain_data[:]
terrain_mmap.flush()
# Workers read from same file
worker_terrain = np.memmap('terrain_cache.dat', dtype='int16', mode='r', shape=(3601, 3601))
```
3. **Pickle once, load in each worker:**
```python
# Main process saves data
import pickle
with open('worker_data.pkl', 'wb') as f:
pickle.dump({'terrain': terrain_cache, 'buildings': buildings}, f)
# Worker loads once at init
def _init_worker(data_path):
global _worker_data
with open(data_path, 'rb') as f:
_worker_data = pickle.load(f)
```
**Recommendation:** Start with pickle (simplest), optimize with mmap if needed.
---
### Task 2.3.3: Integrate Parallel Service (2 hours)
**Update `coverage_service.py`:**
```python
class CoverageService:
def __init__(self):
self.parallel_service = ParallelCoverageService()
self.use_parallel = True # Can be toggled
self.parallel_threshold = 100 # Use parallel for > 100 points
async def calculate(self, sites, settings):
grid = self._generate_grid(sites, settings)
# Decide execution mode
if self.use_parallel and len(grid) > self.parallel_threshold:
return await self._calculate_parallel(sites, settings, grid)
else:
return await self._calculate_sequential(sites, settings, grid)
async def _calculate_parallel(self, sites, settings, grid):
# Phase 1: OSM fetch (same as before)
buildings, streets, water, vegetation = await self._fetch_osm_grid_aligned(...)
# Phase 2: Terrain pre-load (same as before)
await self.terrain.ensure_tiles_for_bbox(...)
terrain_cache = self.terrain._tile_cache.copy()
# Phase 3: Parallel point calculation
spatial_index_data = self._serialize_spatial_index(spatial_idx)
results = await self.parallel_service.calculate_parallel(
sites=sites,
settings=settings,
terrain_cache=terrain_cache,
buildings=buildings,
spatial_index_data=spatial_index_data,
)
return results
```
---
### Task 2.3.4: GPU Acceleration (Optional) (3-4 hours)
**Only if NVIDIA GPU detected. Use CuPy for NumPy-like GPU operations.**
**Create `backend/app/services/gpu_service.py`:**
```python
import os
# Check for GPU
GPU_AVAILABLE = False
try:
import cupy as cp
GPU_AVAILABLE = cp.cuda.runtime.getDeviceCount() > 0
if GPU_AVAILABLE:
print(f"[GPU] CUDA available: {cp.cuda.runtime.getDeviceProperties(0)['name'].decode()}")
except ImportError:
pass
class GPUService:
"""GPU-accelerated calculations using CuPy."""
def __init__(self):
self.enabled = GPU_AVAILABLE
def calculate_path_loss_batch(
self,
distances: np.ndarray, # (N,) array of distances in meters
frequency_mhz: float,
tx_height: float,
rx_height: float,
) -> np.ndarray:
"""Calculate Okumura-Hata path loss for all points at once."""
if self.enabled:
import cupy as cp
d = cp.asarray(distances)
else:
d = distances
# Okumura-Hata formula (vectorized)
d_km = d / 1000.0
f = frequency_mhz
hb = tx_height
hm = rx_height
# Urban area correction
a_hm = (1.1 * np.log10(f) - 0.7) * hm - (1.56 * np.log10(f) - 0.8)
# Path loss
L = (46.3 + 33.9 * np.log10(f) - 13.82 * np.log10(hb) - a_hm +
(44.9 - 6.55 * np.log10(hb)) * np.log10(d_km))
if self.enabled:
return cp.asnumpy(L)
return L
def calculate_distances_batch(
self,
site_lat: float,
site_lon: float,
point_lats: np.ndarray,
point_lons: np.ndarray,
) -> np.ndarray:
"""Calculate distances from site to all points (Haversine)."""
if self.enabled:
import cupy as cp
lat1 = cp.radians(site_lat)
lon1 = cp.radians(site_lon)
lat2 = cp.radians(cp.asarray(point_lats))
lon2 = cp.radians(cp.asarray(point_lons))
else:
lat1 = np.radians(site_lat)
lon1 = np.radians(site_lon)
lat2 = np.radians(point_lats)
lon2 = np.radians(point_lons)
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
c = 2 * np.arcsin(np.sqrt(a))
R = 6371000 # Earth radius in meters
distances = R * c
if self.enabled:
return cp.asnumpy(distances)
return distances
gpu_service = GPUService()
```
**Add to requirements.txt (optional):**
```
cupy-cuda12x>=12.0.0 # For CUDA 12.x
# or cupy-cuda11x>=11.0.0 # For CUDA 11.x
```
---
### Task 2.3.5: Settings UI for Parallel/GPU (1 hour)
**Add to frontend Settings panel:**
```typescript
// Performance settings
<div className="settings-section">
<h4>Performance</h4>
<label>
<input
type="checkbox"
checked={settings.useParallel}
onChange={(e) => updateSettings({ useParallel: e.target.checked })}
/>
Use parallel processing ({cpuCores} cores)
</label>
{gpuAvailable && (
<label>
<input
type="checkbox"
checked={settings.useGPU}
onChange={(e) => updateSettings({ useGPU: e.target.checked })}
/>
Use GPU acceleration ({gpuName})
</label>
)}
<div className="worker-count">
<label>Worker processes:</label>
<input
type="number"
min={1}
max={cpuCores}
value={settings.workerCount}
onChange={(e) => updateSettings({ workerCount: e.target.value })}
/>
</div>
</div>
```
**Add API endpoint for system info:**
```python
@router.get("/api/system/info")
async def get_system_info():
import multiprocessing as mp
gpu_info = None
try:
import cupy as cp
if cp.cuda.runtime.getDeviceCount() > 0:
props = cp.cuda.runtime.getDeviceProperties(0)
gpu_info = {
'name': props['name'].decode(),
'memory_mb': props['totalGlobalMem'] // (1024 * 1024),
}
except:
pass
return {
'cpu_cores': mp.cpu_count(),
'gpu': gpu_info,
'parallel_enabled': True,
'gpu_enabled': gpu_info is not None,
}
```
---
## 🧪 Testing
```bash
# Run performance test
cd installer
.\test-coverage.bat
# Expected results after optimization:
# Fast: 0.03s (unchanged)
# Standard: ~5s (was 13s)
# Detailed: ~30s (was 300s+ timeout)
```
**Benchmark script:**
```python
# test_parallel.py
import asyncio
import time
from app.services.coverage_service import coverage_service
async def benchmark():
settings = CoverageSettings(
radius=5000,
resolution=300,
preset='detailed',
)
site = Site(lat=50.45, lon=30.52, ...)
# Warm up
await coverage_service.calculate([site], settings)
# Benchmark
times = []
for i in range(3):
start = time.time()
result = await coverage_service.calculate([site], settings)
elapsed = time.time() - start
times.append(elapsed)
print(f"Run {i+1}: {elapsed:.1f}s, {len(result)} points")
print(f"Average: {sum(times)/len(times):.1f}s")
asyncio.run(benchmark())
```
---
## ✅ Success Criteria
- [ ] Multiprocessing uses all available CPU cores
- [ ] Detailed preset completes in <60s for 5km radius
- [ ] No memory leaks with large calculations
- [ ] GPU acceleration works if NVIDIA card present
- [ ] Settings UI shows core count and GPU status
- [ ] Progress indicator updates during calculation
---
## 📊 Expected Performance
| Preset | Before | After (14 cores) | After (14 cores + GPU) |
|--------|--------|------------------|------------------------|
| Fast | 0.03s | 0.03s | 0.03s |
| Standard | 13s | ~2s | ~1s |
| Detailed | 300s+ | ~25s | ~10s |
---
## 🔜 Next: Phase 2.4
- [ ] R-tree spatial index (replace grid-based)
- [ ] Simplified building geometry for distant points
- [ ] Level-of-detail (LOD) system
- [ ] Streaming results (show partial coverage while calculating)
---
**Ready for Claude Code** 🚀