From cca91bd8751dee7fb79f39b7dbf0688838ece667 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 16:10:18 +0000 Subject: [PATCH] feat(adr-017): Implement ruvector integrations in signal crate (partial) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agents completed three of seven ADR-017 integration points: 1. subcarrier_selection.rs — ruvector-mincut: mincut_subcarrier_partition partitions subcarriers into (sensitive, insensitive) groups using DynamicMinCut. O(n^1.5 log n) amortized vs O(n log n) static sort. Includes test: mincut_partition_separates_high_low. 2. spectrogram.rs — ruvector-attn-mincut: gate_spectrogram applies self-attention (Q=K=V) over STFT time frames to suppress noise and multipath interference frames. Configurable lambda gating strength. Includes tests: preserves shape, finite values. 3. bvp.rs — ruvector-attention stub added (in progress by agent). 4. Cargo.toml — added ruvector-mincut, ruvector-attn-mincut, ruvector-temporal-tensor, ruvector-solver, ruvector-attention as workspace deps in wifi-densepose-signal crate. Cargo.lock updated for new dependencies. Remaining ADR-017 integrations (fresnel.rs, MAT crate) still in progress via background agents. https://claude.ai/code/session_01BSBAQJ34SLkiJy4A8SoiL4 --- .claude-flow/daemon-state.json | 10 +- .swarm/memory.db | Bin 0 -> 147456 bytes .swarm/schema.sql | 305 ++++++++++++++++++ .swarm/state.json | 8 + rust-port/wifi-densepose-rs/Cargo.lock | 4 + .../crates/wifi-densepose-signal/Cargo.toml | 8 + .../crates/wifi-densepose-signal/src/bvp.rs | 85 +++++ .../wifi-densepose-signal/src/spectrogram.rs | 68 ++++ .../src/subcarrier_selection.rs | 92 ++++++ 9 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 .swarm/memory.db create mode 100644 .swarm/schema.sql create mode 100644 .swarm/state.json diff --git a/.claude-flow/daemon-state.json b/.claude-flow/daemon-state.json index ba785ae..66ff77e 100644 --- a/.claude-flow/daemon-state.json +++ b/.claude-flow/daemon-state.json @@ -39,13 +39,13 @@ "isRunning": false }, "testgaps": { - "runCount": 26, + "runCount": 27, "successCount": 0, - "failureCount": 26, + "failureCount": 27, "averageDurationMs": 0, - "lastRun": "2026-02-28T15:41:19.031Z", + "lastRun": "2026-02-28T16:08:19.369Z", "nextRun": "2026-02-28T16:22:19.355Z", - "isRunning": true + "isRunning": false }, "predict": { "runCount": 0, @@ -131,5 +131,5 @@ } ] }, - "savedAt": "2026-02-28T16:05:19.091Z" + "savedAt": "2026-02-28T16:08:19.369Z" } \ No newline at end of file diff --git a/.swarm/memory.db b/.swarm/memory.db new file mode 100644 index 0000000000000000000000000000000000000000..00916a4857200ed45f3843384fb2017ef9dddd0c GIT binary patch literal 147456 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 of length n_velocity_bins +pub fn attention_weighted_bvp( + stft_rows: &[Vec], + sensitivity: &[f32], + n_velocity_bins: usize, +) -> Vec { + if stft_rows.is_empty() || n_velocity_bins == 0 { + return vec![0.0; n_velocity_bins]; + } + + let attn = ScaledDotProductAttention::new(n_velocity_bins); + let sens_sum: f32 = sensitivity.iter().sum::().max(1e-9); + + // Query: sensitivity-weighted mean of all subcarrier profiles + let query: Vec = (0..n_velocity_bins) + .map(|v| { + stft_rows + .iter() + .zip(sensitivity.iter()) + .map(|(row, &s)| { + row.get(v).copied().unwrap_or(0.0) * s + }) + .sum::() + / sens_sum + }) + .collect(); + + let keys: Vec<&[f32]> = stft_rows.iter().map(|r| r.as_slice()).collect(); + let values: Vec<&[f32]> = stft_rows.iter().map(|r| r.as_slice()).collect(); + + attn.compute(&query, &keys, &values) + .unwrap_or_else(|_| { + // Fallback: plain weighted sum + (0..n_velocity_bins) + .map(|v| { + stft_rows + .iter() + .zip(sensitivity.iter()) + .map(|(row, &s)| row.get(v).copied().unwrap_or(0.0) * s) + .sum::() + / sens_sum + }) + .collect() + }) +} + +#[cfg(test)] +mod attn_bvp_tests { + use super::*; + + #[test] + fn attention_bvp_output_shape() { + let n_sc = 4_usize; + let n_vbins = 8_usize; + let stft_rows: Vec> = (0..n_sc) + .map(|i| vec![i as f32 * 0.1; n_vbins]) + .collect(); + let sensitivity = vec![0.9_f32, 0.1, 0.8, 0.2]; + let bvp = attention_weighted_bvp(&stft_rows, &sensitivity, n_vbins); + assert_eq!(bvp.len(), n_vbins); + assert!(bvp.iter().all(|x| x.is_finite())); + } + + #[test] + fn attention_bvp_empty_input() { + let bvp = attention_weighted_bvp(&[], &[], 8); + assert_eq!(bvp.len(), 8); + assert!(bvp.iter().all(|&x| x == 0.0)); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/spectrogram.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/spectrogram.rs index 5d8419b..d97fafe 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/spectrogram.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/spectrogram.rs @@ -9,6 +9,7 @@ use ndarray::Array2; use num_complex::Complex64; +use ruvector_attn_mincut::attn_mincut; use rustfft::FftPlanner; use std::f64::consts::PI; @@ -164,6 +165,47 @@ fn make_window(kind: WindowFunction, size: usize) -> Vec { } } +/// Apply attention-gating to a computed CSI spectrogram using ruvector-attn-mincut. +/// +/// Treats each time frame as an attention token (d = n_freq_bins features, +/// seq_len = n_time_frames tokens). Self-attention (Q=K=V) gates coherent +/// body-motion frames and suppresses uncorrelated noise/interference frames. +/// +/// # Arguments +/// * `spectrogram` - Row-major [n_freq_bins × n_time_frames] f32 slice +/// * `n_freq` - Number of frequency bins (feature dimension d) +/// * `n_time` - Number of time frames (sequence length) +/// * `lambda` - Gating strength: 0.1 = mild, 0.3 = moderate, 0.5 = aggressive +/// +/// # Returns +/// Gated spectrogram as Vec, same shape as input +pub fn gate_spectrogram( + spectrogram: &[f32], + n_freq: usize, + n_time: usize, + lambda: f32, +) -> Vec { + debug_assert_eq!(spectrogram.len(), n_freq * n_time, + "spectrogram length must equal n_freq * n_time"); + + if n_freq == 0 || n_time == 0 { + return spectrogram.to_vec(); + } + + // Q = K = V = spectrogram (self-attention over time frames) + let result = attn_mincut( + spectrogram, + spectrogram, + spectrogram, + n_freq, // d = feature dimension + n_time, // seq_len = time tokens + lambda, + /*tau=*/ 2, + /*eps=*/ 1e-7_f32, + ); + result.output +} + /// Errors from spectrogram computation. #[derive(Debug, thiserror::Error)] pub enum SpectrogramError { @@ -297,3 +339,29 @@ mod tests { } } } + +#[cfg(test)] +mod gate_tests { + use super::*; + + #[test] + fn gate_spectrogram_preserves_shape() { + let n_freq = 16_usize; + let n_time = 10_usize; + let spectrogram: Vec = (0..n_freq * n_time).map(|i| i as f32 * 0.01).collect(); + let gated = gate_spectrogram(&spectrogram, n_freq, n_time, 0.3); + assert_eq!(gated.len(), n_freq * n_time); + } + + #[test] + fn gate_spectrogram_zero_lambda_is_identity_ish() { + let n_freq = 8_usize; + let n_time = 4_usize; + let spectrogram: Vec = vec![1.0; n_freq * n_time]; + // Uniform input — gated output should also be approximately uniform + let gated = gate_spectrogram(&spectrogram, n_freq, n_time, 0.01); + assert_eq!(gated.len(), n_freq * n_time); + // All values should be finite + assert!(gated.iter().all(|x| x.is_finite())); + } +} diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/subcarrier_selection.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/subcarrier_selection.rs index 33d1b1e..cff9814 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/subcarrier_selection.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/subcarrier_selection.rs @@ -9,6 +9,7 @@ //! - WiGest: Using WiFi Gestures for Device-Free Sensing (SenSys 2015) use ndarray::Array2; +use ruvector_mincut::MinCutBuilder; /// Configuration for subcarrier selection. #[derive(Debug, Clone)] @@ -168,6 +169,72 @@ fn column_variance(data: &Array2, col: usize) -> f64 { col_data.iter().map(|x| (x - mean).powi(2)).sum::() / (n - 1.0) } +/// Partition subcarriers into (sensitive, insensitive) groups via DynamicMinCut. +/// +/// Builds a similarity graph: subcarriers are vertices, edges encode inverse +/// variance-ratio distance. The min-cut separates high-sensitivity from +/// low-sensitivity subcarriers in O(n^1.5 log n) amortized time. +/// +/// # Arguments +/// * `sensitivity` - Per-subcarrier sensitivity score (variance_motion / variance_static) +/// +/// # Returns +/// (sensitive_indices, insensitive_indices) — indices into the input slice +pub fn mincut_subcarrier_partition(sensitivity: &[f32]) -> (Vec, Vec) { + let n = sensitivity.len(); + if n < 4 { + // Too small for meaningful cut — put all in sensitive + return ((0..n).collect(), Vec::new()); + } + + // Build similarity graph: edge weight = 1 / |sensitivity_i - sensitivity_j| + // Only include edges where weight > min_weight (prune very weak similarities) + let min_weight = 0.5_f64; + let mut edges: Vec<(u64, u64, f64)> = Vec::new(); + for i in 0..n { + for j in (i + 1)..n { + let diff = (sensitivity[i] - sensitivity[j]).abs() as f64; + let weight = if diff > 1e-9 { 1.0 / diff } else { 1e6_f64 }; + if weight > min_weight { + edges.push((i as u64, j as u64, weight)); + } + } + } + + if edges.is_empty() { + // All subcarriers equally sensitive — split by median + let median_idx = n / 2; + return ((0..median_idx).collect(), (median_idx..n).collect()); + } + + let mc = MinCutBuilder::new().exact().with_edges(edges).build(); + let (side_a, side_b) = mc.partition(); + + // The side with higher mean sensitivity is the "sensitive" group + let mean_a: f32 = if side_a.is_empty() { + 0.0 + } else { + side_a.iter().map(|&i| sensitivity[i as usize]).sum::() / side_a.len() as f32 + }; + let mean_b: f32 = if side_b.is_empty() { + 0.0 + } else { + side_b.iter().map(|&i| sensitivity[i as usize]).sum::() / side_b.len() as f32 + }; + + if mean_a >= mean_b { + ( + side_a.into_iter().map(|x| x as usize).collect(), + side_b.into_iter().map(|x| x as usize).collect(), + ) + } else { + ( + side_b.into_iter().map(|x| x as usize).collect(), + side_a.into_iter().map(|x| x as usize).collect(), + ) + } +} + /// Errors from subcarrier selection. #[derive(Debug, thiserror::Error)] pub enum SelectionError { @@ -290,3 +357,28 @@ mod tests { )); } } + +#[cfg(test)] +mod mincut_tests { + use super::*; + + #[test] + fn mincut_partition_separates_high_low() { + // High sensitivity: indices 0,1,2; low: 3,4,5 + let sensitivity = vec![0.9_f32, 0.85, 0.92, 0.1, 0.12, 0.08]; + let (sensitive, insensitive) = mincut_subcarrier_partition(&sensitivity); + // High-sensitivity indices should cluster together + assert!(!sensitive.is_empty()); + assert!(!insensitive.is_empty()); + let sens_mean: f32 = sensitive.iter().map(|&i| sensitivity[i]).sum::() / sensitive.len() as f32; + let insens_mean: f32 = insensitive.iter().map(|&i| sensitivity[i]).sum::() / insensitive.len() as f32; + assert!(sens_mean > insens_mean, "sensitive mean {sens_mean} should exceed insensitive mean {insens_mean}"); + } + + #[test] + fn mincut_partition_small_input() { + let sensitivity = vec![0.5_f32, 0.8]; + let (sensitive, insensitive) = mincut_subcarrier_partition(&sensitivity); + assert_eq!(sensitive.len() + insensitive.len(), 2); + } +}