mirror of
https://github.com/ruvnet/RuView
synced 2026-06-29 13:33:19 +00:00
81cc241b9e
The Rust port at v2/ has been the primary codebase since the rename in #427. The Python implementation at v1/ is no longer the active target; the only load-bearing path is the deterministic proof bundle at v1/data/proof/ (per ADR-011 / ADR-028 witness verification). Move the whole Python tree into archive/v1/ and document the policy in archive/README.md: no new features, bug fixes only when they affect a still-load-bearing path (currently just the proof), CI continues to verify the proof on every push and PR. Path references updated in 26 files via path-pattern sed (only matches v1/<known-child> patterns, never bare v1 or API URLs like /api/v1/). Two double-prefix typos (archive/archive/v1/) caught and hand-fixed in verify-pipeline.yml and ADR-011. Validated: - Python proof verify.py imports cleanly at archive/v1/data/proof/ (numpy/scipy still required; CI installs requirements-lock.txt from archive/v1/ now) - cargo test --workspace --no-default-features → 1,539 passed, 0 failed, 8 ignored (unaffected by Python tree relocation) - ESP32-S3 on COM7 untouched (no firmware paths changed) After-merge: contributors should re-run any local `python v1/...` commands as `python archive/v1/...` (CLAUDE.md and CHANGELOG already updated).
136 lines
4.4 KiB
Python
136 lines
4.4 KiB
Python
"""Frame budget benchmark for CSI processing pipeline.
|
|
|
|
Verifies that per-frame CSI processing stays within the 50 ms budget
|
|
required for real-time sensing at 20 FPS.
|
|
"""
|
|
|
|
import time
|
|
import statistics
|
|
import pytest
|
|
import numpy as np
|
|
|
|
from src.core.csi_processor import CSIProcessor
|
|
|
|
|
|
def _make_config():
|
|
return {
|
|
"sampling_rate": 1000,
|
|
"window_size": 256,
|
|
"overlap": 0.5,
|
|
"noise_threshold": -60,
|
|
"human_detection_threshold": 0.8,
|
|
"smoothing_factor": 0.9,
|
|
"max_history_size": 500,
|
|
"num_subcarriers": 256,
|
|
"num_antennas": 3,
|
|
"doppler_window": 64,
|
|
}
|
|
|
|
|
|
def _make_csi_data(n_subcarriers=256, n_antennas=3, seed=None):
|
|
"""Generate a synthetic CSI frame with complex-valued subcarriers."""
|
|
rng = np.random.default_rng(seed)
|
|
from unittest.mock import MagicMock
|
|
csi = MagicMock()
|
|
csi.amplitude = rng.random((n_antennas, n_subcarriers)).astype(np.float64) * 20.0
|
|
csi.phase = (rng.random((n_antennas, n_subcarriers)).astype(np.float64) - 0.5) * np.pi * 2
|
|
csi.frequency = 5.0e9
|
|
csi.bandwidth = 80e6
|
|
csi.num_subcarriers = n_subcarriers
|
|
csi.num_antennas = n_antennas
|
|
csi.snr = 25.0
|
|
csi.timestamp = time.time()
|
|
csi.metadata = {}
|
|
return csi
|
|
|
|
|
|
class TestSingleFrameBudget:
|
|
"""Single-frame processing must complete in < 50 ms."""
|
|
|
|
def test_single_frame_under_50ms(self):
|
|
proc = CSIProcessor(config=_make_config())
|
|
frame = _make_csi_data(seed=42)
|
|
|
|
# Warm up
|
|
proc.preprocess_csi_data(frame)
|
|
|
|
start = time.perf_counter()
|
|
proc.preprocess_csi_data(frame)
|
|
features = proc.extract_features(frame)
|
|
if features:
|
|
proc.detect_human_presence(features)
|
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
|
|
assert elapsed_ms < 50, f"Single frame took {elapsed_ms:.1f} ms (budget: 50 ms)"
|
|
|
|
|
|
class TestSustainedFrameBudget:
|
|
"""Sustained 100-frame processing p95 must be < 50 ms per frame."""
|
|
|
|
def test_sustained_100_frames_p95(self):
|
|
proc = CSIProcessor(config=_make_config())
|
|
rng = np.random.default_rng(123)
|
|
n_frames = 100
|
|
latencies = []
|
|
|
|
for i in range(n_frames):
|
|
frame = _make_csi_data(seed=i)
|
|
start = time.perf_counter()
|
|
preprocessed = proc.preprocess_csi_data(frame)
|
|
features = proc.extract_features(preprocessed)
|
|
if features:
|
|
proc.detect_human_presence(features)
|
|
proc.add_to_history(frame)
|
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
latencies.append(elapsed_ms)
|
|
|
|
p50 = statistics.median(latencies)
|
|
p95 = sorted(latencies)[int(0.95 * len(latencies))]
|
|
p99 = sorted(latencies)[int(0.99 * len(latencies))]
|
|
|
|
print(f"\n--- Sustained {n_frames}-frame benchmark ---")
|
|
print(f" p50: {p50:.2f} ms")
|
|
print(f" p95: {p95:.2f} ms")
|
|
print(f" p99: {p99:.2f} ms")
|
|
print(f" min: {min(latencies):.2f} ms")
|
|
print(f" max: {max(latencies):.2f} ms")
|
|
|
|
assert p95 < 50, f"p95 latency {p95:.1f} ms exceeds 50 ms budget"
|
|
|
|
|
|
class TestPipelineWithDoppler:
|
|
"""Full pipeline including Doppler estimation must stay within budget."""
|
|
|
|
def test_doppler_pipeline(self):
|
|
proc = CSIProcessor(config=_make_config())
|
|
n_frames = 100
|
|
latencies = []
|
|
|
|
# Fill history first
|
|
for i in range(20):
|
|
frame = _make_csi_data(seed=i + 1000)
|
|
proc.add_to_history(frame)
|
|
|
|
for i in range(n_frames):
|
|
frame = _make_csi_data(seed=i + 2000)
|
|
start = time.perf_counter()
|
|
preprocessed = proc.preprocess_csi_data(frame)
|
|
features = proc.extract_features(preprocessed)
|
|
if features:
|
|
proc.detect_human_presence(features)
|
|
proc.add_to_history(frame)
|
|
elapsed_ms = (time.perf_counter() - start) * 1000
|
|
latencies.append(elapsed_ms)
|
|
|
|
p50 = statistics.median(latencies)
|
|
p95 = sorted(latencies)[int(0.95 * len(latencies))]
|
|
p99 = sorted(latencies)[int(0.99 * len(latencies))]
|
|
|
|
print(f"\n--- Doppler pipeline benchmark ({n_frames} frames, 20 warmup) ---")
|
|
print(f" p50: {p50:.2f} ms")
|
|
print(f" p95: {p95:.2f} ms")
|
|
print(f" p99: {p99:.2f} ms")
|
|
|
|
# Doppler adds overhead but should still be within budget
|
|
assert p95 < 50, f"Doppler pipeline p95 {p95:.1f} ms exceeds 50 ms budget"
|