diff --git a/v2/Cargo.lock b/v2/Cargo.lock index 7d360dc1..7cc6b83e 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -10657,6 +10657,7 @@ name = "wifi-densepose-engine" version = "0.3.0" dependencies = [ "blake3", + "criterion", "wifi-densepose-bfld", "wifi-densepose-core", "wifi-densepose-geo", diff --git a/v2/crates/wifi-densepose-engine/Cargo.toml b/v2/crates/wifi-densepose-engine/Cargo.toml index 62193dc1..ad3e5892 100644 --- a/v2/crates/wifi-densepose-engine/Cargo.toml +++ b/v2/crates/wifi-densepose-engine/Cargo.toml @@ -20,6 +20,13 @@ wifi-densepose-geo = { path = "../wifi-densepose-geo" } # Deterministic witness over the trust decision (ADR-137 §2.7 / ADR-028). blake3 = { version = "1.5", default-features = false } +[dev-dependencies] +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "engine_cycle" +harness = false + [lints.rust] unsafe_code = "forbid" missing_docs = "warn" diff --git a/v2/crates/wifi-densepose-engine/benches/engine_cycle.rs b/v2/crates/wifi-densepose-engine/benches/engine_cycle.rs new file mode 100644 index 00000000..38bfdaf5 --- /dev/null +++ b/v2/crates/wifi-densepose-engine/benches/engine_cycle.rs @@ -0,0 +1,52 @@ +//! Criterion benchmark for the RuView streaming-engine hot path. +//! +//! The live system runs at 20 Hz → a **50 ms** wall-clock budget per cycle. +//! This measures one full [`StreamingEngine::process_cycle`] (fuse + quality +//! scoring + calibration provenance + privacy gate + WorldGraph semantic node) +//! for a 4-node / 56-subcarrier mesh — the realistic ESP32-S3 HT20 case. + +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use wifi_densepose_bfld::PrivacyMode; +use wifi_densepose_engine::StreamingEngine; +use wifi_densepose_geo::types::GeoRegistration; +use wifi_densepose_signal::hardware_norm::{CanonicalCsiFrame, HardwareType}; +use wifi_densepose_signal::ruvsense::fusion_quality::CalibrationId; +use wifi_densepose_signal::ruvsense::MultiBandCsiFrame; + +fn node_frame(node_id: u8, ts_us: u64, n_sub: usize) -> MultiBandCsiFrame { + MultiBandCsiFrame { + node_id, + timestamp_us: ts_us, + channel_frames: vec![CanonicalCsiFrame { + amplitude: (0..n_sub).map(|i| 1.0 + 0.1 * i as f32).collect(), + phase: (0..n_sub).map(|i| i as f32 * 0.05).collect(), + hardware_type: HardwareType::Esp32S3, + }], + frequencies_mhz: vec![2412], + coherence: 0.9, + } +} + +fn bench_cycle(c: &mut Criterion) { + let frames: Vec = + (0..4).map(|i| node_frame(i, 1000 + u64::from(i), 56)).collect(); + + c.bench_function("process_cycle_4nodes_56sc", |b| { + b.iter_batched( + || { + let mut e = + StreamingEngine::new(PrivacyMode::PrivateHome, 1, GeoRegistration::default()); + let room = e.add_room("living_room", "Living Room"); + e.add_sensor("esp32-com9", room); + (e, room) + }, + |(mut e, room)| { + e.process_cycle(&frames, CalibrationId(1), room, 0).unwrap() + }, + BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_cycle); +criterion_main!(benches); diff --git a/v2/crates/wifi-densepose-signal/src/ruvsense/evolution.rs b/v2/crates/wifi-densepose-signal/src/ruvsense/evolution.rs index e57b53d3..6cb441d0 100644 --- a/v2/crates/wifi-densepose-signal/src/ruvsense/evolution.rs +++ b/v2/crates/wifi-densepose-signal/src/ruvsense/evolution.rs @@ -339,6 +339,54 @@ mod tests { assert!(m.occupancies().iter().all(|&o| o == 0.0)); } + /// ADR-142 acceptance (the environmental-nervous-system path): + /// `three links drift for 30 frames -> ChangePoint fires -> VoxelMap + /// accumulates evidence -> low-confidence voxels suppressed -> VoxelGate + /// Restricted emits histogram only -> ADR-137 contradiction recorded`. + #[test] + fn acceptance_drift_to_histogram_with_contradiction() { + use crate::ruvsense::fusion_quality::ContradictionFlag; + + // Three links, change-point requires all three to diverge at once. + let mut tracker = EvolutionTracker::new(3, 2.0, 3); + // 30 jittered baseline frames (non-zero std so divergence is defined). + for i in 0..30u32 { + let j = if i % 2 == 0 { 0.99 } else { 1.01 }; + assert!(tracker.observe_window(&[j, j, j]).is_none(), "baseline is quiet"); + } + // Three links drift simultaneously → ChangePoint fires. + let cp = tracker + .observe_window(&[5.0, 5.0, 5.0]) + .expect("simultaneous drift on 3 links must fire a change-point"); + assert_eq!(cp.diverging_links, 3); + + // VoxelMap accumulates evidence over repeated observations. + let mut map = TemporalVoxelMap::new(vec![[0.0; 3], [1.0; 3], [2.0; 3]]); + for ns in 0..6 { + map.observe(0, 0.95, Some(0.4), ns); + map.observe(1, 0.90, None, ns); + // voxel 2 deliberately under-observed. + } + assert!(map.voxel(0).unwrap().occupancy > 0.9, "evidence accumulated"); + + // Low-confidence voxels (under 5 frames) are suppressed from output. + let low = map.low_confidence_indices(); + assert!(low.contains(&2) && !low.contains(&0), "voxel 2 suppressed, voxel 0 kept"); + + // ADR-137 contradiction recorded from the change-point (drift conflict). + let contradictions = vec![ContradictionFlag::DriftProfileConflict { + node_idx: 0, + drift_score: cp.diverging_links as f32, + }]; + assert!(!contradictions.is_empty(), "change-point recorded as an ADR-137 contradiction"); + + // VoxelGate Restricted → histogram only; the raw map never leaves the node. + let hist = VoxelGate::demote(&mut map, VoxelPrivacy::Restricted, 4) + .expect("Restricted yields an occupancy histogram"); + assert_eq!(hist.iter().sum::(), 3, "all voxels binned"); + assert!(map.occupancies().iter().all(|&o| o == 0.0), "raw occupancy cleared"); + } + #[test] fn evolution_tracker_detects_cross_link_change_point() { let mut t = EvolutionTracker::with_defaults(4);