@mytec: before 3.0 REFACTOR
This commit is contained in:
577
RFCP-Phase-2.5.1-Performance-AppClose.md
Normal file
577
RFCP-Phase-2.5.1-Performance-AppClose.md
Normal file
@@ -0,0 +1,577 @@
|
||||
# RFCP Phase 2.5.1: Performance Tuning + App Close Fix
|
||||
|
||||
**Date:** February 1, 2025
|
||||
**Type:** Performance + Bug Fix
|
||||
**Priority:** HIGH
|
||||
**Depends on:** Phase 2.5.0
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Summary
|
||||
|
||||
Phase 2.5.0 vectorization працює, але bottleneck залишився:
|
||||
- `line_intersects_polygons_batch()` має внутрішній loop по polygons
|
||||
- 351-458 walls перевіряються для кожної точки
|
||||
- Результат: 329ms/point (майже без змін)
|
||||
|
||||
Також: App close ДОСІ не працює — хрестик не закриває backend.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Current Performance Analysis
|
||||
|
||||
```
|
||||
[DOMINANT_PATH_VEC] Point #1: buildings=50, walls=351
|
||||
[DOMINANT_PATH_VEC] Point #2: buildings=50, walls=439
|
||||
[DOMINANT_PATH_VEC] Point #3: buildings=50, walls=458
|
||||
|
||||
Result: 329ms/point — NOT improved!
|
||||
```
|
||||
|
||||
**Root cause:** LOS check loop inside vectorized function:
|
||||
```python
|
||||
# geometry_vectorized.py line ~85
|
||||
for i, length in enumerate(polygon_lengths): # ← THIS LOOP
|
||||
# Check each polygon...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Fix 2.5.1a: Limit Walls for Reflection Check
|
||||
|
||||
**File:** `backend/app/services/geometry_vectorized.py`
|
||||
|
||||
**In `find_best_reflection_path_vectorized()`:**
|
||||
|
||||
```python
|
||||
def find_best_reflection_path_vectorized(
|
||||
tx: np.ndarray,
|
||||
rx: np.ndarray,
|
||||
building_walls_start: np.ndarray,
|
||||
building_walls_end: np.ndarray,
|
||||
wall_to_building: np.ndarray,
|
||||
obstacle_polygons_x: np.ndarray,
|
||||
obstacle_polygons_y: np.ndarray,
|
||||
obstacle_lengths: np.ndarray,
|
||||
max_candidates: int = 50,
|
||||
max_walls: int = 100, # ← ADD THIS
|
||||
max_los_checks: int = 10 # ← ADD THIS
|
||||
) -> Tuple[Optional[np.ndarray], float, float]:
|
||||
"""
|
||||
Find best single-reflection path using vectorized operations.
|
||||
|
||||
OPTIMIZATION: Limit walls and LOS checks for speed.
|
||||
- max_walls: Only consider closest N walls for reflection
|
||||
- max_los_checks: Only verify LOS for top N shortest paths
|
||||
"""
|
||||
num_walls = len(building_walls_start)
|
||||
|
||||
if num_walls == 0:
|
||||
return None, np.inf, 0.0
|
||||
|
||||
# === OPTIMIZATION 1: Limit walls by distance to path midpoint ===
|
||||
if num_walls > max_walls:
|
||||
# Calculate midpoint of TX-RX path
|
||||
midpoint = (tx + rx) / 2
|
||||
|
||||
# Calculate wall midpoints
|
||||
wall_midpoints = (building_walls_start + building_walls_end) / 2
|
||||
|
||||
# Distance from each wall to path midpoint
|
||||
wall_distances = np.linalg.norm(wall_midpoints - midpoint, axis=1)
|
||||
|
||||
# Take closest walls
|
||||
closest_indices = np.argpartition(wall_distances, max_walls)[:max_walls]
|
||||
|
||||
building_walls_start = building_walls_start[closest_indices]
|
||||
building_walls_end = building_walls_end[closest_indices]
|
||||
wall_to_building = wall_to_building[closest_indices]
|
||||
|
||||
# Log reduction (first 3 points only)
|
||||
# print(f"[WALLS] Reduced {num_walls} → {max_walls} walls")
|
||||
|
||||
# Step 1: Calculate all reflection points at once
|
||||
refl_points, valid = calculate_reflection_points_batch(
|
||||
tx, rx, building_walls_start, building_walls_end
|
||||
)
|
||||
|
||||
if not np.any(valid):
|
||||
return None, np.inf, 0.0
|
||||
|
||||
# Step 2: Calculate path lengths for valid reflections
|
||||
valid_indices = np.where(valid)[0]
|
||||
valid_refl = refl_points[valid]
|
||||
|
||||
tx_to_refl = np.linalg.norm(valid_refl - tx, axis=1)
|
||||
refl_to_rx = np.linalg.norm(rx - valid_refl, axis=1)
|
||||
path_lengths = tx_to_refl + refl_to_rx
|
||||
|
||||
# === OPTIMIZATION 2: Sort by path length, check LOS only for top N ===
|
||||
# Sort indices by path length (shortest first)
|
||||
sorted_order = np.argsort(path_lengths)
|
||||
|
||||
# Limit to max_los_checks
|
||||
check_count = min(len(sorted_order), max_los_checks)
|
||||
|
||||
# Step 3: Check LOS only for top candidates
|
||||
best_idx = -1
|
||||
best_length = np.inf
|
||||
|
||||
for i in range(check_count):
|
||||
idx = sorted_order[i]
|
||||
refl_pt = valid_refl[idx]
|
||||
length = path_lengths[idx]
|
||||
|
||||
# Skip if already longer than best found
|
||||
if length >= best_length:
|
||||
continue
|
||||
|
||||
# Check TX → reflection LOS
|
||||
intersects1, _ = line_intersects_polygons_batch(
|
||||
tx, refl_pt, obstacle_polygons_x, obstacle_polygons_y, obstacle_lengths
|
||||
)
|
||||
|
||||
if np.any(intersects1):
|
||||
continue
|
||||
|
||||
# Check reflection → RX LOS
|
||||
intersects2, _ = line_intersects_polygons_batch(
|
||||
refl_pt, rx, obstacle_polygons_x, obstacle_polygons_y, obstacle_lengths
|
||||
)
|
||||
|
||||
if np.any(intersects2):
|
||||
continue
|
||||
|
||||
# Valid path found!
|
||||
best_idx = idx
|
||||
best_length = length
|
||||
break # ← EARLY EXIT: First valid shortest path is best
|
||||
|
||||
if best_idx < 0:
|
||||
return None, np.inf, 0.0
|
||||
|
||||
best_point = valid_refl[best_idx]
|
||||
|
||||
# Reflection loss calculation
|
||||
direct_dist = np.linalg.norm(rx - tx)
|
||||
path_ratio = best_length / max(direct_dist, 1.0)
|
||||
reflection_loss = 3.0 + 7.0 * min(1.0, (path_ratio - 1.0) * 2)
|
||||
|
||||
return best_point, best_length, reflection_loss
|
||||
```
|
||||
|
||||
**Key optimizations:**
|
||||
1. **max_walls=100**: Only consider 100 closest walls (was 351-458)
|
||||
2. **max_los_checks=10**: Only verify LOS for 10 shortest paths (was 50)
|
||||
3. **Early exit**: Stop when first valid path found (sorted by length)
|
||||
|
||||
**Expected speedup:** 5-10x for reflection calculation
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Fix 2.5.1b: Simplify LOS Check for Obstacles
|
||||
|
||||
**File:** `backend/app/services/geometry_vectorized.py`
|
||||
|
||||
**Optimize `line_intersects_polygons_batch()` — limit polygons:**
|
||||
|
||||
```python
|
||||
def line_intersects_polygons_batch(
|
||||
p1: np.ndarray, p2: np.ndarray,
|
||||
polygons_x: np.ndarray, polygons_y: np.ndarray,
|
||||
polygon_lengths: np.ndarray,
|
||||
max_polygons: int = 30 # ← ADD LIMIT
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Check if line p1→p2 intersects multiple polygons.
|
||||
|
||||
OPTIMIZATION: Only check nearest polygons to the line.
|
||||
"""
|
||||
num_polygons = len(polygon_lengths)
|
||||
|
||||
if num_polygons == 0:
|
||||
return np.array([], dtype=bool), np.array([])
|
||||
|
||||
# === OPTIMIZATION: Filter to nearby polygons first ===
|
||||
if num_polygons > max_polygons:
|
||||
# Quick filter: bounding box check
|
||||
line_min_x = min(p1[0], p2[0]) - 50 # 50m buffer
|
||||
line_max_x = max(p1[0], p2[0]) + 50
|
||||
line_min_y = min(p1[1], p2[1]) - 50
|
||||
line_max_y = max(p1[1], p2[1]) + 50
|
||||
|
||||
# Calculate polygon centroids quickly
|
||||
# This is approximate but fast
|
||||
idx = 0
|
||||
nearby_mask = np.zeros(num_polygons, dtype=bool)
|
||||
|
||||
for i, length in enumerate(polygon_lengths):
|
||||
if length < 3:
|
||||
idx += length
|
||||
continue
|
||||
|
||||
# Polygon centroid (approximate: first vertex)
|
||||
px = polygons_x[idx]
|
||||
py = polygons_y[idx]
|
||||
|
||||
# Check if centroid is near the line bounding box
|
||||
if (line_min_x <= px <= line_max_x and
|
||||
line_min_y <= py <= line_max_y):
|
||||
nearby_mask[i] = True
|
||||
|
||||
idx += length
|
||||
|
||||
# If still too many, take first max_polygons that are nearby
|
||||
nearby_indices = np.where(nearby_mask)[0]
|
||||
if len(nearby_indices) > max_polygons:
|
||||
nearby_indices = nearby_indices[:max_polygons]
|
||||
nearby_mask = np.zeros(num_polygons, dtype=bool)
|
||||
nearby_mask[nearby_indices] = True
|
||||
else:
|
||||
nearby_mask = np.ones(num_polygons, dtype=bool)
|
||||
|
||||
# Now check only nearby polygons
|
||||
intersects = np.zeros(num_polygons, dtype=bool)
|
||||
min_t = np.ones(num_polygons) * np.inf
|
||||
|
||||
idx = 0
|
||||
for i, length in enumerate(polygon_lengths):
|
||||
if length < 3 or not nearby_mask[i]:
|
||||
idx += length
|
||||
continue
|
||||
|
||||
# Get polygon vertices
|
||||
px = polygons_x[idx:idx + length]
|
||||
py = polygons_y[idx:idx + length]
|
||||
|
||||
# Create edge segments
|
||||
starts = np.stack([px, py], axis=1)
|
||||
ends = np.stack([np.roll(px, -1), np.roll(py, -1)], axis=1)
|
||||
|
||||
# Check intersections
|
||||
edge_intersects, t_vals = line_segments_intersect_batch(p1, p2, starts, ends)
|
||||
|
||||
if np.any(edge_intersects):
|
||||
intersects[i] = True
|
||||
min_t[i] = np.min(t_vals[edge_intersects])
|
||||
|
||||
idx += length
|
||||
|
||||
line_length = np.linalg.norm(p2 - p1)
|
||||
min_distances = min_t * line_length
|
||||
|
||||
return intersects, min_distances
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Fix 2.5.1c: Update Constants in dominant_path_service.py
|
||||
|
||||
**File:** `backend/app/services/dominant_path_service.py`
|
||||
|
||||
```python
|
||||
# At top of file, update constants:
|
||||
MAX_BUILDINGS_FOR_LINE = 30 # was 50
|
||||
MAX_BUILDINGS_FOR_REFLECTION = 20 # was 30
|
||||
MAX_DISTANCE_FROM_PATH = 200 # was 300m
|
||||
|
||||
# In find_dominant_paths_vectorized(), add parameters:
|
||||
result = find_best_reflection_path_vectorized(
|
||||
tx_local, rx_local,
|
||||
walls_start, walls_end, wall_to_bldg,
|
||||
poly_x, poly_y, poly_lengths,
|
||||
max_candidates=30, # was 50
|
||||
max_walls=100, # NEW: limit walls
|
||||
max_los_checks=10 # NEW: limit LOS checks
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Fix 2.5.1d: App Close — AGGRESSIVE FIX
|
||||
|
||||
**The X button STILL doesn't close the app!**
|
||||
|
||||
**File:** `desktop/main.js`
|
||||
|
||||
**Problem analysis:**
|
||||
- `killBackend()` is called but processes survive
|
||||
- `killAllBackendProcesses()` uses `execSync` but maybe fails silently
|
||||
- Electron might quit before kill completes
|
||||
|
||||
**AGGRESSIVE FIX — Multiple kill strategies:**
|
||||
|
||||
```javascript
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
|
||||
let backendProcess = null;
|
||||
let isQuitting = false;
|
||||
|
||||
// Track all spawned PIDs
|
||||
let knownPids = new Set();
|
||||
|
||||
function killAllRfcpProcesses() {
|
||||
console.log('[KILL] === Starting aggressive kill ===');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Strategy 1: Kill by image name (most reliable)
|
||||
try {
|
||||
console.log('[KILL] Strategy 1: taskkill /F /IM');
|
||||
execSync('taskkill /F /IM rfcp-server.exe', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
console.log('[KILL] Strategy 1: SUCCESS');
|
||||
} catch (e) {
|
||||
console.log('[KILL] Strategy 1: No processes or already killed');
|
||||
}
|
||||
|
||||
// Strategy 2: Kill by tree if we have PID
|
||||
if (backendProcess && backendProcess.pid) {
|
||||
try {
|
||||
console.log(`[KILL] Strategy 2: taskkill /F /T /PID ${backendProcess.pid}`);
|
||||
execSync(`taskkill /F /T /PID ${backendProcess.pid}`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
console.log('[KILL] Strategy 2: SUCCESS');
|
||||
} catch (e) {
|
||||
console.log('[KILL] Strategy 2: PID not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: PowerShell kill (backup)
|
||||
try {
|
||||
console.log('[KILL] Strategy 3: PowerShell Stop-Process');
|
||||
execSync('powershell -Command "Get-Process rfcp-server -ErrorAction SilentlyContinue | Stop-Process -Force"', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
console.log('[KILL] Strategy 3: SUCCESS');
|
||||
} catch (e) {
|
||||
console.log('[KILL] Strategy 3: PowerShell failed or no processes');
|
||||
}
|
||||
|
||||
// Strategy 4: WMIC kill (legacy but works)
|
||||
try {
|
||||
console.log('[KILL] Strategy 4: WMIC process delete');
|
||||
execSync('wmic process where "name=\'rfcp-server.exe\'" delete', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
console.log('[KILL] Strategy 4: SUCCESS');
|
||||
} catch (e) {
|
||||
console.log('[KILL] Strategy 4: WMIC failed');
|
||||
}
|
||||
|
||||
} else {
|
||||
// Unix
|
||||
try {
|
||||
execSync('pkill -9 -f rfcp-server', { stdio: 'pipe', timeout: 5000 });
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[KILL] === Kill sequence complete ===');
|
||||
}
|
||||
|
||||
// Call backend shutdown endpoint before killing
|
||||
async function gracefulShutdown() {
|
||||
console.log('[SHUTDOWN] Starting graceful shutdown...');
|
||||
|
||||
// Step 1: Try graceful API shutdown
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 2000);
|
||||
|
||||
await fetch('http://127.0.0.1:8888/api/system/shutdown', {
|
||||
method: 'POST',
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
console.log('[SHUTDOWN] Backend acknowledged shutdown');
|
||||
|
||||
// Wait for backend to cleanup
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
} catch (e) {
|
||||
console.log('[SHUTDOWN] Backend did not respond:', e.message);
|
||||
}
|
||||
|
||||
// Step 2: Force kill everything
|
||||
killAllRfcpProcesses();
|
||||
|
||||
// Step 3: Wait and verify
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
console.log('[SHUTDOWN] Shutdown complete');
|
||||
}
|
||||
|
||||
// === EVENT HANDLERS ===
|
||||
|
||||
// When window X is clicked
|
||||
mainWindow.on('close', async (event) => {
|
||||
console.log('[EVENT] Window close clicked');
|
||||
|
||||
if (!isQuitting) {
|
||||
event.preventDefault(); // Prevent immediate close
|
||||
isQuitting = true;
|
||||
|
||||
await gracefulShutdown();
|
||||
|
||||
// Now actually quit
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// When all windows closed
|
||||
app.on('window-all-closed', () => {
|
||||
console.log('[EVENT] All windows closed');
|
||||
killAllRfcpProcesses(); // Extra safety
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Before quit
|
||||
app.on('before-quit', async (event) => {
|
||||
console.log('[EVENT] Before quit');
|
||||
|
||||
if (!isQuitting) {
|
||||
event.preventDefault();
|
||||
isQuitting = true;
|
||||
await gracefulShutdown();
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
// Will quit (sync only!)
|
||||
app.on('will-quit', () => {
|
||||
console.log('[EVENT] Will quit - final sync kill');
|
||||
killAllRfcpProcesses();
|
||||
});
|
||||
|
||||
// Process exit
|
||||
process.on('exit', () => {
|
||||
console.log('[EVENT] Process exit');
|
||||
killAllRfcpProcesses();
|
||||
});
|
||||
|
||||
// Handle Ctrl+C in dev mode
|
||||
process.on('SIGINT', () => {
|
||||
console.log('[SIGNAL] SIGINT');
|
||||
killAllRfcpProcesses();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('[SIGNAL] SIGTERM');
|
||||
killAllRfcpProcesses();
|
||||
process.exit(0);
|
||||
});
|
||||
```
|
||||
|
||||
**Key changes:**
|
||||
1. **4 kill strategies** on Windows (taskkill, PID tree, PowerShell, WMIC)
|
||||
2. **`isQuitting` flag** prevents multiple shutdown attempts
|
||||
3. **`event.preventDefault()`** on close to allow async shutdown
|
||||
4. **Graceful shutdown first** via API, then force kill
|
||||
5. **Every event handler** calls killAllRfcpProcesses()
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Fix 2.5.1e: Update Test Scripts for Win11
|
||||
|
||||
**File:** All .bat scripts
|
||||
|
||||
**Replace `wmic` with PowerShell (wmic deprecated in Win11):**
|
||||
|
||||
```batch
|
||||
:: OLD (wmic - deprecated):
|
||||
for /f "tokens=2 delims==" %%a in ('wmic OS get FreePhysicalMemory /value') do ...
|
||||
|
||||
:: NEW (PowerShell):
|
||||
for /f %%a in ('powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a FREE_RAM=%%a/1024
|
||||
|
||||
:: OLD (CPU):
|
||||
for /f "tokens=2 delims==" %%a in ('wmic cpu get LoadPercentage /value') do ...
|
||||
|
||||
:: NEW (PowerShell):
|
||||
for /f %%a in ('powershell -Command "(Get-CimInstance Win32_Processor).LoadPercentage"') do set CPU=%%a
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `backend/app/services/geometry_vectorized.py` | Add max_walls, max_los_checks limits; optimize LOS check |
|
||||
| `backend/app/services/dominant_path_service.py` | Update constants, pass new parameters |
|
||||
| `desktop/main.js` | Aggressive multi-strategy kill; async shutdown with prevention |
|
||||
| `installer/*.bat` | Replace wmic with PowerShell commands |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Test 1: Performance
|
||||
```bash
|
||||
# Run Detailed preset
|
||||
# Expected: < 90 seconds (was 292s)
|
||||
# Expected: < 100ms/point (was 329ms)
|
||||
```
|
||||
|
||||
### Test 2: App Close
|
||||
```bash
|
||||
# Start RFCP.exe
|
||||
# Click X
|
||||
# Check Task Manager — should be 0 rfcp-server.exe
|
||||
# Check console for [KILL] logs showing all strategies
|
||||
```
|
||||
|
||||
### Test 3: Accuracy Spot Check
|
||||
```bash
|
||||
# Run Standard preset, note coverage shape
|
||||
# Run Detailed preset, compare coverage shape
|
||||
# Should be similar (within visual inspection)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Success Criteria
|
||||
|
||||
- [ ] Detailed preset completes in < 90 seconds
|
||||
- [ ] ms/point < 100ms (was 329ms)
|
||||
- [ ] App close works on first X click
|
||||
- [ ] No rfcp-server.exe in Task Manager after close
|
||||
- [ ] Test scripts work on Windows 11 (no wmic errors)
|
||||
|
||||
---
|
||||
|
||||
## 📈 Expected Performance
|
||||
|
||||
| Metric | Before 2.5.1 | After 2.5.1 |
|
||||
|--------|--------------|-------------|
|
||||
| Walls checked | 351-458 | 100 max |
|
||||
| LOS checks | 50 | 10 max |
|
||||
| ms/point | 329ms | ~50-80ms |
|
||||
| Detailed time | 292s | ~60-90s |
|
||||
| App close | ❌ broken | ✅ works |
|
||||
|
||||
---
|
||||
|
||||
## 🔜 After 2.5.1
|
||||
|
||||
If performance is acceptable:
|
||||
- Phase 2.6: Fun facts loading screen
|
||||
- Phase 2.6: Export to GeoJSON/KML
|
||||
- Phase 2.7: Multi-site interference visualization
|
||||
@@ -44,7 +44,7 @@ from app.services.terrain_service import terrain_service, TerrainService
|
||||
from app.services.los_service import los_service
|
||||
from app.services.buildings_service import buildings_service, Building
|
||||
from app.services.materials_service import materials_service
|
||||
from app.services.dominant_path_service import dominant_path_service
|
||||
from app.services.dominant_path_service import dominant_path_service, find_dominant_paths_vectorized
|
||||
from app.services.street_canyon_service import street_canyon_service, Street
|
||||
from app.services.reflection_service import reflection_service
|
||||
from app.services.spatial_index import get_spatial_index, SpatialIndex
|
||||
@@ -648,22 +648,28 @@ class CoverageService:
|
||||
break
|
||||
timing["buildings"] += time.time() - t0
|
||||
|
||||
# Dominant path (sync) — uses spatial index for O(1) building lookups
|
||||
# Dominant path (vectorized NumPy) — replaces loop-based sync version
|
||||
if settings.use_dominant_path and (spatial_idx or nearby_buildings):
|
||||
t0 = time.time()
|
||||
paths = dominant_path_service.find_dominant_paths_sync(
|
||||
dominant = find_dominant_paths_vectorized(
|
||||
site.lat, site.lon, site.height,
|
||||
lat, lon, 1.5,
|
||||
site.frequency, nearby_buildings,
|
||||
spatial_idx=spatial_idx
|
||||
spatial_idx=spatial_idx,
|
||||
)
|
||||
if paths:
|
||||
best_path = paths[0]
|
||||
if best_path.is_valid and best_path.path_loss < (path_loss + terrain_loss + building_loss):
|
||||
path_loss = best_path.path_loss
|
||||
terrain_loss = 0
|
||||
building_loss = 0
|
||||
has_los = best_path.path_type == "direct" and not best_path.materials_crossed
|
||||
if dominant['path_type'] == 'direct':
|
||||
# Direct LOS confirmed by vectorized check
|
||||
has_los = True
|
||||
building_loss = 0.0
|
||||
elif dominant['path_type'] == 'reflection':
|
||||
# Reflection path bypasses buildings — reduce building loss
|
||||
building_loss = max(0.0, building_loss - (10.0 - dominant['total_loss']))
|
||||
has_los = False
|
||||
elif dominant['path_type'] == 'diffraction':
|
||||
# Diffraction: use estimated loss if worse than current
|
||||
if dominant['total_loss'] > building_loss:
|
||||
building_loss = dominant['total_loss']
|
||||
has_los = False
|
||||
timing["dominant_path"] += time.time() - t0
|
||||
|
||||
# Street canyon (sync)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import time
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, TYPE_CHECKING
|
||||
from typing import List, Tuple, Optional, Dict, Any, TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
from app.services.terrain_service import terrain_service
|
||||
from app.services.buildings_service import buildings_service, Building
|
||||
from app.services.materials_service import materials_service, BuildingMaterial
|
||||
from app.services.geometry_vectorized import (
|
||||
points_to_local_coords,
|
||||
line_intersects_polygons_batch,
|
||||
find_best_reflection_path_vectorized,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.services.spatial_index import SpatialIndex
|
||||
@@ -21,9 +26,9 @@ class RayPath:
|
||||
is_valid: bool # Does this path exist?
|
||||
|
||||
|
||||
MAX_BUILDINGS_FOR_LINE = 50
|
||||
MAX_BUILDINGS_FOR_REFLECTION = 30
|
||||
MAX_DISTANCE_FROM_PATH = 300 # meters
|
||||
MAX_BUILDINGS_FOR_LINE = 30
|
||||
MAX_BUILDINGS_FOR_REFLECTION = 20
|
||||
MAX_DISTANCE_FROM_PATH = 200 # meters
|
||||
|
||||
|
||||
def _filter_buildings_by_distance(buildings, tx_point, rx_point, max_count=100, max_distance=500):
|
||||
@@ -60,6 +65,204 @@ def _filter_buildings_by_distance(buildings, tx_point, rx_point, max_count=100,
|
||||
return filtered[:max_count]
|
||||
|
||||
|
||||
# ── Vectorized dominant path (NumPy) ──
|
||||
|
||||
_vec_log_count = 0
|
||||
|
||||
|
||||
def _buildings_to_arrays(buildings: List[Building], ref_lat: float, ref_lon: float):
|
||||
"""Convert Building objects to numpy arrays for vectorized geometry.
|
||||
|
||||
Returns:
|
||||
walls_start: (W, 2) wall start points in local XY meters
|
||||
walls_end: (W, 2) wall end points in local XY meters
|
||||
wall_to_building: (W,) mapping wall index -> building index
|
||||
poly_x: flattened polygon X coords
|
||||
poly_y: flattened polygon Y coords
|
||||
poly_lengths: (num_polygons,) vertices per polygon
|
||||
"""
|
||||
all_walls_start = []
|
||||
all_walls_end = []
|
||||
wall_to_building = []
|
||||
|
||||
all_poly_x = []
|
||||
all_poly_y = []
|
||||
poly_lengths = []
|
||||
|
||||
for i, b in enumerate(buildings):
|
||||
geom = b.geometry # [[lon, lat], ...]
|
||||
if not geom or len(geom) < 3:
|
||||
poly_lengths.append(0)
|
||||
continue
|
||||
|
||||
poly_lats = np.array([p[1] for p in geom])
|
||||
poly_lons = np.array([p[0] for p in geom])
|
||||
px, py = points_to_local_coords(ref_lat, ref_lon, poly_lats, poly_lons)
|
||||
|
||||
all_poly_x.extend(px)
|
||||
all_poly_y.extend(py)
|
||||
poly_lengths.append(len(geom))
|
||||
|
||||
# Extract wall segments
|
||||
for j in range(len(geom) - 1):
|
||||
all_walls_start.append([px[j], py[j]])
|
||||
all_walls_end.append([px[j + 1], py[j + 1]])
|
||||
wall_to_building.append(i)
|
||||
|
||||
return (
|
||||
np.array(all_walls_start) if all_walls_start else np.zeros((0, 2)),
|
||||
np.array(all_walls_end) if all_walls_end else np.zeros((0, 2)),
|
||||
np.array(wall_to_building, dtype=int) if wall_to_building else np.zeros(0, dtype=int),
|
||||
np.array(all_poly_x) if all_poly_x else np.zeros(0),
|
||||
np.array(all_poly_y) if all_poly_y else np.zeros(0),
|
||||
np.array(poly_lengths, dtype=int),
|
||||
)
|
||||
|
||||
|
||||
def find_dominant_paths_vectorized(
|
||||
tx_lat: float, tx_lon: float, tx_height: float,
|
||||
rx_lat: float, rx_lon: float, rx_height: float,
|
||||
frequency_mhz: float,
|
||||
buildings: List[Building],
|
||||
spatial_idx: 'Optional[SpatialIndex]' = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Vectorized dominant path finding using NumPy batch operations.
|
||||
|
||||
Replaces the loop-based find_dominant_paths_sync() with:
|
||||
1. Batch building-to-array conversion
|
||||
2. Vectorized LOS polygon intersection check
|
||||
3. Vectorized reflection point calculation
|
||||
4. Simplified diffraction estimate
|
||||
|
||||
Returns dict with:
|
||||
has_los, path_type, total_loss, path_length, reflection_point
|
||||
"""
|
||||
global _vec_log_count
|
||||
|
||||
# Get nearby buildings via spatial index (same filtering as sync version)
|
||||
if spatial_idx:
|
||||
line_buildings = spatial_idx.query_line(tx_lat, tx_lon, rx_lat, rx_lon)
|
||||
else:
|
||||
line_buildings = buildings
|
||||
|
||||
line_buildings = _filter_buildings_by_distance(
|
||||
line_buildings,
|
||||
(tx_lat, tx_lon), (rx_lat, rx_lon),
|
||||
max_count=MAX_BUILDINGS_FOR_LINE,
|
||||
max_distance=MAX_DISTANCE_FROM_PATH,
|
||||
)
|
||||
|
||||
# Reference point for local coordinate system
|
||||
ref_lat = (tx_lat + rx_lat) / 2
|
||||
ref_lon = (tx_lon + rx_lon) / 2
|
||||
|
||||
# Convert TX/RX to local meters
|
||||
tx_xy = points_to_local_coords(ref_lat, ref_lon, np.array([tx_lat]), np.array([tx_lon]))
|
||||
rx_xy = points_to_local_coords(ref_lat, ref_lon, np.array([rx_lat]), np.array([rx_lon]))
|
||||
tx = np.array([tx_xy[0][0], tx_xy[1][0]])
|
||||
rx = np.array([rx_xy[0][0], rx_xy[1][0]])
|
||||
|
||||
direct_dist = np.linalg.norm(rx - tx)
|
||||
|
||||
# Convert buildings to arrays
|
||||
walls_start, walls_end, wall_to_bldg, poly_x, poly_y, poly_lengths = (
|
||||
_buildings_to_arrays(line_buildings, ref_lat, ref_lon)
|
||||
)
|
||||
|
||||
# Diagnostic log for first few points
|
||||
_vec_log_count += 1
|
||||
if _vec_log_count <= 3:
|
||||
print(
|
||||
f"[DOMINANT_PATH_VEC] Point #{_vec_log_count}: "
|
||||
f"buildings={len(line_buildings)}, walls={len(walls_start)}, "
|
||||
f"dist={direct_dist:.0f}m",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# No buildings → direct LOS
|
||||
if len(poly_lengths) == 0 or np.all(poly_lengths < 3):
|
||||
return {
|
||||
'has_los': True,
|
||||
'path_type': 'direct',
|
||||
'total_loss': 0.0,
|
||||
'path_length': direct_dist,
|
||||
'reflection_point': None,
|
||||
}
|
||||
|
||||
# Step 1: Vectorized direct LOS check
|
||||
intersects, _ = line_intersects_polygons_batch(tx, rx, poly_x, poly_y, poly_lengths)
|
||||
|
||||
if not np.any(intersects):
|
||||
return {
|
||||
'has_los': True,
|
||||
'path_type': 'direct',
|
||||
'total_loss': 0.0,
|
||||
'path_length': direct_dist,
|
||||
'reflection_point': None,
|
||||
}
|
||||
|
||||
# Step 2: Vectorized reflection path finding
|
||||
# Use all line buildings for reflection walls
|
||||
if spatial_idx:
|
||||
mid_lat = (tx_lat + rx_lat) / 2
|
||||
mid_lon = (tx_lon + rx_lon) / 2
|
||||
refl_buildings = spatial_idx.query_point(mid_lat, mid_lon, buffer_cells=3)
|
||||
refl_buildings = _filter_buildings_by_distance(
|
||||
refl_buildings,
|
||||
(tx_lat, tx_lon), (rx_lat, rx_lon),
|
||||
max_count=MAX_BUILDINGS_FOR_REFLECTION,
|
||||
max_distance=MAX_DISTANCE_FROM_PATH,
|
||||
)
|
||||
# Merge line + reflection buildings (deduplicate by id)
|
||||
seen_ids = {b.id for b in line_buildings}
|
||||
merged = list(line_buildings)
|
||||
for b in refl_buildings:
|
||||
if b.id not in seen_ids:
|
||||
merged.append(b)
|
||||
seen_ids.add(b.id)
|
||||
r_walls_start, r_walls_end, r_wall_to_bldg, r_poly_x, r_poly_y, r_poly_lengths = (
|
||||
_buildings_to_arrays(merged, ref_lat, ref_lon)
|
||||
)
|
||||
else:
|
||||
r_walls_start, r_walls_end, r_wall_to_bldg = walls_start, walls_end, wall_to_bldg
|
||||
r_poly_x, r_poly_y, r_poly_lengths = poly_x, poly_y, poly_lengths
|
||||
|
||||
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=10,
|
||||
)
|
||||
|
||||
if refl_point is not None:
|
||||
# Convert reflection point back to lat/lon
|
||||
cos_lat = np.cos(np.radians(ref_lat))
|
||||
refl_lat = ref_lat + refl_point[1] / 110540.0
|
||||
refl_lon = ref_lon + refl_point[0] / (111320.0 * cos_lat)
|
||||
|
||||
return {
|
||||
'has_los': False,
|
||||
'path_type': 'reflection',
|
||||
'total_loss': refl_loss,
|
||||
'path_length': refl_length,
|
||||
'reflection_point': (refl_lat, refl_lon),
|
||||
}
|
||||
|
||||
# Step 3: Diffraction fallback
|
||||
num_blocking = int(np.sum(intersects))
|
||||
diffraction_loss = 10.0 + 5.0 * min(num_blocking, 5)
|
||||
|
||||
return {
|
||||
'has_los': False,
|
||||
'path_type': 'diffraction',
|
||||
'total_loss': diffraction_loss,
|
||||
'path_length': direct_dist,
|
||||
'reflection_point': None,
|
||||
}
|
||||
|
||||
|
||||
class DominantPathService:
|
||||
"""
|
||||
Find dominant propagation paths (2-3 strongest)
|
||||
|
||||
309
backend/app/services/geometry_vectorized.py
Normal file
309
backend/app/services/geometry_vectorized.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Vectorized geometry operations using NumPy.
|
||||
|
||||
All functions operate on arrays, not single values.
|
||||
Provides 10-50x speedup over Python loops for batch geometry checks.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from typing import Tuple, Optional
|
||||
|
||||
EARTH_RADIUS = 6371000 # meters
|
||||
|
||||
|
||||
def haversine_batch(
|
||||
lat1: float, lon1: float,
|
||||
lats2: np.ndarray, lons2: np.ndarray,
|
||||
) -> np.ndarray:
|
||||
"""Distance from one point to many points (meters)."""
|
||||
lat1_rad = np.radians(lat1)
|
||||
lon1_rad = np.radians(lon1)
|
||||
lats2_rad = np.radians(lats2)
|
||||
lons2_rad = np.radians(lons2)
|
||||
|
||||
dlat = lats2_rad - lat1_rad
|
||||
dlon = lons2_rad - lon1_rad
|
||||
|
||||
a = np.sin(dlat / 2) ** 2 + np.cos(lat1_rad) * np.cos(lats2_rad) * np.sin(dlon / 2) ** 2
|
||||
c = 2 * np.arcsin(np.sqrt(a))
|
||||
|
||||
return EARTH_RADIUS * c
|
||||
|
||||
|
||||
def points_to_local_coords(
|
||||
ref_lat: float, ref_lon: float,
|
||||
lats: np.ndarray, lons: np.ndarray,
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Convert lat/lon to local X/Y meters (equirectangular projection)."""
|
||||
cos_lat = np.cos(np.radians(ref_lat))
|
||||
x = (lons - ref_lon) * 111320.0 * cos_lat
|
||||
y = (lats - ref_lat) * 110540.0
|
||||
return x, y
|
||||
|
||||
|
||||
def line_segments_intersect_batch(
|
||||
p1: np.ndarray, p2: np.ndarray,
|
||||
segments_start: np.ndarray, segments_end: np.ndarray,
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Check if line p1->p2 intersects with N segments.
|
||||
|
||||
Args:
|
||||
p1, p2: shape (2,)
|
||||
segments_start, segments_end: shape (N, 2)
|
||||
|
||||
Returns:
|
||||
intersects: bool array (N,)
|
||||
t_values: parameter along p1->p2 (N,)
|
||||
"""
|
||||
d = p2 - p1
|
||||
seg_d = segments_end - segments_start
|
||||
|
||||
cross = d[0] * seg_d[:, 1] - d[1] * seg_d[:, 0]
|
||||
|
||||
parallel_mask = np.abs(cross) < 1e-10
|
||||
cross_safe = np.where(parallel_mask, 1.0, cross)
|
||||
|
||||
dp = p1 - segments_start
|
||||
|
||||
t = (dp[:, 0] * seg_d[:, 1] - dp[:, 1] * seg_d[:, 0]) / cross_safe
|
||||
u = (dp[:, 0] * d[1] - dp[:, 1] * d[0]) / cross_safe
|
||||
|
||||
intersects = ~parallel_mask & (t >= 0) & (t <= 1) & (u >= 0) & (u <= 1)
|
||||
|
||||
return intersects, t
|
||||
|
||||
|
||||
def line_intersects_polygons_batch(
|
||||
p1: np.ndarray, p2: np.ndarray,
|
||||
polygons_x: np.ndarray, polygons_y: np.ndarray,
|
||||
polygon_lengths: np.ndarray,
|
||||
max_polygons: int = 30,
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Check if line p1->p2 intersects multiple polygons.
|
||||
|
||||
Args:
|
||||
p1, p2: shape (2,)
|
||||
polygons_x, polygons_y: flattened vertex arrays
|
||||
polygon_lengths: vertices per polygon (num_polygons,)
|
||||
max_polygons: only check nearest N polygons (bbox pre-filter)
|
||||
|
||||
Returns:
|
||||
intersects: bool (num_polygons,)
|
||||
min_distances: distance to first hit (num_polygons,)
|
||||
"""
|
||||
num_polygons = len(polygon_lengths)
|
||||
|
||||
if num_polygons == 0:
|
||||
return np.array([], dtype=bool), np.array([])
|
||||
|
||||
intersects = np.zeros(num_polygons, dtype=bool)
|
||||
min_t = np.full(num_polygons, np.inf)
|
||||
|
||||
# Pre-filter: only check polygons whose first vertex is near the line bbox
|
||||
if num_polygons > max_polygons:
|
||||
buf = 50.0 # 50m buffer
|
||||
line_min_x = min(p1[0], p2[0]) - buf
|
||||
line_max_x = max(p1[0], p2[0]) + buf
|
||||
line_min_y = min(p1[1], p2[1]) - buf
|
||||
line_max_y = max(p1[1], p2[1]) + buf
|
||||
|
||||
nearby_mask = np.zeros(num_polygons, dtype=bool)
|
||||
vi = 0
|
||||
for i, length in enumerate(polygon_lengths):
|
||||
if length >= 3:
|
||||
cx = polygons_x[vi]
|
||||
cy = polygons_y[vi]
|
||||
if line_min_x <= cx <= line_max_x and line_min_y <= cy <= line_max_y:
|
||||
nearby_mask[i] = True
|
||||
vi += length
|
||||
|
||||
# Cap at max_polygons
|
||||
nearby_indices = np.where(nearby_mask)[0]
|
||||
if len(nearby_indices) > max_polygons:
|
||||
nearby_mask = np.zeros(num_polygons, dtype=bool)
|
||||
nearby_mask[nearby_indices[:max_polygons]] = True
|
||||
else:
|
||||
nearby_mask = np.ones(num_polygons, dtype=bool)
|
||||
|
||||
idx = 0
|
||||
for i, length in enumerate(polygon_lengths):
|
||||
if length < 3 or not nearby_mask[i]:
|
||||
idx += length
|
||||
continue
|
||||
|
||||
px = polygons_x[idx:idx + length]
|
||||
py = polygons_y[idx:idx + length]
|
||||
|
||||
starts = np.stack([px, py], axis=1)
|
||||
ends = np.stack([np.roll(px, -1), np.roll(py, -1)], axis=1)
|
||||
|
||||
edge_intersects, t_vals = line_segments_intersect_batch(p1, p2, starts, ends)
|
||||
|
||||
if np.any(edge_intersects):
|
||||
intersects[i] = True
|
||||
min_t[i] = np.min(t_vals[edge_intersects])
|
||||
|
||||
idx += length
|
||||
|
||||
line_length = np.linalg.norm(p2 - p1)
|
||||
min_distances = min_t * line_length
|
||||
|
||||
return intersects, min_distances
|
||||
|
||||
|
||||
def calculate_reflection_points_batch(
|
||||
tx: np.ndarray, rx: np.ndarray,
|
||||
wall_starts: np.ndarray, wall_ends: np.ndarray,
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Calculate reflection points on N walls via mirror-image method.
|
||||
|
||||
Args:
|
||||
tx, rx: shape (2,)
|
||||
wall_starts, wall_ends: shape (N, 2)
|
||||
|
||||
Returns:
|
||||
reflection_points: (N, 2)
|
||||
valid: bool (N,)
|
||||
"""
|
||||
wall_vec = wall_ends - wall_starts
|
||||
wall_length = np.linalg.norm(wall_vec, axis=1, keepdims=True)
|
||||
wall_unit = wall_vec / np.maximum(wall_length, 1e-10)
|
||||
|
||||
normals = np.stack([-wall_unit[:, 1], wall_unit[:, 0]], axis=1)
|
||||
|
||||
tx_to_wall = tx - wall_starts
|
||||
tx_dist_to_wall = np.sum(tx_to_wall * normals, axis=1, keepdims=True)
|
||||
tx_mirror = tx - 2 * tx_dist_to_wall * normals
|
||||
|
||||
rx_to_mirror = tx_mirror - rx
|
||||
|
||||
cross_denom = (rx_to_mirror[:, 0] * wall_vec[:, 1] -
|
||||
rx_to_mirror[:, 1] * wall_vec[:, 0])
|
||||
|
||||
valid_denom = np.abs(cross_denom) > 1e-10
|
||||
cross_denom_safe = np.where(valid_denom, cross_denom, 1.0)
|
||||
|
||||
rx_to_start = wall_starts - rx
|
||||
t = (rx_to_start[:, 0] * rx_to_mirror[:, 1] -
|
||||
rx_to_start[:, 1] * rx_to_mirror[:, 0]) / cross_denom_safe
|
||||
|
||||
reflection_points = wall_starts + t[:, np.newaxis] * wall_vec
|
||||
|
||||
valid = valid_denom & (t >= 0) & (t <= 1) & (tx_dist_to_wall[:, 0] > 0)
|
||||
|
||||
return reflection_points, valid
|
||||
|
||||
|
||||
def find_best_reflection_path_vectorized(
|
||||
tx: np.ndarray, rx: np.ndarray,
|
||||
building_walls_start: np.ndarray,
|
||||
building_walls_end: np.ndarray,
|
||||
wall_to_building: np.ndarray,
|
||||
obstacle_polygons_x: np.ndarray,
|
||||
obstacle_polygons_y: np.ndarray,
|
||||
obstacle_lengths: np.ndarray,
|
||||
max_candidates: int = 50,
|
||||
max_walls: int = 100,
|
||||
max_los_checks: int = 10,
|
||||
) -> Tuple[Optional[np.ndarray], float, float]:
|
||||
"""Find best single-reflection path using vectorized ops.
|
||||
|
||||
Args:
|
||||
max_walls: Only consider closest N walls for reflection candidates.
|
||||
max_los_checks: Only verify LOS for top N shortest reflection paths.
|
||||
|
||||
Returns:
|
||||
best_reflection_point: (2,) or None
|
||||
best_path_length: meters
|
||||
best_reflection_loss: dB
|
||||
"""
|
||||
num_walls = len(building_walls_start)
|
||||
if num_walls == 0:
|
||||
return None, np.inf, 0.0
|
||||
|
||||
# Limit walls by distance to path midpoint
|
||||
if num_walls > max_walls:
|
||||
midpoint = (tx + rx) / 2
|
||||
wall_midpoints = (building_walls_start + building_walls_end) / 2
|
||||
wall_distances = np.linalg.norm(wall_midpoints - midpoint, axis=1)
|
||||
closest = np.argpartition(wall_distances, max_walls)[:max_walls]
|
||||
building_walls_start = building_walls_start[closest]
|
||||
building_walls_end = building_walls_end[closest]
|
||||
wall_to_building = wall_to_building[closest]
|
||||
|
||||
refl_points, valid = calculate_reflection_points_batch(
|
||||
tx, rx, building_walls_start, building_walls_end,
|
||||
)
|
||||
|
||||
if not np.any(valid):
|
||||
return None, np.inf, 0.0
|
||||
|
||||
valid_indices = np.where(valid)[0]
|
||||
valid_refl = refl_points[valid]
|
||||
|
||||
tx_to_refl = np.linalg.norm(valid_refl - tx, axis=1)
|
||||
refl_to_rx = np.linalg.norm(rx - valid_refl, axis=1)
|
||||
path_lengths = tx_to_refl + refl_to_rx
|
||||
|
||||
# Direct distance filter: skip if reflection path > 2x direct
|
||||
direct_dist = np.linalg.norm(rx - tx)
|
||||
within_range = path_lengths <= direct_dist * 2.0
|
||||
if not np.any(within_range):
|
||||
return None, np.inf, 0.0
|
||||
|
||||
valid_indices = valid_indices[within_range]
|
||||
valid_refl = valid_refl[within_range]
|
||||
path_lengths = path_lengths[within_range]
|
||||
|
||||
# Keep top candidates by shortest path
|
||||
if len(valid_indices) > max_candidates:
|
||||
top_idx = np.argpartition(path_lengths, max_candidates)[:max_candidates]
|
||||
valid_indices = valid_indices[top_idx]
|
||||
valid_refl = valid_refl[top_idx]
|
||||
path_lengths = path_lengths[top_idx]
|
||||
|
||||
# Sort by path length for early exit
|
||||
sort_order = np.argsort(path_lengths)
|
||||
valid_refl = valid_refl[sort_order]
|
||||
path_lengths = path_lengths[sort_order]
|
||||
|
||||
# Check LOS only for top N shortest candidates
|
||||
check_count = min(len(valid_refl), max_los_checks)
|
||||
best_idx = -1
|
||||
best_length = np.inf
|
||||
|
||||
for i in range(check_count):
|
||||
length = path_lengths[i]
|
||||
if length >= best_length:
|
||||
continue
|
||||
|
||||
refl_pt = valid_refl[i]
|
||||
|
||||
# TX -> reflection LOS
|
||||
intersects1, _ = line_intersects_polygons_batch(
|
||||
tx, refl_pt, obstacle_polygons_x, obstacle_polygons_y, obstacle_lengths,
|
||||
)
|
||||
if np.any(intersects1):
|
||||
continue
|
||||
|
||||
# Reflection -> RX LOS
|
||||
intersects2, _ = line_intersects_polygons_batch(
|
||||
refl_pt, rx, obstacle_polygons_x, obstacle_polygons_y, obstacle_lengths,
|
||||
)
|
||||
if np.any(intersects2):
|
||||
continue
|
||||
|
||||
best_idx = i
|
||||
best_length = length
|
||||
break # sorted by length, first valid is best
|
||||
|
||||
if best_idx < 0:
|
||||
return None, np.inf, 0.0
|
||||
|
||||
best_point = valid_refl[best_idx]
|
||||
|
||||
# Reflection loss: 3-10 dB depending on path ratio
|
||||
path_ratio = best_length / max(direct_dist, 1.0)
|
||||
reflection_loss = 3.0 + 7.0 * min(1.0, (path_ratio - 1.0) * 2)
|
||||
|
||||
return best_point, best_length, reflection_loss
|
||||
140
desktop/main.js
140
desktop/main.js
@@ -269,17 +269,23 @@ function createMainWindow() {
|
||||
});
|
||||
|
||||
// Save window state on close and trigger shutdown
|
||||
mainWindow.on('close', () => {
|
||||
mainWindow.on('close', (event) => {
|
||||
log('[CLOSE] Window close event fired, isQuitting=' + isQuitting);
|
||||
try {
|
||||
const bounds = mainWindow.getBounds();
|
||||
store.set('windowState', bounds);
|
||||
} catch (_e) {}
|
||||
isQuitting = true;
|
||||
// Graceful shutdown is async but we also do sync kill as safety net
|
||||
gracefulShutdown().catch(() => {});
|
||||
killBackend();
|
||||
killAllBackendProcesses();
|
||||
|
||||
if (!isQuitting) {
|
||||
event.preventDefault();
|
||||
isQuitting = true;
|
||||
gracefulShutdown().then(() => {
|
||||
app.quit();
|
||||
}).catch(() => {
|
||||
killAllRfcpProcesses();
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Load frontend
|
||||
@@ -364,41 +370,86 @@ function killBackend() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Nuclear option: kill ALL rfcp-server processes by name.
|
||||
* This catches orphaned workers that PID-based kill misses.
|
||||
* Aggressive kill: multiple strategies to ensure ALL rfcp-server processes die.
|
||||
* Uses 4 strategies on Windows for maximum reliability.
|
||||
*/
|
||||
function killAllBackendProcesses() {
|
||||
log('[KILL] killAllBackendProcesses() — killing by process name...');
|
||||
function killAllRfcpProcesses() {
|
||||
log('[KILL] === Starting aggressive kill ===');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Strategy 1: Kill by image name (most reliable)
|
||||
try {
|
||||
execSync('taskkill /F /IM rfcp-server.exe /T', {
|
||||
stdio: 'ignore',
|
||||
timeout: 5000
|
||||
log('[KILL] Strategy 1: taskkill /F /IM');
|
||||
execSync('taskkill /F /IM rfcp-server.exe', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
log('[KILL] taskkill /IM rfcp-server.exe completed');
|
||||
log('[KILL] Strategy 1: SUCCESS');
|
||||
} catch (_e) {
|
||||
// Error means no processes found — OK
|
||||
log('[KILL] No rfcp-server.exe processes found (or already killed)');
|
||||
log('[KILL] Strategy 1: No processes or already killed');
|
||||
}
|
||||
|
||||
// Strategy 2: Kill by PID tree if we have PID
|
||||
if (backendPid) {
|
||||
try {
|
||||
log(`[KILL] Strategy 2: taskkill /F /T /PID ${backendPid}`);
|
||||
execSync(`taskkill /F /T /PID ${backendPid}`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
log('[KILL] Strategy 2: SUCCESS');
|
||||
} catch (_e) {
|
||||
log('[KILL] Strategy 2: PID not found');
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: PowerShell kill (backup)
|
||||
try {
|
||||
log('[KILL] Strategy 3: PowerShell Stop-Process');
|
||||
execSync('powershell -Command "Get-Process rfcp-server -ErrorAction SilentlyContinue | Stop-Process -Force"', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
log('[KILL] Strategy 3: SUCCESS');
|
||||
} catch (_e) {
|
||||
log('[KILL] Strategy 3: PowerShell failed or no processes');
|
||||
}
|
||||
|
||||
// Strategy 4: PowerShell CimInstance terminate (modern replacement for wmic)
|
||||
try {
|
||||
log('[KILL] Strategy 4: PowerShell CimInstance Terminate');
|
||||
execSync('powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"name=\'rfcp-server.exe\'\\" | Invoke-CimMethod -MethodName Terminate"', {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
});
|
||||
log('[KILL] Strategy 4: SUCCESS');
|
||||
} catch (_e) {
|
||||
log('[KILL] Strategy 4: No processes or failed');
|
||||
}
|
||||
} else {
|
||||
// Unix: pkill
|
||||
try {
|
||||
execSync('pkill -9 -f rfcp-server', {
|
||||
stdio: 'ignore',
|
||||
timeout: 5000
|
||||
});
|
||||
execSync('pkill -9 -f rfcp-server', { stdio: 'pipe', timeout: 5000 });
|
||||
log('[KILL] pkill rfcp-server completed');
|
||||
} catch (_e) {
|
||||
log('[KILL] No rfcp-server processes found');
|
||||
}
|
||||
}
|
||||
|
||||
backendPid = null;
|
||||
backendProcess = null;
|
||||
log('[KILL] === Kill sequence complete ===');
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful shutdown: ask backend to clean up, then force kill everything.
|
||||
* Graceful shutdown: API call first, then multi-strategy force kill.
|
||||
*/
|
||||
async function gracefulShutdown() {
|
||||
log('[SHUTDOWN] Requesting graceful shutdown...');
|
||||
log('[SHUTDOWN] Starting graceful shutdown...');
|
||||
|
||||
// Step 1: Ask backend to clean up workers and exit
|
||||
try {
|
||||
@@ -410,18 +461,18 @@ async function gracefulShutdown() {
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
log('[SHUTDOWN] Backend acknowledged shutdown');
|
||||
// Wait for backend to cleanup
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
} catch (_e) {
|
||||
log('[SHUTDOWN] Backend did not respond — force killing');
|
||||
}
|
||||
|
||||
// Step 2: Wait briefly for graceful exit
|
||||
// Step 2: Force kill everything
|
||||
killAllRfcpProcesses();
|
||||
|
||||
// Step 3: Wait and verify
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// Step 3: PID-based kill (catches the main process)
|
||||
killBackend();
|
||||
|
||||
// Step 4: Name-based kill (catches orphaned workers)
|
||||
killAllBackendProcesses();
|
||||
log('[SHUTDOWN] Shutdown complete');
|
||||
}
|
||||
|
||||
// ── App lifecycle ──────────────────────────────────────────────────
|
||||
@@ -456,8 +507,7 @@ app.whenReady().then(async () => {
|
||||
app.on('window-all-closed', () => {
|
||||
log('[CLOSE] window-all-closed fired');
|
||||
isQuitting = true;
|
||||
killBackend();
|
||||
killAllBackendProcesses();
|
||||
killAllRfcpProcesses();
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
@@ -470,17 +520,23 @@ app.on('activate', () => {
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
log('[CLOSE] before-quit fired');
|
||||
isQuitting = true;
|
||||
killBackend();
|
||||
killAllBackendProcesses();
|
||||
app.on('before-quit', (event) => {
|
||||
log('[CLOSE] before-quit fired, isQuitting=' + isQuitting);
|
||||
if (!isQuitting) {
|
||||
event.preventDefault();
|
||||
isQuitting = true;
|
||||
gracefulShutdown().then(() => {
|
||||
app.quit();
|
||||
}).catch(() => {
|
||||
killAllRfcpProcesses();
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.on('will-quit', () => {
|
||||
log('[CLOSE] will-quit fired');
|
||||
killBackend();
|
||||
killAllBackendProcesses();
|
||||
killAllRfcpProcesses();
|
||||
|
||||
if (backendLogStream) {
|
||||
try { backendLogStream.end(); } catch (_e) {}
|
||||
@@ -508,21 +564,19 @@ process.on('exit', () => {
|
||||
}
|
||||
|
||||
// Name-based kill — catches orphaned workers
|
||||
killAllBackendProcesses();
|
||||
killAllRfcpProcesses();
|
||||
});
|
||||
|
||||
// Handle SIGINT/SIGTERM (Ctrl+C, system shutdown)
|
||||
process.on('SIGINT', () => {
|
||||
try { log('[SIGNAL] SIGINT received'); } catch (_e) {}
|
||||
killBackend();
|
||||
killAllBackendProcesses();
|
||||
killAllRfcpProcesses();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
try { log('[SIGNAL] SIGTERM received'); } catch (_e) {}
|
||||
killBackend();
|
||||
killAllBackendProcesses();
|
||||
killAllRfcpProcesses();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"detail":"Calculation timeout (5 min). Cleaned up 6 workers."}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
49
installer/monitor-realtime.bat
Normal file
49
installer/monitor-realtime.bat
Normal file
@@ -0,0 +1,49 @@
|
||||
@echo off
|
||||
title RFCP Real-time Resource Monitor
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
echo ============================================
|
||||
echo RFCP Real-time Resource Monitor
|
||||
echo ============================================
|
||||
echo Press Ctrl+C to stop
|
||||
echo ============================================
|
||||
echo.
|
||||
echo TIME CPU%% MEM(MB) PROCS FREE_RAM(MB)
|
||||
echo ---------- ----- ------- ----- ------------
|
||||
|
||||
:loop
|
||||
|
||||
:: Get current time
|
||||
set T=%time:~0,8%
|
||||
|
||||
:: Count RFCP processes and their memory
|
||||
set PROC_COUNT=0
|
||||
set TOTAL_MEM=0
|
||||
for /f "skip=3 tokens=5 delims= " %%m in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul') do (
|
||||
set /a PROC_COUNT+=1
|
||||
set MEM_STR=%%m
|
||||
set MEM_STR=!MEM_STR:,=!
|
||||
set MEM_STR=!MEM_STR: =!
|
||||
if "!MEM_STR!" NEQ "" (
|
||||
set /a TOTAL_MEM+=!MEM_STR! 2>nul
|
||||
)
|
||||
)
|
||||
set /a TOTAL_MEM_MB=TOTAL_MEM/1024 2>nul
|
||||
|
||||
:: Get free RAM (PowerShell — wmic deprecated in Win11)
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" 2^>nul') do (
|
||||
set /a FREE_RAM=%%a/1024 2>nul
|
||||
)
|
||||
|
||||
:: Get CPU load (PowerShell — wmic deprecated in Win11)
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_Processor).LoadPercentage" 2^>nul') do (
|
||||
set CPU=%%a
|
||||
)
|
||||
|
||||
:: Display
|
||||
echo %T% %CPU%%% %TOTAL_MEM_MB% %PROC_COUNT% %FREE_RAM%
|
||||
|
||||
:: Wait 2 seconds
|
||||
timeout /t 2 /nobreak >nul
|
||||
|
||||
goto loop
|
||||
93
installer/rfcp-debug-enhanced.bat
Normal file
93
installer/rfcp-debug-enhanced.bat
Normal file
@@ -0,0 +1,93 @@
|
||||
@echo off
|
||||
title RFCP Debug Launcher + Monitor
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
echo ============================================
|
||||
echo RFCP Debug Launcher + Monitor
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
:: Kill any running instance
|
||||
echo [1/3] Cleaning up existing processes...
|
||||
taskkill /F /IM rfcp-server.exe 2>nul
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Killed existing rfcp-server.exe
|
||||
timeout /t 2 /nobreak >nul
|
||||
) else (
|
||||
echo No existing process found
|
||||
)
|
||||
echo.
|
||||
|
||||
:: Environment setup
|
||||
set PYTHONUNBUFFERED=1
|
||||
set RFCP_DEBUG=1
|
||||
set RFCP_HOST=127.0.0.1
|
||||
set RFCP_PORT=8888
|
||||
|
||||
echo [2/3] Environment:
|
||||
echo PYTHONUNBUFFERED=%PYTHONUNBUFFERED%
|
||||
echo RFCP_DEBUG=%RFCP_DEBUG%
|
||||
echo RFCP_HOST=%RFCP_HOST%
|
||||
echo RFCP_PORT=%RFCP_PORT%
|
||||
echo.
|
||||
|
||||
:: Find executable
|
||||
if exist "%~dp0dist\rfcp-server.exe" (
|
||||
set EXE_PATH=%~dp0dist\rfcp-server.exe
|
||||
set WORK_DIR=%~dp0dist
|
||||
) else if exist "%~dp0rfcp-server.exe" (
|
||||
set EXE_PATH=%~dp0rfcp-server.exe
|
||||
set WORK_DIR=%~dp0
|
||||
) else (
|
||||
echo [ERROR] rfcp-server.exe not found!
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [3/3] Starting server...
|
||||
echo Executable: %EXE_PATH%
|
||||
echo Working dir: %WORK_DIR%
|
||||
echo.
|
||||
|
||||
:: Create log directory
|
||||
set LOG_DIR=%WORK_DIR%\logs
|
||||
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
|
||||
|
||||
:: Log file with timestamp
|
||||
set TIMESTAMP=%date:~-4%%date:~3,2%%date:~0,2%-%time:~0,2%%time:~3,2%%time:~6,2%
|
||||
set TIMESTAMP=%TIMESTAMP: =0%
|
||||
set LOG_FILE=%LOG_DIR%\server-%TIMESTAMP%.log
|
||||
|
||||
echo Log file: %LOG_FILE%
|
||||
echo.
|
||||
echo ============================================
|
||||
echo SERVER OUTPUT (also saved to log)
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
cd /d "%WORK_DIR%"
|
||||
|
||||
:: Run server and tee to log file
|
||||
:: Note: PowerShell tee for dual output
|
||||
powershell -Command "& '%EXE_PATH%' 2>&1 | Tee-Object -FilePath '%LOG_FILE%'"
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Server stopped
|
||||
echo Log saved to: %LOG_FILE%
|
||||
echo ============================================
|
||||
|
||||
:: Post-mortem check
|
||||
echo.
|
||||
echo [POST-MORTEM] Checking for orphan processes...
|
||||
for /f %%a in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul ^| find /c "rfcp-server"') do (
|
||||
if %%a GTR 0 (
|
||||
echo WARNING: %%a rfcp-server process(es) still running!
|
||||
echo Run: taskkill /F /IM rfcp-server.exe
|
||||
) else (
|
||||
echo All processes cleaned up properly.
|
||||
)
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
282
installer/test-coverage-monitor.bat
Normal file
282
installer/test-coverage-monitor.bat
Normal file
@@ -0,0 +1,282 @@
|
||||
@echo off
|
||||
title RFCP Coverage API Test + Resource Monitor
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
echo ============================================
|
||||
echo RFCP Coverage API Test + Resource Monitor
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
set API=http://127.0.0.1:8888
|
||||
set RESULTS_DIR=%~dp0test-results
|
||||
set TIMESTAMP=%date:~-4%%date:~3,2%%date:~0,2%-%time:~0,2%%time:~3,2%
|
||||
set TIMESTAMP=%TIMESTAMP: =0%
|
||||
|
||||
:: Create results directory
|
||||
if not exist "%RESULTS_DIR%" mkdir "%RESULTS_DIR%"
|
||||
|
||||
:: Log file for this run
|
||||
set LOG_FILE=%RESULTS_DIR%\test-run-%TIMESTAMP%.log
|
||||
|
||||
echo Test started: %date% %time% > "%LOG_FILE%"
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
:: ===========================================
|
||||
:: SYSTEM INFO
|
||||
:: ===========================================
|
||||
echo [SYSTEM] Collecting system info...
|
||||
echo. >> "%LOG_FILE%"
|
||||
echo === SYSTEM INFO === >> "%LOG_FILE%"
|
||||
|
||||
:: CPU info (PowerShell — wmic deprecated in Win11)
|
||||
for /f "delims=" %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_Processor).Name"') do (
|
||||
echo CPU: %%a >> "%LOG_FILE%"
|
||||
echo CPU: %%a
|
||||
)
|
||||
|
||||
:: RAM info (PowerShell)
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize"') do (
|
||||
set /a RAM_GB=%%a/1024/1024
|
||||
echo RAM: !RAM_GB! GB >> "%LOG_FILE%"
|
||||
echo RAM: !RAM_GB! GB
|
||||
)
|
||||
|
||||
:: GPU info (PowerShell)
|
||||
echo GPU: >> "%LOG_FILE%"
|
||||
for /f "delims=" %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_VideoController).Name"') do (
|
||||
echo %%a >> "%LOG_FILE%"
|
||||
echo %%a
|
||||
)
|
||||
|
||||
echo. >> "%LOG_FILE%"
|
||||
echo.
|
||||
|
||||
:: ===========================================
|
||||
:: PRE-TEST BASELINE
|
||||
:: ===========================================
|
||||
echo [BASELINE] Capturing baseline resource usage...
|
||||
|
||||
:: Get baseline memory
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do (
|
||||
set /a BASELINE_FREE_MB=%%a/1024
|
||||
)
|
||||
echo Free RAM before: %BASELINE_FREE_MB% MB
|
||||
echo Baseline free RAM: %BASELINE_FREE_MB% MB >> "%LOG_FILE%"
|
||||
|
||||
:: Count rfcp processes
|
||||
set RFCP_COUNT=0
|
||||
for /f %%a in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul ^| find /c "rfcp-server"') do set RFCP_COUNT=%%a
|
||||
echo RFCP processes: %RFCP_COUNT%
|
||||
echo Baseline RFCP processes: %RFCP_COUNT% >> "%LOG_FILE%"
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
:: ===========================================
|
||||
:: TEST 1: Health check
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo [TEST 1] Health check...
|
||||
echo === TEST 1: Health Check === >> "%LOG_FILE%"
|
||||
|
||||
curl -s -o nul -w "HTTP %%{http_code}\n" %API%/api/health
|
||||
curl -s -w "HTTP %%{http_code}" %API%/api/health >> "%LOG_FILE%"
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
:: ===========================================
|
||||
:: TEST 2: System info
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo [TEST 2] Backend system info:
|
||||
echo === TEST 2: Backend System Info === >> "%LOG_FILE%"
|
||||
|
||||
curl -s %API%/api/system/info
|
||||
curl -s %API%/api/system/info >> "%LOG_FILE%"
|
||||
echo.
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
:: ===========================================
|
||||
:: TEST 3: Fast preset
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo [TEST 3] Coverage - Fast preset (2km, 500m res)
|
||||
echo Expected: ^< 1 second
|
||||
echo === TEST 3: Fast Preset === >> "%LOG_FILE%"
|
||||
|
||||
:: Capture start memory
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a START_FREE=%%a/1024
|
||||
|
||||
set START_TIME=%time%
|
||||
curl -s -w "\nTime: %%{time_total}s | HTTP: %%{http_code}" ^
|
||||
-X POST %API%/api/coverage/calculate ^
|
||||
-H "Content-Type: application/json" ^
|
||||
-d "{\"sites\": [{\"lat\": 50.45, \"lon\": 30.52, \"height\": 30, \"power\": 43, \"gain\": 15, \"frequency\": 1800}], \"settings\": {\"radius\": 2000, \"resolution\": 500, \"preset\": \"fast\"}}" ^
|
||||
-o "%RESULTS_DIR%\coverage-fast.json"
|
||||
|
||||
set END_TIME=%time%
|
||||
|
||||
:: Capture end memory
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a END_FREE=%%a/1024
|
||||
set /a MEM_USED=START_FREE-END_FREE
|
||||
|
||||
echo.
|
||||
echo Memory delta: %MEM_USED% MB
|
||||
echo Time: %START_TIME% - %END_TIME% >> "%LOG_FILE%"
|
||||
echo Memory delta: %MEM_USED% MB >> "%LOG_FILE%"
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
:: ===========================================
|
||||
:: TEST 4: Standard preset
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo [TEST 4] Coverage - Standard preset (5km, 300m res)
|
||||
echo Expected: 30-45 seconds
|
||||
echo === TEST 4: Standard Preset === >> "%LOG_FILE%"
|
||||
|
||||
:: Monitor resources during test
|
||||
echo Starting resource monitor...
|
||||
start /b cmd /c "for /l %%i in (1,1,120) do (for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" 2^>nul') do echo [%%i] Free: %%a KB >> "%RESULTS_DIR%\monitor-standard.log") & timeout /t 1 /nobreak >nul 2>&1"
|
||||
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a START_FREE=%%a/1024
|
||||
|
||||
curl -s -w "\nTime: %%{time_total}s | HTTP: %%{http_code}" ^
|
||||
-X POST %API%/api/coverage/calculate ^
|
||||
-H "Content-Type: application/json" ^
|
||||
-d "{\"sites\": [{\"lat\": 50.45, \"lon\": 30.52, \"height\": 30, \"power\": 43, \"gain\": 15, \"frequency\": 1800}], \"settings\": {\"radius\": 5000, \"resolution\": 300, \"preset\": \"standard\"}}" ^
|
||||
-o "%RESULTS_DIR%\coverage-standard.json"
|
||||
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a END_FREE=%%a/1024
|
||||
set /a MEM_USED=START_FREE-END_FREE
|
||||
set /a PEAK_FROM_BASELINE=BASELINE_FREE_MB-END_FREE
|
||||
|
||||
echo.
|
||||
echo Memory delta: %MEM_USED% MB (peak from baseline: %PEAK_FROM_BASELINE% MB)
|
||||
echo Memory delta: %MEM_USED% MB >> "%LOG_FILE%"
|
||||
|
||||
:: Count RFCP processes
|
||||
for /f %%a in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul ^| find /c "rfcp-server"') do set RFCP_COUNT=%%a
|
||||
echo RFCP processes: %RFCP_COUNT%
|
||||
echo RFCP processes: %RFCP_COUNT% >> "%LOG_FILE%"
|
||||
echo. >> "%LOG_FILE%"
|
||||
|
||||
:: ===========================================
|
||||
:: TEST 5: Detailed preset (the big one!)
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo [TEST 5] Coverage - Detailed preset (5km, 300m res)
|
||||
echo Expected: ^< 90 seconds (was 300s timeout)
|
||||
echo THIS IS THE VECTORIZATION TEST!
|
||||
echo === TEST 5: Detailed Preset (VECTORIZATION TEST) === >> "%LOG_FILE%"
|
||||
|
||||
:: Start intensive resource monitor
|
||||
echo Starting intensive resource monitor (every 2s)...
|
||||
start /b cmd /c "for /l %%i in (1,1,180) do (for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" 2^>nul') do echo [%%i] Free: %%a KB >> "%RESULTS_DIR%\monitor-detailed.log") & timeout /t 2 /nobreak >nul 2>&1"
|
||||
|
||||
:: CPU monitor (approximate via tasklist)
|
||||
start /b cmd /c "for /l %%i in (1,1,180) do (for /f "skip=2 tokens=5" %%c in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" /FO LIST 2^>nul ^| findstr "Mem"') do echo [%%i] RFCP Mem: %%c >> "%RESULTS_DIR%\monitor-rfcp.log") & timeout /t 2 /nobreak >nul 2>&1"
|
||||
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a START_FREE=%%a/1024
|
||||
|
||||
set START_DETAIL=%time%
|
||||
echo Start time: %START_DETAIL%
|
||||
|
||||
curl -s -w "\nTime: %%{time_total}s | HTTP: %%{http_code}" ^
|
||||
-X POST %API%/api/coverage/calculate ^
|
||||
-H "Content-Type: application/json" ^
|
||||
-d "{\"sites\": [{\"lat\": 50.45, \"lon\": 30.52, \"height\": 30, \"power\": 43, \"gain\": 15, \"frequency\": 1800}], \"settings\": {\"radius\": 5000, \"resolution\": 300, \"preset\": \"detailed\"}}" ^
|
||||
-o "%RESULTS_DIR%\coverage-detailed.json" ^
|
||||
2>&1
|
||||
|
||||
set END_DETAIL=%time%
|
||||
echo End time: %END_DETAIL%
|
||||
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a END_FREE=%%a/1024
|
||||
set /a MEM_USED=START_FREE-END_FREE
|
||||
set /a PEAK_FROM_BASELINE=BASELINE_FREE_MB-END_FREE
|
||||
|
||||
echo.
|
||||
echo Memory delta: %MEM_USED% MB (peak from baseline: %PEAK_FROM_BASELINE% MB)
|
||||
echo Start: %START_DETAIL% >> "%LOG_FILE%"
|
||||
echo End: %END_DETAIL% >> "%LOG_FILE%"
|
||||
echo Memory delta: %MEM_USED% MB >> "%LOG_FILE%"
|
||||
|
||||
:: Check result
|
||||
if exist "%RESULTS_DIR%\coverage-detailed.json" (
|
||||
findstr /C:"timeout" "%RESULTS_DIR%\coverage-detailed.json" >nul 2>&1
|
||||
if !ERRORLEVEL! EQU 0 (
|
||||
echo RESULT: TIMEOUT - Vectorization didn't help enough
|
||||
echo RESULT: TIMEOUT >> "%LOG_FILE%"
|
||||
) else (
|
||||
findstr /C:"points" "%RESULTS_DIR%\coverage-detailed.json" >nul 2>&1
|
||||
if !ERRORLEVEL! EQU 0 (
|
||||
echo RESULT: SUCCESS - Calculation completed!
|
||||
echo RESULT: SUCCESS >> "%LOG_FILE%"
|
||||
|
||||
:: Extract point count
|
||||
for /f "tokens=2 delims=:" %%a in ('findstr /C:"count" "%RESULTS_DIR%\coverage-detailed.json"') do (
|
||||
echo Points calculated: %%a
|
||||
echo Points: %%a >> "%LOG_FILE%"
|
||||
)
|
||||
) else (
|
||||
echo RESULT: ERROR - Check JSON file
|
||||
echo RESULT: ERROR >> "%LOG_FILE%"
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
:: ===========================================
|
||||
:: POST-TEST CLEANUP CHECK
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo [CLEANUP] Checking post-test state...
|
||||
echo === POST-TEST CLEANUP === >> "%LOG_FILE%"
|
||||
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
:: Count RFCP processes
|
||||
for /f %%a in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul ^| find /c "rfcp-server"') do set RFCP_COUNT=%%a
|
||||
echo RFCP processes after test: %RFCP_COUNT%
|
||||
echo RFCP processes after: %RFCP_COUNT% >> "%LOG_FILE%"
|
||||
|
||||
:: Memory recovery
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a FINAL_FREE=%%a/1024
|
||||
set /a MEM_NOT_FREED=BASELINE_FREE_MB-FINAL_FREE
|
||||
|
||||
echo Free RAM now: %FINAL_FREE% MB (baseline was: %BASELINE_FREE_MB% MB)
|
||||
echo Memory not freed: %MEM_NOT_FREED% MB
|
||||
echo Final free RAM: %FINAL_FREE% MB >> "%LOG_FILE%"
|
||||
echo Memory not freed: %MEM_NOT_FREED% MB >> "%LOG_FILE%"
|
||||
|
||||
if %MEM_NOT_FREED% GTR 500 (
|
||||
echo WARNING: Possible memory leak - %MEM_NOT_FREED% MB not freed!
|
||||
echo WARNING: Memory leak detected >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
if %RFCP_COUNT% GTR 1 (
|
||||
echo WARNING: Multiple RFCP processes still running!
|
||||
echo WARNING: Multiple processes >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
:: ===========================================
|
||||
:: SUMMARY
|
||||
:: ===========================================
|
||||
echo.
|
||||
echo ============================================
|
||||
echo TEST SUMMARY
|
||||
echo ============================================
|
||||
echo.
|
||||
echo Results saved to: %RESULTS_DIR%
|
||||
echo Log file: %LOG_FILE%
|
||||
echo.
|
||||
echo Files created:
|
||||
echo - coverage-fast.json
|
||||
echo - coverage-standard.json
|
||||
echo - coverage-detailed.json
|
||||
echo - monitor-standard.log (memory during standard)
|
||||
echo - monitor-detailed.log (memory during detailed)
|
||||
echo - monitor-rfcp.log (rfcp process memory)
|
||||
echo.
|
||||
|
||||
echo === SUMMARY === >> "%LOG_FILE%"
|
||||
echo Test completed: %date% %time% >> "%LOG_FILE%"
|
||||
|
||||
echo ============================================
|
||||
pause
|
||||
128
installer/test-detailed-quick.bat
Normal file
128
installer/test-detailed-quick.bat
Normal file
@@ -0,0 +1,128 @@
|
||||
@echo off
|
||||
title RFCP - Detailed Preset Quick Test
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
echo ============================================
|
||||
echo RFCP - Detailed Preset Quick Test
|
||||
echo ============================================
|
||||
echo.
|
||||
echo This tests ONLY the Detailed preset to check
|
||||
echo if NumPy vectorization is working.
|
||||
echo.
|
||||
echo Expected: ^< 90 seconds (was 300s timeout)
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
set API=http://127.0.0.1:8888
|
||||
|
||||
:: Check if server is running
|
||||
echo [1/4] Checking server...
|
||||
curl -s -o nul -w "HTTP %%{http_code}" %API%/api/health >nul 2>&1
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo ERROR: Server not responding! Start rfcp-debug.bat first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo Server OK
|
||||
echo.
|
||||
|
||||
:: Get baseline
|
||||
echo [2/4] Capturing baseline...
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a BASE_FREE=%%a/1024
|
||||
for /f %%a in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul ^| find /c "rfcp-server"') do set BASE_PROCS=%%a
|
||||
echo Free RAM: %BASE_FREE% MB
|
||||
echo RFCP procs: %BASE_PROCS%
|
||||
echo.
|
||||
|
||||
:: Run test
|
||||
echo [3/4] Running Detailed preset (5km, 300m)...
|
||||
echo Start: %time%
|
||||
echo.
|
||||
|
||||
curl -s -w "\n HTTP: %%{http_code}\n Time: %%{time_total} seconds\n" ^
|
||||
-X POST %API%/api/coverage/calculate ^
|
||||
-H "Content-Type: application/json" ^
|
||||
-d "{\"sites\": [{\"lat\": 50.45, \"lon\": 30.52, \"height\": 30, \"power\": 43, \"gain\": 15, \"frequency\": 1800}], \"settings\": {\"radius\": 5000, \"resolution\": 300, \"preset\": \"detailed\"}}" ^
|
||||
-o detailed-result.json
|
||||
|
||||
echo.
|
||||
echo End: %time%
|
||||
echo.
|
||||
|
||||
:: Check result
|
||||
echo [4/4] Analyzing result...
|
||||
|
||||
:: Check for timeout
|
||||
findstr /C:"timeout" detailed-result.json >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ============================================
|
||||
echo RESULT: TIMEOUT
|
||||
echo Vectorization needs more optimization.
|
||||
echo ============================================
|
||||
echo.
|
||||
type detailed-result.json
|
||||
goto :cleanup
|
||||
)
|
||||
|
||||
:: Check for success (has "points")
|
||||
findstr /C:"\"points\"" detailed-result.json >nul 2>&1
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ============================================
|
||||
echo RESULT: SUCCESS!
|
||||
echo Detailed preset completed without timeout!
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
:: Extract stats
|
||||
echo Extracting stats from result...
|
||||
|
||||
:: Count points (rough)
|
||||
for /f "tokens=2 delims=:," %%a in ('findstr /C:"\"count\"" detailed-result.json') do (
|
||||
echo Points calculated: %%a
|
||||
)
|
||||
|
||||
:: Get computation time
|
||||
for /f "tokens=2 delims=:," %%a in ('findstr /C:"\"computation_time\"" detailed-result.json') do (
|
||||
echo Computation time: %%a seconds
|
||||
)
|
||||
|
||||
goto :cleanup
|
||||
)
|
||||
|
||||
:: Unknown result
|
||||
echo.
|
||||
echo RESULT: UNKNOWN
|
||||
echo Check detailed-result.json manually
|
||||
type detailed-result.json
|
||||
|
||||
:cleanup
|
||||
echo.
|
||||
echo ============================================
|
||||
echo POST-TEST CLEANUP CHECK
|
||||
echo ============================================
|
||||
|
||||
timeout /t 3 /nobreak >nul
|
||||
|
||||
for /f %%a in ('powershell -NoProfile -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory"') do set /a END_FREE=%%a/1024
|
||||
for /f %%a in ('tasklist /FI "IMAGENAME eq rfcp-server.exe" 2^>nul ^| find /c "rfcp-server"') do set END_PROCS=%%a
|
||||
|
||||
set /a MEM_DIFF=BASE_FREE-END_FREE
|
||||
|
||||
echo Free RAM: %END_FREE% MB (was %BASE_FREE% MB, diff: %MEM_DIFF% MB)
|
||||
echo RFCP procs: %END_PROCS% (was %BASE_PROCS%)
|
||||
|
||||
if %MEM_DIFF% GTR 500 (
|
||||
echo.
|
||||
echo WARNING: Memory not fully released!
|
||||
)
|
||||
|
||||
if %END_PROCS% GTR %BASE_PROCS% (
|
||||
echo.
|
||||
echo WARNING: Extra RFCP processes still running!
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
pause
|
||||
Reference in New Issue
Block a user