1514 lines
58 KiB
Markdown
1514 lines
58 KiB
Markdown
# RFCP Rust Migration Plan
|
||
|
||
## Python/React/Electron → Tauri/SvelteKit/Rust
|
||
|
||
**Version:** 1.0
|
||
**Date:** February 2026
|
||
**Status:** Planning
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
This document provides a comprehensive migration plan for RFCP (RF Coverage Planner) from the current Python/React/Electron stack to Tauri/SvelteKit/Rust. The migration targets:
|
||
|
||
- **Performance:** 50km full-preset calculations (currently times out at ~20km)
|
||
- **Bundle Size:** 5-15MB (currently 170MB+)
|
||
- **Memory:** Constant memory for any radius (currently OOMs on large areas)
|
||
|
||
---
|
||
|
||
## Part 1: Current Architecture Analysis
|
||
|
||
### 1.1 High-Level Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────┐
|
||
│ Electron Shell │
|
||
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
|
||
│ │ React Frontend │ │ Python Backend (PyInstaller) │ │
|
||
│ │ - Vite/TypeScript │ │ - FastAPI/Uvicorn │ │
|
||
│ │ - Leaflet/WebGL │◄──►│ - NumPy/SciPy │ │
|
||
│ │ - Zustand stores │HTTP│ - Motor (MongoDB) │ │
|
||
│ │ - IndexedDB │ WS │ - ProcessPoolExecutor │ │
|
||
│ └─────────────────────┘ └─────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌─────────────────┼─────────────────┐
|
||
▼ ▼ ▼
|
||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||
│ SRTM │ │ OSM │ │ MongoDB │
|
||
│ Tiles │ │ Overpass │ │ │
|
||
└──────────┘ └──────────┘ └──────────┘
|
||
```
|
||
|
||
### 1.2 Complete Module Map
|
||
|
||
#### Backend (`backend/app/`)
|
||
|
||
```
|
||
backend/app/
|
||
├── main.py # FastAPI app, CORS, lifespan
|
||
├── core/
|
||
│ ├── config.py # Pydantic Settings (MONGODB_URL, TERRAIN_DIR)
|
||
│ ├── database.py # Motor async MongoDB singleton
|
||
│ ├── calculator.py # Legacy calc engine (unused)
|
||
│ ├── engine.py # Legacy RF engine (unused)
|
||
│ ├── grid.py # Legacy grid utils (unused)
|
||
│ └── result.py # Legacy result structs (unused)
|
||
├── models/
|
||
│ ├── site.py # Site/Sector Pydantic models
|
||
│ └── project.py # Project/CoverageSettings models
|
||
├── api/
|
||
│ ├── deps.py # FastAPI dependencies
|
||
│ ├── websocket.py # WebSocket progress handler
|
||
│ └── routes/
|
||
│ ├── coverage.py # POST /calculate, /preview, /link-budget (1600 lines)
|
||
│ ├── terrain.py # GET /elevation, /profile
|
||
│ ├── health.py # GET /health
|
||
│ ├── projects.py # Project CRUD
|
||
│ ├── regions.py # Region management
|
||
│ ├── system.py # System info
|
||
│ └── gpu.py # GPU detection
|
||
├── geometry/
|
||
│ ├── haversine.py # Great-circle distance
|
||
│ ├── los.py # Line-of-sight with earth curvature
|
||
│ ├── diffraction.py # ITU-R P.526 knife-edge
|
||
│ ├── intersection.py # Ray-polygon tests
|
||
│ └── reflection.py # Specular reflection math
|
||
├── propagation/
|
||
│ ├── base.py # PropagationModel ABC
|
||
│ ├── free_space.py # FSPL model (>2GHz)
|
||
│ ├── okumura_hata.py # 150-1500 MHz
|
||
│ ├── cost231_hata.py # 1500-2000 MHz (LTE)
|
||
│ ├── cost231_wi.py # Street canyon model
|
||
│ ├── itu_r_p1546.py # 30-3000 MHz field strength
|
||
│ ├── itu_r_p526.py # Knife-edge diffraction
|
||
│ └── longley_rice.py # VHF (<150 MHz)
|
||
├── services/
|
||
│ ├── coverage_service.py # MAIN ORCHESTRATOR (1630 lines)
|
||
│ ├── parallel_coverage_service.py # ProcessPool/Ray parallelism
|
||
│ ├── terrain_service.py # SRTM loading, mmap, bilinear interp
|
||
│ ├── buildings_service.py # OSM buildings fetch/cache
|
||
│ ├── los_service.py # Terrain LOS analysis
|
||
│ ├── dominant_path_service.py # BOTTLENECK: reflection/diffraction paths
|
||
│ ├── materials_service.py # Building penetration loss
|
||
│ ├── street_canyon_service.py # Urban canyon propagation
|
||
│ ├── reflection_service.py # Single/double bounce
|
||
│ ├── vegetation_service.py # Forest attenuation
|
||
│ ├── water_service.py # Water reflection gain
|
||
│ ├── weather_service.py # Rain attenuation (ITU-R P.838)
|
||
│ ├── indoor_service.py # Indoor penetration loss
|
||
│ ├── atmospheric_service.py # O2/H2O absorption
|
||
│ ├── spatial_index.py # Grid-based building index
|
||
│ ├── geometry_vectorized.py # NumPy batch geometry ops
|
||
│ ├── gpu_service.py # CuPy/NumPy abstraction
|
||
│ ├── gpu_backend.py # GPU array manager
|
||
│ ├── cache.py # Generic cache utilities
|
||
│ ├── cache_db.py # SQLite cache backend
|
||
│ ├── osm_client.py # Overpass API client
|
||
│ ├── tile_processor.py # Tiled calculation support
|
||
│ └── boundary_service.py # Coverage contour generation
|
||
└── utils/
|
||
├── logging.py # Logging config
|
||
├── progress.py # Progress reporting
|
||
└── units.py # Unit conversions
|
||
```
|
||
|
||
#### Frontend (`frontend/src/`)
|
||
|
||
```
|
||
frontend/src/
|
||
├── App.tsx # Main app, layout, state wiring
|
||
├── main.tsx # React entry point
|
||
├── index.css # Tailwind base
|
||
├── components/
|
||
│ ├── map/
|
||
│ │ ├── Map.tsx # Leaflet MapContainer
|
||
│ │ ├── SiteMarker.tsx # Site markers with sector wedges
|
||
│ │ ├── WebGLRadialCoverageLayer.tsx # GPU radial gradients (NEW)
|
||
│ │ ├── WebGLCoverageLayer.tsx # GPU texture interpolation
|
||
│ │ ├── GeographicHeatmap.tsx # Canvas fallback
|
||
│ │ ├── HeatmapTileRenderer.ts # Tile-based canvas renderer
|
||
│ │ ├── CoverageBoundary.tsx # Contour polygon
|
||
│ │ ├── ElevationLayer.tsx # Terrain visualization
|
||
│ │ ├── ElevationDisplay.tsx # Cursor elevation
|
||
│ │ ├── CoordinateGrid.tsx # Lat/lon grid overlay
|
||
│ │ ├── MeasurementTool.tsx # Distance measurement
|
||
│ │ ├── TerrainProfile.tsx # Elevation profile chart
|
||
│ │ ├── LinkBudgetOverlay.tsx # TX-RX link display
|
||
│ │ ├── HeatmapLegend.tsx # RSRP color legend
|
||
│ │ └── MapExtras.tsx # Miscellaneous overlays
|
||
│ ├── panels/
|
||
│ │ ├── SiteList.tsx # Site management (19KB)
|
||
│ │ ├── BatchEdit.tsx # Batch operations (14KB)
|
||
│ │ ├── SiteForm.tsx # Site form component
|
||
│ │ ├── LinkBudgetPanel.tsx # Link budget analysis
|
||
│ │ ├── CoverageStats.tsx # Calculation statistics
|
||
│ │ ├── ResultsPanel.tsx # Results summary
|
||
│ │ ├── HistoryPanel.tsx # Calculation history
|
||
│ │ ├── ProjectPanel.tsx # Project save/load
|
||
│ │ ├── FrequencyBandPanel.tsx # Frequency presets
|
||
│ │ ├── FrequencySelector.tsx # Frequency input
|
||
│ │ ├── BatchFrequencyChange.tsx # Batch frequency edit
|
||
│ │ ├── SiteImportExport.tsx # Import/export
|
||
│ │ └── ExportPanel.tsx # KML/GeoJSON export
|
||
│ ├── modals/
|
||
│ │ ├── SiteConfigModal.tsx # Create/edit site dialog
|
||
│ │ ├── ModalBackdrop.tsx # Modal backdrop
|
||
│ │ └── index.ts # Modal exports
|
||
│ └── ui/
|
||
│ ├── Button.tsx # Styled button
|
||
│ ├── Input.tsx # Text input
|
||
│ ├── NumberInput.tsx # Numeric input
|
||
│ ├── Toast.tsx # Toast notifications
|
||
│ ├── ThemeToggle.tsx # Dark/light mode
|
||
│ ├── GPUIndicator.tsx # GPU status
|
||
│ └── ConfirmDialog.tsx # Confirmation modal
|
||
├── store/
|
||
│ ├── sites.ts # Site CRUD + IndexedDB
|
||
│ ├── coverage.ts # Coverage calculation state
|
||
│ ├── settings.ts # UI/map preferences
|
||
│ ├── history.ts # Undo/redo stack
|
||
│ ├── projects.ts # Project management
|
||
│ ├── calcHistory.ts # Calculation history
|
||
│ └── tools.ts # Active tool state
|
||
├── services/
|
||
│ ├── api.ts # HTTP API client
|
||
│ ├── websocket.ts # WebSocket client
|
||
│ └── terrain.ts # Terrain utilities
|
||
├── hooks/
|
||
│ ├── useWebSocket.ts # WS connection hook
|
||
│ ├── useKeyboardShortcuts.ts # Keyboard shortcuts
|
||
│ ├── useElevation.ts # Elevation queries
|
||
│ └── useUnsavedChanges.ts # Unsaved changes warning
|
||
├── types/
|
||
│ ├── site.ts # Site interfaces
|
||
│ ├── coverage.ts # Coverage interfaces
|
||
│ └── frequency.ts # Frequency types
|
||
├── rf/
|
||
│ ├── fspl.ts # Free-space path loss
|
||
│ ├── antenna-pattern.ts # Antenna gain patterns
|
||
│ ├── calculator.ts # Client-side RF utils
|
||
│ └── utils.ts # RF helpers
|
||
├── db/
|
||
│ └── schema.ts # Dexie IndexedDB schema
|
||
├── lib/
|
||
│ └── desktop.ts # Electron IPC bridge
|
||
├── constants/
|
||
│ ├── frequencies.ts # Frequency band presets
|
||
│ └── rsrp-thresholds.ts # Signal thresholds
|
||
└── utils/
|
||
├── logger.ts # Logging utility
|
||
├── geographicScale.ts # Geographic calculations
|
||
└── colorGradient.ts # Color utilities
|
||
```
|
||
|
||
#### Desktop (`desktop/`)
|
||
|
||
```
|
||
desktop/
|
||
├── main.js # Electron main process
|
||
├── preload.js # IPC bridge
|
||
├── splash.html # Loading screen
|
||
├── package.json # Electron-builder config
|
||
└── assets/
|
||
├── icon.ico # Windows icon
|
||
├── icon.png # Linux icon
|
||
└── icon.icns # macOS icon
|
||
```
|
||
|
||
### 1.3 Data Flow Diagram
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────────┐
|
||
│ Coverage Calculation Pipeline │
|
||
└──────────────────────────────────────────────────────────────────────────────┘
|
||
|
||
User clicks "Calculate"
|
||
│
|
||
▼
|
||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||
│ Frontend │ │ WebSocket or │ │ Backend │
|
||
│ useCoverageStore│────►│ HTTP POST │────►│ /api/coverage/ │
|
||
│ .calculate() │ │ │ │ calculate │
|
||
└─────────────────┘ └─────────────────┘ └────────┬────────┘
|
||
│
|
||
┌────────────────────────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ CoverageService.calculate() │
|
||
│ │
|
||
│ 1. apply_preset() ─────────────────────────┐ │
|
||
│ │ │
|
||
│ 2. _fetch_osm_grid_aligned() │ │
|
||
│ ├── buildings_service.fetch_buildings() │ │
|
||
│ ├── street_canyon_service.fetch_streets()│ │
|
||
│ ├── water_service.fetch_water_bodies() │ │
|
||
│ └── vegetation_service.fetch_areas() │ │
|
||
│ │ │
|
||
│ 3. terrain_service.ensure_tiles() │ │
|
||
│ │ │
|
||
│ 4. GPU precomputation (if available) │ │
|
||
│ ├── precompute_distances() │ │
|
||
│ ├── precompute_path_loss() │ │
|
||
│ ├── batch_terrain_los() │ │
|
||
│ └── batch_antenna_pattern() │ │
|
||
│ │ │
|
||
│ 5. Point loop (parallel or sequential) │ │
|
||
│ │ │ │
|
||
│ │ For each grid point: │ │
|
||
│ │ ┌─────────────────────────────────┐ │ │
|
||
│ │ │ a. Use precomputed distance │ │ │
|
||
│ │ │ b. Use precomputed path_loss │ │ │
|
||
│ │ │ c. Use precomputed antenna_loss │ │ │
|
||
│ │ │ d. Building LOS check │◄──┼──┼── BOTTLENECK
|
||
│ │ │ e. Material penetration loss │ │ │
|
||
│ │ │ f. Dominant path analysis │◄──┼──┼── BOTTLENECK
|
||
│ │ │ g. Street canyon loss │ │ │
|
||
│ │ │ h. Vegetation attenuation │ │ │
|
||
│ │ │ i. Reflection paths │ │ │
|
||
│ │ │ j. Water reflection │ │ │
|
||
│ │ │ k. Rain attenuation │ │ │
|
||
│ │ │ l. Indoor penetration │ │ │
|
||
│ │ │ m. Atmospheric loss │ │ │
|
||
│ │ │ n. Combine all losses │ │ │
|
||
│ │ │ o. Final RSRP │ │ │
|
||
│ │ └─────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ └── Parallel: ProcessPoolExecutor │ │
|
||
│ - 4-6 workers │ │
|
||
│ - Data pickled per worker │ │
|
||
│ - ~100MB pickle overhead each │ │
|
||
│ │ │
|
||
│ 6. Collect results, compute stats │ │
|
||
└──────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ CoverageResult │
|
||
│ - points: CoveragePoint[] (lat, lon, rsrp, ...) │
|
||
│ - stats: min/max/avg, LOS%, model distribution │
|
||
│ - computation_time, models_used │
|
||
│ - boundary: contour polygon │
|
||
└────────────────────────┬────────────────────────┘
|
||
│
|
||
┌─────────────┘
|
||
▼
|
||
┌─────────────────────────────────────────────────┐
|
||
│ WebGL Rendering │
|
||
│ │
|
||
│ WebGLRadialCoverageLayer (preferred): │
|
||
│ - Pass 1: Point accumulation (Gaussian radial) │
|
||
│ - Pass 2: Normalize + colormap │
|
||
│ - Instanced rendering: 1 draw call for all pts │
|
||
│ │
|
||
│ WebGLCoverageLayer (texture): │
|
||
│ - Grid to texture │
|
||
│ - Quintic Hermite interpolation │
|
||
│ - Single-pass colormap │
|
||
└─────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.4 Performance Bottleneck Analysis
|
||
|
||
#### Critical Bottlenecks (Line-Level Detail)
|
||
|
||
**1. Dominant Path Service (`dominant_path_service.py:155-350`)**
|
||
|
||
The `find_dominant_paths_vectorized()` function is the #1 bottleneck:
|
||
|
||
```python
|
||
# Line 208-214: Building filtering per point
|
||
line_buildings = _filter_buildings_by_distance(
|
||
line_buildings,
|
||
(tx_lat, tx_lon), (rx_lat, rx_lon),
|
||
max_count=MAX_BUILDINGS_FOR_LINE, # 30
|
||
max_distance=MAX_DISTANCE_FROM_PATH, # 200m
|
||
)
|
||
|
||
# Line 230-234: Building to array conversion (allocates new arrays per point!)
|
||
walls_start, walls_end, wall_to_bldg, poly_x, poly_y, poly_lengths = (
|
||
_buildings_to_arrays(line_buildings, ref_lat, ref_lon)
|
||
)
|
||
|
||
# Line 300-307: Vectorized reflection search (still expensive)
|
||
refl_point, refl_length, refl_loss = find_best_reflection_path_vectorized(
|
||
tx, rx, r_walls_start, r_walls_end, r_wall_to_bldg,
|
||
r_poly_x, r_poly_y, r_poly_lengths,
|
||
max_candidates=30, max_walls=100, max_los_checks=5,
|
||
)
|
||
```
|
||
|
||
**Timing breakdown (from logs):**
|
||
- Building query: 0.5-2ms per point
|
||
- Array conversion: 1-5ms per point
|
||
- LOS polygon test: 0.5-2ms per point
|
||
- Reflection search: 5-20ms per point (when LOS blocked)
|
||
- **Total: 10-30ms per point with buildings**
|
||
|
||
For 6,700 points (50m resolution, 10km): **67-200 seconds just for dominant path**
|
||
|
||
**2. ProcessPoolExecutor Overhead (`parallel_coverage_service.py`)**
|
||
|
||
```python
|
||
# Each worker receives a pickled copy of:
|
||
# - buildings list: 10-50MB (5000+ buildings with geometry)
|
||
# - terrain cache: Referenced but requires re-mmap
|
||
# - vegetation areas: 1-10MB
|
||
# - water bodies: 1-5MB
|
||
|
||
# Line 180-195: Data serialization
|
||
chunk_data = {
|
||
'grid_chunk': grid_points[start:end],
|
||
'site_params': site_params,
|
||
'settings': settings,
|
||
'buildings': buildings, # FULL COPY
|
||
'streets': streets,
|
||
'water_bodies': water_bodies,
|
||
'vegetation_areas': vegetation_areas,
|
||
}
|
||
```
|
||
|
||
**Memory impact:** 4 workers × 100MB pickle = 400MB overhead just for parallelism
|
||
|
||
**3. Terrain Service (`terrain_service.py:180-232`)**
|
||
|
||
Bilinear interpolation is called per point:
|
||
```python
|
||
def _bilinear_sample(self, tile: np.ndarray, lat: float, lon: float) -> float:
|
||
# 20+ float operations per sample
|
||
# Not vectorized - called in Python loop
|
||
```
|
||
|
||
For terrain LOS checks: 50 samples × 6,700 points = 335,000 calls
|
||
|
||
**4. Building Intersection (`dominant_path_service.py:674-690`)**
|
||
|
||
```python
|
||
def _line_intersects_building_3d(self, ...):
|
||
# Line 682: 20 samples per line segment
|
||
for t in np.linspace(0, 1, 20):
|
||
# Point-in-polygon test per sample
|
||
if buildings_service.point_in_building(lat, lon, building):
|
||
if height < building.height:
|
||
return True
|
||
```
|
||
|
||
**5. Python GIL and Async Overhead**
|
||
|
||
- ProcessPoolExecutor requires pickling all data
|
||
- asyncio event loop adds latency for every await
|
||
- NumPy releases GIL but building intersection code doesn't
|
||
|
||
### 1.5 Memory Usage Patterns
|
||
|
||
| Component | Memory (10km) | Memory (50km) | Notes |
|
||
|-----------|--------------|---------------|-------|
|
||
| Grid points | 2MB | 50MB | 6,700 → 167,000 points |
|
||
| SRTM tiles | 25-100MB | 100-400MB | 1-16 tiles × 25MB |
|
||
| Buildings | 10-100MB | 50-500MB | Geometry arrays |
|
||
| Worker copies | 100-400MB | 400-2GB | Pickle per worker |
|
||
| GPU buffers | 50-200MB | 200-800MB | CuPy arrays |
|
||
| **Total** | **200-800MB** | **800MB-4GB** | OOM likely at 50km |
|
||
|
||
---
|
||
|
||
## Part 2: Target Architecture
|
||
|
||
### 2.1 Rust Backend Structure
|
||
|
||
```
|
||
src-tauri/
|
||
├── Cargo.toml # Workspace + deps
|
||
├── src/
|
||
│ ├── main.rs # Tauri app entry
|
||
│ ├── lib.rs # Library exports
|
||
│ ├── commands/ # Tauri IPC commands
|
||
│ │ ├── mod.rs
|
||
│ │ ├── coverage.rs # #[tauri::command] calculate_coverage
|
||
│ │ ├── terrain.rs # Elevation queries
|
||
│ │ ├── projects.rs # Project CRUD
|
||
│ │ └── system.rs # System info, GPU detection
|
||
│ │
|
||
│ ├── coverage/ # Coverage calculation engine
|
||
│ │ ├── mod.rs
|
||
│ │ ├── engine.rs # Main calculation orchestrator
|
||
│ │ ├── grid.rs # Grid generation (adaptive)
|
||
│ │ ├── point.rs # Per-point calculation
|
||
│ │ ├── precompute.rs # Vectorized precomputation
|
||
│ │ └── result.rs # CoveragePoint, CoverageResult
|
||
│ │
|
||
│ ├── propagation/ # RF propagation models
|
||
│ │ ├── mod.rs
|
||
│ │ ├── traits.rs # PropagationModel trait
|
||
│ │ ├── free_space.rs # FSPL
|
||
│ │ ├── okumura_hata.rs # 150-1500 MHz
|
||
│ │ ├── cost231_hata.rs # 1500-2000 MHz
|
||
│ │ ├── cost231_wi.rs # Street canyon
|
||
│ │ ├── itu_r_p1546.rs # Field strength model
|
||
│ │ ├── longley_rice.rs # VHF
|
||
│ │ └── knife_edge.rs # ITU-R P.526 diffraction
|
||
│ │
|
||
│ ├── geometry/ # Geometry operations
|
||
│ │ ├── mod.rs
|
||
│ │ ├── haversine.rs # Great-circle distance (SIMD)
|
||
│ │ ├── los.rs # Line-of-sight (vectorized)
|
||
│ │ ├── intersection.rs # Ray-polygon (SIMD)
|
||
│ │ ├── reflection.rs # Specular reflection
|
||
│ │ └── spatial_index.rs # R-tree or grid index
|
||
│ │
|
||
│ ├── terrain/ # Terrain data management
|
||
│ │ ├── mod.rs
|
||
│ │ ├── srtm.rs # SRTM tile loading (mmap)
|
||
│ │ ├── cache.rs # LRU tile cache
|
||
│ │ ├── sample.rs # Bilinear interpolation (SIMD)
|
||
│ │ └── profile.rs # Elevation profiles
|
||
│ │
|
||
│ ├── osm/ # OpenStreetMap data
|
||
│ │ ├── mod.rs
|
||
│ │ ├── client.rs # Overpass API client
|
||
│ │ ├── building.rs # Building struct + parsing
|
||
│ │ ├── cache.rs # SQLite OSM cache
|
||
│ │ └── spatial.rs # Spatial queries
|
||
│ │
|
||
│ ├── environment/ # Environmental effects
|
||
│ │ ├── mod.rs
|
||
│ │ ├── materials.rs # Building materials/penetration
|
||
│ │ ├── vegetation.rs # Forest attenuation
|
||
│ │ ├── water.rs # Water reflection
|
||
│ │ ├── weather.rs # Rain attenuation
|
||
│ │ ├── indoor.rs # Indoor penetration
|
||
│ │ └── atmospheric.rs # O2/H2O absorption
|
||
│ │
|
||
│ ├── gpu/ # GPU compute (optional)
|
||
│ │ ├── mod.rs
|
||
│ │ ├── wgpu_backend.rs # wgpu compute shaders
|
||
│ │ ├── shaders/
|
||
│ │ │ ├── propagation.wgsl # Path loss compute
|
||
│ │ │ ├── terrain_los.wgsl # Batch LOS check
|
||
│ │ │ └── antenna.wgsl # Antenna pattern
|
||
│ │ └── fallback.rs # CPU SIMD fallback
|
||
│ │
|
||
│ ├── db/ # Data persistence
|
||
│ │ ├── mod.rs
|
||
│ │ ├── sqlite.rs # SQLite for projects/cache
|
||
│ │ └── schema.rs # Database schema
|
||
│ │
|
||
│ └── utils/
|
||
│ ├── mod.rs
|
||
│ ├── progress.rs # Progress reporting
|
||
│ └── units.rs # Unit conversions
|
||
│
|
||
└── build.rs # Build script (shader compilation)
|
||
```
|
||
|
||
### 2.2 SvelteKit Frontend Structure
|
||
|
||
```
|
||
src/
|
||
├── app.html # HTML template
|
||
├── app.css # Global styles (Tailwind)
|
||
├── routes/
|
||
│ ├── +layout.svelte # Root layout
|
||
│ └── +page.svelte # Main app page
|
||
│
|
||
├── lib/
|
||
│ ├── components/
|
||
│ │ ├── map/
|
||
│ │ │ ├── MapContainer.svelte # Leaflet wrapper
|
||
│ │ │ ├── SiteMarker.svelte # Site markers
|
||
│ │ │ ├── CoverageLayer.svelte # WebGL coverage (port shaders)
|
||
│ │ │ ├── ElevationLayer.svelte
|
||
│ │ │ ├── TerrainProfile.svelte
|
||
│ │ │ ├── MeasurementTool.svelte
|
||
│ │ │ └── Legend.svelte
|
||
│ │ │
|
||
│ │ ├── panels/
|
||
│ │ │ ├── SiteList.svelte # Site management
|
||
│ │ │ ├── BatchEdit.svelte # Batch operations
|
||
│ │ │ ├── Settings.svelte # Coverage settings
|
||
│ │ │ ├── LinkBudget.svelte # Link budget analysis
|
||
│ │ │ ├── History.svelte # Calculation history
|
||
│ │ │ └── Export.svelte # Export options
|
||
│ │ │
|
||
│ │ ├── modals/
|
||
│ │ │ ├── SiteConfig.svelte # Site edit modal
|
||
│ │ │ └── Confirm.svelte # Confirmation dialog
|
||
│ │ │
|
||
│ │ └── ui/
|
||
│ │ ├── Button.svelte
|
||
│ │ ├── Input.svelte
|
||
│ │ ├── Select.svelte
|
||
│ │ ├── Toast.svelte
|
||
│ │ └── ThemeToggle.svelte
|
||
│ │
|
||
│ ├── stores/
|
||
│ │ ├── sites.ts # Site state (Svelte stores)
|
||
│ │ ├── coverage.ts # Coverage state
|
||
│ │ ├── settings.ts # UI settings
|
||
│ │ ├── history.ts # Undo/redo
|
||
│ │ └── projects.ts # Project state
|
||
│ │
|
||
│ ├── tauri/
|
||
│ │ ├── commands.ts # Tauri command wrappers
|
||
│ │ ├── events.ts # Event listeners
|
||
│ │ └── types.ts # Shared types
|
||
│ │
|
||
│ ├── webgl/
|
||
│ │ ├── radial-layer.ts # WebGL radial gradients (port)
|
||
│ │ ├── texture-layer.ts # WebGL texture (port)
|
||
│ │ └── shaders.ts # GLSL shaders (reuse)
|
||
│ │
|
||
│ ├── rf/
|
||
│ │ ├── fspl.ts # Client-side FSPL
|
||
│ │ └── antenna.ts # Antenna patterns
|
||
│ │
|
||
│ └── utils/
|
||
│ ├── colors.ts # Color utilities
|
||
│ └── geo.ts # Geographic helpers
|
||
│
|
||
├── static/
|
||
│ └── icons/ # App icons
|
||
│
|
||
└── tests/
|
||
└── ...
|
||
```
|
||
|
||
### 2.3 Tauri IPC Design
|
||
|
||
#### Commands (Rust → JS)
|
||
|
||
```rust
|
||
// src-tauri/src/commands/coverage.rs
|
||
|
||
#[tauri::command]
|
||
async fn calculate_coverage(
|
||
app: AppHandle,
|
||
sites: Vec<SiteParams>,
|
||
settings: CoverageSettings,
|
||
) -> Result<CoverageResult, String> {
|
||
// Spawns calculation on Rayon thread pool
|
||
// Emits progress events via app.emit_all()
|
||
}
|
||
|
||
#[tauri::command]
|
||
fn cancel_coverage(app: AppHandle) -> Result<(), String> {
|
||
// Sets cancellation flag
|
||
}
|
||
|
||
#[tauri::command]
|
||
async fn get_elevation(lat: f64, lon: f64) -> Result<f64, String> {
|
||
// Single point elevation
|
||
}
|
||
|
||
#[tauri::command]
|
||
async fn get_elevation_profile(
|
||
lat1: f64, lon1: f64,
|
||
lat2: f64, lon2: f64,
|
||
num_points: u32,
|
||
) -> Result<Vec<ElevationPoint>, String> {
|
||
// Terrain profile
|
||
}
|
||
|
||
#[tauri::command]
|
||
async fn fetch_osm_data(
|
||
min_lat: f64, min_lon: f64,
|
||
max_lat: f64, max_lon: f64,
|
||
layers: Vec<String>,
|
||
) -> Result<OsmData, String> {
|
||
// Pre-fetch buildings, water, vegetation
|
||
}
|
||
```
|
||
|
||
#### Events (JS → Rust progress updates)
|
||
|
||
```typescript
|
||
// src/lib/tauri/events.ts
|
||
|
||
import { listen } from '@tauri-apps/api/event';
|
||
|
||
interface ProgressPayload {
|
||
phase: string;
|
||
progress: number; // 0.0-1.0
|
||
eta_seconds?: number;
|
||
tile?: { current: number; total: number };
|
||
}
|
||
|
||
interface PartialResultPayload {
|
||
points: CoveragePoint[];
|
||
tile_index: number;
|
||
}
|
||
|
||
export function listenCoverageProgress(
|
||
onProgress: (p: ProgressPayload) => void,
|
||
onPartial: (p: PartialResultPayload) => void,
|
||
) {
|
||
const unlisten1 = listen<ProgressPayload>('coverage:progress', e => onProgress(e.payload));
|
||
const unlisten2 = listen<PartialResultPayload>('coverage:partial', e => onPartial(e.payload));
|
||
return () => { unlisten1.then(f => f()); unlisten2.then(f => f()); };
|
||
}
|
||
```
|
||
|
||
#### Data Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ SvelteKit Frontend │
|
||
│ │
|
||
│ coverageStore.calculate() │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ invoke('calculate_coverage', { sites, settings }) │
|
||
│ │ │
|
||
│ listen('coverage:progress', updateProgress) │
|
||
│ listen('coverage:partial', addPartialPoints) │
|
||
│ │ │
|
||
└────────┼─────────────────────────────────────────────────────┘
|
||
│ Tauri IPC (JSON serialization, ~1ms)
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Rust Backend │
|
||
│ │
|
||
│ #[tauri::command] │
|
||
│ async fn calculate_coverage(...) { │
|
||
│ // Spawn on Rayon thread pool │
|
||
│ rayon::spawn(move || { │
|
||
│ // Parallel grid processing │
|
||
│ grid_points.par_chunks(256) │
|
||
│ .for_each(|chunk| { │
|
||
│ // SIMD-optimized point calculation │
|
||
│ let results = calculate_chunk(chunk); │
|
||
│ // Emit partial results │
|
||
│ app.emit_all("coverage:partial", results); │
|
||
│ }); │
|
||
│ }); │
|
||
│ } │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.4 Data Management Strategy
|
||
|
||
#### Terrain (SRTM)
|
||
|
||
```rust
|
||
// Memory-mapped tiles with zero-copy access
|
||
pub struct TerrainManager {
|
||
tiles: HashMap<TileKey, Mmap>, // Direct mmap, no copy
|
||
lru: LruCache<TileKey, ()>, // LRU tracking only
|
||
}
|
||
|
||
impl TerrainManager {
|
||
pub fn get_elevation(&self, lat: f64, lon: f64) -> f32 {
|
||
let tile = self.get_tile(lat, lon)?;
|
||
// Direct pointer arithmetic on mmap'd data
|
||
// SIMD-optimized bilinear interpolation
|
||
simd_bilinear_sample(tile.as_ptr(), lat, lon)
|
||
}
|
||
|
||
pub fn get_elevations_batch(&self, coords: &[(f64, f64)]) -> Vec<f32> {
|
||
// Process 8 points at a time with AVX2
|
||
coords.par_chunks(8)
|
||
.flat_map(|chunk| simd_batch_sample(chunk))
|
||
.collect()
|
||
}
|
||
}
|
||
```
|
||
|
||
#### OSM Data
|
||
|
||
```rust
|
||
// SQLite cache with FlatBuffers serialization
|
||
pub struct OsmCache {
|
||
db: Connection, // rusqlite
|
||
}
|
||
|
||
impl OsmCache {
|
||
pub fn get_buildings(&self, bbox: BBox) -> Vec<Building> {
|
||
// Query by spatial index
|
||
// Deserialize with FlatBuffers (zero-copy)
|
||
}
|
||
|
||
pub fn get_or_fetch(&self, bbox: BBox) -> Vec<Building> {
|
||
if let Some(cached) = self.get_buildings(bbox) {
|
||
return cached;
|
||
}
|
||
// Fetch from Overpass, cache, return
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Projects
|
||
|
||
```rust
|
||
// SQLite for projects (simple, portable)
|
||
pub struct ProjectStore {
|
||
db: Connection,
|
||
}
|
||
|
||
// Schema
|
||
CREATE TABLE projects (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
data BLOB, -- MessagePack serialized
|
||
created_at INTEGER,
|
||
updated_at INTEGER
|
||
);
|
||
|
||
CREATE TABLE sites (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT,
|
||
data BLOB,
|
||
FOREIGN KEY (project_id) REFERENCES projects(id)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## Part 3: Migration Phases
|
||
|
||
### Phase 1: Tauri + SvelteKit Shell (Replace Electron)
|
||
|
||
**Goal:** Working desktop app with same UI, backend still Python
|
||
|
||
**Duration:** 2-3 weeks
|
||
|
||
#### Tasks
|
||
|
||
1. **Initialize Tauri project**
|
||
```bash
|
||
npm create tauri-app@latest rfcp-tauri -- --template svelte-ts
|
||
```
|
||
|
||
2. **Port UI components to Svelte**
|
||
- Start with layout (App.tsx → +page.svelte)
|
||
- Port UI components (Button, Input, etc.)
|
||
- Port map components (reuse Leaflet)
|
||
- Port panels (SiteList, Settings, etc.)
|
||
|
||
3. **Port WebGL layers**
|
||
- Copy GLSL shaders verbatim
|
||
- Adapt TypeScript to Svelte (minimal changes)
|
||
- WebGLRadialCoverageLayer → radial-layer.ts
|
||
|
||
4. **Implement Tauri commands as HTTP proxies**
|
||
```rust
|
||
#[tauri::command]
|
||
async fn calculate_coverage(...) -> Result<Value, String> {
|
||
// Forward to Python backend via HTTP
|
||
let response = reqwest::get("http://localhost:8090/api/coverage/calculate")
|
||
.await?;
|
||
Ok(response.json().await?)
|
||
}
|
||
```
|
||
|
||
5. **Port state management**
|
||
- Zustand → Svelte stores
|
||
- Keep same persistence (IndexedDB via idb)
|
||
|
||
6. **Test parity**
|
||
- All features working
|
||
- Same performance as Electron version
|
||
|
||
**Deliverable:** RFCP desktop app on Tauri, Python backend, ~20MB bundle
|
||
|
||
---
|
||
|
||
### Phase 2: Rust Propagation Engine (The Big Win)
|
||
|
||
**Goal:** RF calculations in Rust, 50-100x faster for geometry-heavy operations
|
||
|
||
**Duration:** 4-6 weeks
|
||
|
||
#### Tasks
|
||
|
||
1. **Core geometry in Rust**
|
||
```rust
|
||
// Haversine with SIMD (8 distances per instruction)
|
||
pub fn haversine_batch(
|
||
site: (f64, f64),
|
||
points: &[(f64, f64)],
|
||
) -> Vec<f64> {
|
||
points.par_chunks(8)
|
||
.flat_map(|chunk| simd_haversine(site, chunk))
|
||
.collect()
|
||
}
|
||
|
||
// Line-polygon intersection with SIMD
|
||
pub fn line_intersects_polygon_batch(
|
||
line: (Point, Point),
|
||
polygons: &[Polygon],
|
||
) -> Vec<bool> {
|
||
// Vectorized edge tests
|
||
}
|
||
```
|
||
|
||
2. **Propagation models**
|
||
```rust
|
||
pub trait PropagationModel: Send + Sync {
|
||
fn calculate(&self, input: &PropagationInput) -> PropagationOutput;
|
||
fn calculate_batch(&self, inputs: &[PropagationInput]) -> Vec<PropagationOutput>;
|
||
}
|
||
|
||
// FSPL: trivial, ~1 nanosecond per point
|
||
// Okumura-Hata: ~10 nanoseconds per point
|
||
// COST-231: ~15 nanoseconds per point
|
||
```
|
||
|
||
3. **Dominant path in Rust**
|
||
```rust
|
||
pub fn find_dominant_path(
|
||
tx: Point3D,
|
||
rx: Point3D,
|
||
buildings: &SpatialIndex<Building>,
|
||
) -> DominantPath {
|
||
// R-tree query: O(log n)
|
||
let nearby = buildings.query_line(tx.to_2d(), rx.to_2d());
|
||
|
||
// SIMD polygon tests
|
||
let hits = simd_line_polygon_batch(&nearby, tx, rx);
|
||
|
||
// Vectorized reflection search
|
||
if !hits.is_empty() {
|
||
find_best_reflection_simd(&nearby, tx, rx)
|
||
} else {
|
||
DominantPath::Direct
|
||
}
|
||
}
|
||
```
|
||
|
||
4. **Parallel grid processing**
|
||
```rust
|
||
pub fn calculate_coverage(
|
||
sites: &[Site],
|
||
settings: &CoverageSettings,
|
||
on_progress: impl Fn(f32),
|
||
) -> CoverageResult {
|
||
let grid = generate_grid(sites, settings);
|
||
|
||
// Rayon parallel iteration
|
||
let points: Vec<CoveragePoint> = grid
|
||
.par_iter()
|
||
.enumerate()
|
||
.map(|(i, coord)| {
|
||
if i % 1000 == 0 {
|
||
on_progress(i as f32 / grid.len() as f32);
|
||
}
|
||
calculate_point(coord, sites, settings)
|
||
})
|
||
.collect();
|
||
|
||
CoverageResult { points, stats: compute_stats(&points) }
|
||
}
|
||
```
|
||
|
||
5. **Benchmark against Python**
|
||
- Target: 10x faster for basic (terrain only)
|
||
- Target: 50x faster for full (buildings + dominant path)
|
||
|
||
**Deliverable:** Rust coverage engine, 50km calculations possible
|
||
|
||
---
|
||
|
||
### Phase 3: Rust Terrain/OSM Services (Offline-First)
|
||
|
||
**Goal:** Complete data layer in Rust, no Python dependency
|
||
|
||
**Duration:** 3-4 weeks
|
||
|
||
#### Tasks
|
||
|
||
1. **Terrain service**
|
||
```rust
|
||
pub struct TerrainService {
|
||
data_dir: PathBuf,
|
||
tiles: RwLock<HashMap<TileKey, Mmap>>,
|
||
download_queue: Mutex<VecDeque<TileKey>>,
|
||
}
|
||
|
||
impl TerrainService {
|
||
pub async fn ensure_tiles(&self, bbox: BBox) -> Result<()> {
|
||
let needed = self.get_needed_tiles(bbox);
|
||
for tile in needed {
|
||
if !self.has_tile(tile) {
|
||
self.download_tile(tile).await?;
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
```
|
||
|
||
2. **OSM client with caching**
|
||
```rust
|
||
pub struct OsmService {
|
||
client: reqwest::Client,
|
||
cache: SqliteCache,
|
||
}
|
||
|
||
impl OsmService {
|
||
pub async fn get_buildings(&self, bbox: BBox) -> Vec<Building> {
|
||
if let Some(cached) = self.cache.get_buildings(bbox) {
|
||
return cached;
|
||
}
|
||
|
||
let response = self.client
|
||
.post("https://overpass-api.de/api/interpreter")
|
||
.body(overpass_query(bbox, "building"))
|
||
.send()
|
||
.await?;
|
||
|
||
let buildings = parse_overpass_response(response);
|
||
self.cache.store_buildings(bbox, &buildings);
|
||
buildings
|
||
}
|
||
}
|
||
```
|
||
|
||
3. **Spatial indexing**
|
||
```rust
|
||
// R-tree for fast spatial queries
|
||
pub struct BuildingIndex {
|
||
rtree: RTree<BuildingRef>,
|
||
}
|
||
|
||
impl BuildingIndex {
|
||
pub fn query_line(&self, p1: Point, p2: Point) -> Vec<&Building> {
|
||
let envelope = LineString::new(vec![p1, p2]).envelope();
|
||
self.rtree
|
||
.locate_in_envelope(&envelope)
|
||
.filter(|b| b.geometry.intersects_line(p1, p2))
|
||
.collect()
|
||
}
|
||
}
|
||
```
|
||
|
||
4. **Project persistence**
|
||
- SQLite database for projects
|
||
- Binary serialization (MessagePack or bincode)
|
||
|
||
5. **Remove Python backend**
|
||
- All functionality in Rust
|
||
- Single executable
|
||
|
||
**Deliverable:** Self-contained Rust app, no Python, ~10MB bundle
|
||
|
||
---
|
||
|
||
### Phase 4: Advanced Features (GPU Compute, 50km+)
|
||
|
||
**Goal:** Handle massive calculations with GPU acceleration
|
||
|
||
**Duration:** 4-6 weeks
|
||
|
||
#### Tasks
|
||
|
||
1. **wgpu compute shaders**
|
||
```wgsl
|
||
// propagation.wgsl
|
||
@group(0) @binding(0) var<storage, read> grid_points: array<vec2<f32>>;
|
||
@group(0) @binding(1) var<storage, read> site: vec4<f32>; // lat, lon, height, freq
|
||
@group(0) @binding(2) var<storage, read_write> path_loss: array<f32>;
|
||
|
||
@compute @workgroup_size(256)
|
||
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
||
let idx = id.x;
|
||
if (idx >= arrayLength(&grid_points)) { return; }
|
||
|
||
let point = grid_points[idx];
|
||
let dist = haversine(site.xy, point);
|
||
|
||
// COST-231 Hata
|
||
let freq = site.w;
|
||
let height = site.z;
|
||
path_loss[idx] = cost231_hata(dist, freq, height);
|
||
}
|
||
```
|
||
|
||
2. **Tiled GPU processing**
|
||
```rust
|
||
pub async fn calculate_coverage_gpu(
|
||
sites: &[Site],
|
||
settings: &CoverageSettings,
|
||
gpu: &GpuContext,
|
||
) -> CoverageResult {
|
||
let grid = generate_grid(sites, settings);
|
||
|
||
// Process in tiles that fit in GPU memory
|
||
let tile_size = 50_000; // 50k points per tile
|
||
let mut all_points = Vec::new();
|
||
|
||
for tile in grid.chunks(tile_size) {
|
||
// Upload to GPU
|
||
let grid_buffer = gpu.create_buffer(tile);
|
||
|
||
// Dispatch compute shader
|
||
gpu.dispatch_propagation(grid_buffer, sites);
|
||
gpu.dispatch_terrain_los(grid_buffer, terrain);
|
||
|
||
// Read back results
|
||
let results = gpu.read_buffer(grid_buffer);
|
||
all_points.extend(results);
|
||
}
|
||
|
||
CoverageResult { points: all_points, ... }
|
||
}
|
||
```
|
||
|
||
3. **Streaming results**
|
||
- Emit partial results per tile
|
||
- Progressive rendering in UI
|
||
|
||
4. **Memory-mapped result caching**
|
||
- Cache calculation results
|
||
- Instant re-render on settings change
|
||
|
||
**Deliverable:** 50km+ calculations, GPU-accelerated, <30 second full preset
|
||
|
||
---
|
||
|
||
## Part 4: Component Migration Map
|
||
|
||
### Backend Services
|
||
|
||
| Current File | Action | Target | Notes |
|
||
|-------------|--------|--------|-------|
|
||
| `coverage_service.py` | REWRITE | `src-tauri/src/coverage/engine.rs` | Rayon parallelism, SIMD geometry |
|
||
| `parallel_coverage_service.py` | DELETE | N/A | Replaced by Rayon |
|
||
| `terrain_service.py` | REWRITE | `src-tauri/src/terrain/srtm.rs` | mmap + SIMD bilinear |
|
||
| `buildings_service.py` | REWRITE | `src-tauri/src/osm/building.rs` | R-tree index |
|
||
| `los_service.py` | REWRITE | `src-tauri/src/geometry/los.rs` | Vectorized LOS |
|
||
| `dominant_path_service.py` | REWRITE | `src-tauri/src/coverage/dominant_path.rs` | SIMD intersection tests |
|
||
| `materials_service.py` | REWRITE | `src-tauri/src/environment/materials.rs` | Direct port |
|
||
| `street_canyon_service.py` | REWRITE | `src-tauri/src/environment/street_canyon.rs` | Direct port |
|
||
| `reflection_service.py` | REWRITE | `src-tauri/src/geometry/reflection.rs` | SIMD specular calc |
|
||
| `vegetation_service.py` | REWRITE | `src-tauri/src/environment/vegetation.rs` | Direct port |
|
||
| `water_service.py` | REWRITE | `src-tauri/src/environment/water.rs` | Direct port |
|
||
| `weather_service.py` | REWRITE | `src-tauri/src/environment/weather.rs` | Direct port |
|
||
| `indoor_service.py` | REWRITE | `src-tauri/src/environment/indoor.rs` | Direct port (trivial) |
|
||
| `atmospheric_service.py` | REWRITE | `src-tauri/src/environment/atmospheric.rs` | Direct port |
|
||
| `spatial_index.py` | REWRITE | `src-tauri/src/geometry/spatial_index.rs` | Use `rstar` crate |
|
||
| `geometry_vectorized.py` | REWRITE | `src-tauri/src/geometry/intersection.rs` | SIMD with `packed_simd2` |
|
||
| `gpu_service.py` | REWRITE | `src-tauri/src/gpu/wgpu_backend.rs` | wgpu compute shaders |
|
||
| `osm_client.py` | REWRITE | `src-tauri/src/osm/client.rs` | reqwest + serde |
|
||
| `cache.py` | REWRITE | `src-tauri/src/db/sqlite.rs` | rusqlite |
|
||
|
||
### Propagation Models
|
||
|
||
| Current File | Action | Target | Notes |
|
||
|-------------|--------|--------|-------|
|
||
| `free_space.py` | REWRITE | `src-tauri/src/propagation/free_space.rs` | Trivial port |
|
||
| `okumura_hata.py` | REWRITE | `src-tauri/src/propagation/okumura_hata.rs` | Same formulas |
|
||
| `cost231_hata.py` | REWRITE | `src-tauri/src/propagation/cost231_hata.rs` | Same formulas |
|
||
| `cost231_wi.py` | REWRITE | `src-tauri/src/propagation/cost231_wi.rs` | Same formulas |
|
||
| `itu_r_p1546.py` | REWRITE | `src-tauri/src/propagation/itu_r_p1546.rs` | Same formulas |
|
||
| `longley_rice.py` | REWRITE | `src-tauri/src/propagation/longley_rice.rs` | Same formulas |
|
||
| `itu_r_p526.py` | REWRITE | `src-tauri/src/propagation/knife_edge.rs` | Same formulas |
|
||
|
||
### Geometry
|
||
|
||
| Current File | Action | Target | Notes |
|
||
|-------------|--------|--------|-------|
|
||
| `geometry/haversine.py` | REWRITE | `src-tauri/src/geometry/haversine.rs` | SIMD batch version |
|
||
| `geometry/los.py` | REWRITE | `src-tauri/src/geometry/los.rs` | Vectorized |
|
||
| `geometry/diffraction.py` | REWRITE | `src-tauri/src/geometry/diffraction.rs` | Same formulas |
|
||
| `geometry/intersection.py` | REWRITE | `src-tauri/src/geometry/intersection.rs` | SIMD |
|
||
| `geometry/reflection.py` | REWRITE | `src-tauri/src/geometry/reflection.rs` | SIMD |
|
||
|
||
### Frontend Components
|
||
|
||
| Current File | Action | Target | Notes |
|
||
|-------------|--------|--------|-------|
|
||
| `App.tsx` | PORT | `src/routes/+page.svelte` | Svelte syntax |
|
||
| `components/map/Map.tsx` | PORT | `src/lib/components/map/MapContainer.svelte` | Svelte + Leaflet |
|
||
| `components/map/WebGLRadialCoverageLayer.tsx` | PORT | `src/lib/webgl/radial-layer.ts` | Keep shaders verbatim |
|
||
| `components/map/WebGLCoverageLayer.tsx` | PORT | `src/lib/webgl/texture-layer.ts` | Keep shaders verbatim |
|
||
| `components/map/SiteMarker.tsx` | PORT | `src/lib/components/map/SiteMarker.svelte` | Svelte syntax |
|
||
| `components/map/*.tsx` | PORT | `src/lib/components/map/*.svelte` | Svelte syntax |
|
||
| `components/panels/SiteList.tsx` | PORT | `src/lib/components/panels/SiteList.svelte` | Svelte syntax |
|
||
| `components/panels/*.tsx` | PORT | `src/lib/components/panels/*.svelte` | Svelte syntax |
|
||
| `components/modals/*.tsx` | PORT | `src/lib/components/modals/*.svelte` | Svelte syntax |
|
||
| `components/ui/*.tsx` | PORT | `src/lib/components/ui/*.svelte` | Svelte syntax |
|
||
| `store/sites.ts` | PORT | `src/lib/stores/sites.ts` | Svelte stores |
|
||
| `store/coverage.ts` | PORT | `src/lib/stores/coverage.ts` | + Tauri commands |
|
||
| `store/*.ts` | PORT | `src/lib/stores/*.ts` | Svelte stores |
|
||
| `services/api.ts` | REWRITE | `src/lib/tauri/commands.ts` | Tauri invoke() |
|
||
| `services/websocket.ts` | REWRITE | `src/lib/tauri/events.ts` | Tauri events |
|
||
| `hooks/*.ts` | PORT | `src/lib/hooks/*.ts` | Svelte stores |
|
||
| `rf/*.ts` | PORT | `src/lib/rf/*.ts` | Direct copy |
|
||
| `types/*.ts` | PORT | `src/lib/tauri/types.ts` | Merge + simplify |
|
||
|
||
### Desktop
|
||
|
||
| Current File | Action | Target | Notes |
|
||
|-------------|--------|--------|-------|
|
||
| `desktop/main.js` | DELETE | N/A | Tauri handles this |
|
||
| `desktop/preload.js` | DELETE | N/A | Tauri IPC |
|
||
| `desktop/splash.html` | PORT | `src-tauri/src/splashscreen.rs` | Optional |
|
||
| Electron-builder config | REWRITE | `src-tauri/tauri.conf.json` | Tauri config |
|
||
|
||
---
|
||
|
||
## Part 5: Risk Assessment
|
||
|
||
### 5.1 Hard to Port
|
||
|
||
| Component | Difficulty | Risk | Mitigation |
|
||
|-----------|------------|------|------------|
|
||
| Dominant path geometry | High | Complex vectorization | Extensive testing, benchmark vs Python |
|
||
| SRTM bilinear interp | Medium | Numerical precision | Unit tests with known values |
|
||
| Overpass API parsing | Medium | OSM data quirks | Use established crate (osmpbf) |
|
||
| WebGL shaders | Low | Already GLSL | Copy verbatim |
|
||
| Leaflet integration | Low | Same as React | svelte-leaflet exists |
|
||
|
||
### 5.2 Needs R&D
|
||
|
||
| Topic | Effort | Notes |
|
||
|-------|--------|-------|
|
||
| wgpu compute shaders | 2-3 weeks | Learn wgpu, write propagation shaders |
|
||
| SIMD geometry | 1-2 weeks | Use `packed_simd2` or `std::simd` |
|
||
| R-tree spatial index | 1 week | Evaluate `rstar` vs custom |
|
||
| SQLite FTS for OSM | 1 week | May need RTree extension |
|
||
|
||
### 5.3 Minimum Viable Product (MVP)
|
||
|
||
**Phase 1 MVP (Tauri shell):**
|
||
- SvelteKit UI with Leaflet
|
||
- WebGL coverage rendering
|
||
- Site management (CRUD, import/export)
|
||
- Python backend via HTTP proxy
|
||
- ~20MB bundle
|
||
|
||
**Phase 2 MVP (Rust engine):**
|
||
- Basic propagation (FSPL, Okumura-Hata, COST-231)
|
||
- Terrain LOS (no buildings yet)
|
||
- 10km calculations in <10 seconds
|
||
- ~12MB bundle
|
||
|
||
**Phase 3 MVP (Full Rust):**
|
||
- Buildings + dominant path
|
||
- All propagation models
|
||
- 20km full preset in <30 seconds
|
||
- ~10MB bundle
|
||
|
||
### 5.4 Estimated Timeline
|
||
|
||
| Phase | Duration | Cumulative | Dependencies |
|
||
|-------|----------|------------|--------------|
|
||
| Phase 1: Tauri Shell | 2-3 weeks | 3 weeks | None |
|
||
| Phase 2: Rust Engine | 4-6 weeks | 9 weeks | Phase 1 |
|
||
| Phase 3: Full Rust | 3-4 weeks | 13 weeks | Phase 2 |
|
||
| Phase 4: GPU/50km | 4-6 weeks | 19 weeks | Phase 3 |
|
||
|
||
**Total: ~4-5 months for full migration**
|
||
|
||
### 5.5 Key Dependencies (Rust Crates)
|
||
|
||
```toml
|
||
[dependencies]
|
||
# Tauri
|
||
tauri = { version = "1.5", features = ["api-all"] }
|
||
tauri-plugin-store = "0.1"
|
||
|
||
# Async runtime
|
||
tokio = { version = "1.0", features = ["full"] }
|
||
|
||
# HTTP client
|
||
reqwest = { version = "0.11", features = ["json"] }
|
||
|
||
# Serialization
|
||
serde = { version = "1.0", features = ["derive"] }
|
||
serde_json = "1.0"
|
||
rmp-serde = "1.1" # MessagePack
|
||
|
||
# Database
|
||
rusqlite = { version = "0.30", features = ["bundled"] }
|
||
|
||
# Spatial
|
||
rstar = "0.11" # R-tree
|
||
geo = "0.27" # Geometry types
|
||
|
||
# SIMD
|
||
packed_simd2 = "0.3" # or std::simd on nightly
|
||
|
||
# GPU (optional)
|
||
wgpu = "0.18"
|
||
|
||
# Parallelism
|
||
rayon = "1.8"
|
||
|
||
# Memory mapping
|
||
memmap2 = "0.9"
|
||
|
||
# Logging
|
||
tracing = "0.1"
|
||
tracing-subscriber = "0.3"
|
||
```
|
||
|
||
### 5.6 Performance Targets
|
||
|
||
| Scenario | Python Current | Rust Target | Speedup |
|
||
|----------|---------------|-------------|---------|
|
||
| 5km, fast preset | 5s | 0.5s | 10x |
|
||
| 10km, standard | 60s | 3s | 20x |
|
||
| 10km, detailed | 180s (timeout) | 10s | 18x |
|
||
| 20km, detailed | timeout | 30s | ∞ |
|
||
| 50km, full | OOM | 120s | ∞ |
|
||
|
||
### 5.7 Bundle Size Targets
|
||
|
||
| Component | Python/Electron | Rust/Tauri |
|
||
|-----------|----------------|------------|
|
||
| Frontend | 10MB | 2MB (Svelte is smaller) |
|
||
| Backend executable | 150MB (PyInstaller) | 8MB (Rust binary) |
|
||
| Runtime | 10MB (Electron) | 5MB (WebView2 shared) |
|
||
| **Total** | **170MB+** | **~15MB** |
|
||
|
||
---
|
||
|
||
## Appendix A: Critical Algorithms in Rust
|
||
|
||
### A.1 SIMD Haversine Distance
|
||
|
||
```rust
|
||
use packed_simd2::f64x4;
|
||
use std::f64::consts::PI;
|
||
|
||
const EARTH_RADIUS: f64 = 6371000.0;
|
||
|
||
#[inline]
|
||
pub fn haversine_batch(
|
||
site_lat: f64, site_lon: f64,
|
||
lats: &[f64], lons: &[f64],
|
||
) -> Vec<f64> {
|
||
assert_eq!(lats.len(), lons.len());
|
||
let n = lats.len();
|
||
let mut distances = vec![0.0; n];
|
||
|
||
let site_lat_rad = site_lat * PI / 180.0;
|
||
let site_lon_rad = site_lon * PI / 180.0;
|
||
|
||
let cos_site_lat = site_lat_rad.cos();
|
||
let sin_site_lat = site_lat_rad.sin();
|
||
|
||
// Process 4 points at a time with SIMD
|
||
let chunks = n / 4;
|
||
for i in 0..chunks {
|
||
let base = i * 4;
|
||
|
||
let lat_rad = f64x4::new(
|
||
lats[base] * PI / 180.0,
|
||
lats[base + 1] * PI / 180.0,
|
||
lats[base + 2] * PI / 180.0,
|
||
lats[base + 3] * PI / 180.0,
|
||
);
|
||
let lon_rad = f64x4::new(
|
||
lons[base] * PI / 180.0,
|
||
lons[base + 1] * PI / 180.0,
|
||
lons[base + 2] * PI / 180.0,
|
||
lons[base + 3] * PI / 180.0,
|
||
);
|
||
|
||
let dlat = lat_rad - f64x4::splat(site_lat_rad);
|
||
let dlon = lon_rad - f64x4::splat(site_lon_rad);
|
||
|
||
let sin_dlat_2 = (dlat * f64x4::splat(0.5)).sin_cos().0;
|
||
let sin_dlon_2 = (dlon * f64x4::splat(0.5)).sin_cos().0;
|
||
|
||
let a = sin_dlat_2 * sin_dlat_2
|
||
+ f64x4::splat(cos_site_lat) * lat_rad.cos()
|
||
* sin_dlon_2 * sin_dlon_2;
|
||
|
||
let c = f64x4::splat(2.0) * a.sqrt().asin();
|
||
let dist = f64x4::splat(EARTH_RADIUS) * c;
|
||
|
||
distances[base..base + 4].copy_from_slice(&dist.to_array());
|
||
}
|
||
|
||
// Handle remainder
|
||
for i in chunks * 4..n {
|
||
distances[i] = haversine_scalar(site_lat, site_lon, lats[i], lons[i]);
|
||
}
|
||
|
||
distances
|
||
}
|
||
```
|
||
|
||
### A.2 Parallel Grid Calculation
|
||
|
||
```rust
|
||
use rayon::prelude::*;
|
||
|
||
pub fn calculate_coverage_parallel(
|
||
sites: &[Site],
|
||
settings: &CoverageSettings,
|
||
terrain: &TerrainService,
|
||
buildings: &BuildingIndex,
|
||
progress: impl Fn(f32) + Sync,
|
||
) -> Vec<CoveragePoint> {
|
||
let grid = generate_grid(sites, settings);
|
||
let total = grid.len();
|
||
let counter = std::sync::atomic::AtomicUsize::new(0);
|
||
|
||
grid.par_iter()
|
||
.map(|coord| {
|
||
let i = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||
if i % 1000 == 0 {
|
||
progress(i as f32 / total as f32);
|
||
}
|
||
|
||
calculate_point(coord, sites, settings, terrain, buildings)
|
||
})
|
||
.collect()
|
||
}
|
||
```
|
||
|
||
### A.3 R-tree Spatial Index
|
||
|
||
```rust
|
||
use rstar::{RTree, AABB, PointDistance};
|
||
|
||
pub struct BuildingIndex {
|
||
rtree: RTree<BuildingRef>,
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
struct BuildingRef {
|
||
id: u64,
|
||
envelope: AABB<[f64; 2]>,
|
||
height: f32,
|
||
geometry: Vec<[f64; 2]>,
|
||
}
|
||
|
||
impl rstar::RTreeObject for BuildingRef {
|
||
type Envelope = AABB<[f64; 2]>;
|
||
fn envelope(&self) -> Self::Envelope { self.envelope }
|
||
}
|
||
|
||
impl BuildingIndex {
|
||
pub fn new(buildings: Vec<Building>) -> Self {
|
||
let refs: Vec<BuildingRef> = buildings.iter()
|
||
.map(|b| BuildingRef {
|
||
id: b.id,
|
||
envelope: compute_envelope(&b.geometry),
|
||
height: b.height,
|
||
geometry: b.geometry.clone(),
|
||
})
|
||
.collect();
|
||
|
||
Self { rtree: RTree::bulk_load(refs) }
|
||
}
|
||
|
||
pub fn query_line(&self, p1: [f64; 2], p2: [f64; 2]) -> Vec<&BuildingRef> {
|
||
let envelope = AABB::from_corners(
|
||
[p1[0].min(p2[0]) - 0.001, p1[1].min(p2[1]) - 0.001],
|
||
[p1[0].max(p2[0]) + 0.001, p1[1].max(p2[1]) + 0.001],
|
||
);
|
||
|
||
self.rtree
|
||
.locate_in_envelope(&envelope)
|
||
.filter(|b| line_intersects_polygon(p1, p2, &b.geometry))
|
||
.collect()
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Appendix B: WebGL Shader Reuse
|
||
|
||
The GLSL shaders from `WebGLRadialCoverageLayer.tsx` can be used directly in Svelte:
|
||
|
||
```typescript
|
||
// src/lib/webgl/shaders.ts
|
||
|
||
export const POINT_VERTEX_SHADER = `
|
||
attribute vec2 a_position;
|
||
attribute vec2 a_pointPos;
|
||
attribute float a_pointRsrp;
|
||
attribute float a_pointRadius;
|
||
|
||
varying vec2 v_localPos;
|
||
varying float v_rsrp;
|
||
|
||
void main() {
|
||
vec2 pos = a_pointPos + a_position * a_pointRadius;
|
||
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
|
||
v_localPos = a_position;
|
||
v_rsrp = a_pointRsrp;
|
||
}
|
||
`;
|
||
|
||
export const POINT_FRAGMENT_SHADER = `
|
||
precision highp float;
|
||
varying vec2 v_localPos;
|
||
varying float v_rsrp;
|
||
|
||
void main() {
|
||
float dist = length(v_localPos);
|
||
if (dist > 1.0) discard;
|
||
float weight = exp(-dist * dist * 2.0);
|
||
gl_FragColor = vec4(weight * v_rsrp, weight, 0.0, 1.0);
|
||
}
|
||
`;
|
||
|
||
// ... rest of shaders
|
||
```
|
||
|
||
---
|
||
|
||
## Appendix C: Tauri Configuration
|
||
|
||
```json
|
||
// src-tauri/tauri.conf.json
|
||
{
|
||
"build": {
|
||
"beforeBuildCommand": "npm run build",
|
||
"beforeDevCommand": "npm run dev",
|
||
"devPath": "http://localhost:5173",
|
||
"distDir": "../build"
|
||
},
|
||
"package": {
|
||
"productName": "RFCP",
|
||
"version": "2.0.0"
|
||
},
|
||
"tauri": {
|
||
"bundle": {
|
||
"active": true,
|
||
"targets": ["nsis", "msi", "app", "dmg", "deb", "appimage"],
|
||
"identifier": "one.eliah.rfcp",
|
||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.icns", "icons/icon.ico"],
|
||
"resources": ["terrain/**/*"],
|
||
"windows": {
|
||
"wix": null
|
||
}
|
||
},
|
||
"windows": [
|
||
{
|
||
"title": "RFCP - RF Coverage Planner",
|
||
"width": 1400,
|
||
"height": 900,
|
||
"resizable": true,
|
||
"fullscreen": false
|
||
}
|
||
],
|
||
"security": {
|
||
"csp": null
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
This migration will transform RFCP from a Python/Electron application into a high-performance Rust/Tauri native app. Key benefits:
|
||
|
||
1. **Performance:** 20-50x faster calculations through Rust + SIMD + Rayon
|
||
2. **Size:** 10-15MB bundle vs 170MB+ current
|
||
3. **Memory:** Constant memory usage via mmap and streaming
|
||
4. **Capability:** 50km+ calculations that currently timeout
|
||
|
||
The phased approach allows incremental delivery while maintaining functionality throughout the migration.
|