@mytec: before 3.0 REFACTOR

This commit is contained in:
2026-02-01 14:26:17 +02:00
parent acc90fe538
commit 1dde56705a
13 changed files with 1759 additions and 61 deletions

View 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

View File

@@ -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)

View File

@@ -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)

View 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

View File

@@ -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);
});

BIN
files.zip Normal file

Binary file not shown.

View File

@@ -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

View 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

View 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

View 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

View 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