//! Attention-weighted BVP aggregation (ruvector-attention). //! //! [`attention_weighted_bvp`] combines per-subcarrier STFT rows using //! scaled dot-product attention, weighted by per-subcarrier sensitivity //! scores, to produce a single robust BVP (body velocity profile) vector. use ruvector_attention::attention::ScaledDotProductAttention; use ruvector_attention::traits::Attention; /// Compute attention-weighted BVP aggregation across subcarriers. /// /// `stft_rows`: one row per subcarrier, each row is `[n_velocity_bins]`. /// `sensitivity`: per-subcarrier weight. /// Returns weighted aggregation of length `n_velocity_bins`. /// /// # Arguments /// /// - `stft_rows`: one STFT row per subcarrier; each row has `n_velocity_bins` /// elements representing the Doppler velocity spectrum. /// - `sensitivity`: per-subcarrier sensitivity weight (same length as /// `stft_rows`). Higher values cause the corresponding subcarrier to /// contribute more to the initial query vector. /// - `n_velocity_bins`: number of Doppler velocity bins in each STFT row. /// /// # Returns /// /// Attention-weighted aggregation vector of length `n_velocity_bins`. /// Returns all-zeros on empty input or zero 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 sens_sum: f32 = sensitivity.iter().sum::().max(f32::EPSILON); // Build the weighted-mean query vector across all subcarriers. let query: Vec = (0..n_velocity_bins) .map(|v| { stft_rows .iter() .zip(sensitivity.iter()) .map(|(row, &s)| row[v] * s) .sum::() / sens_sum }) .collect(); let attn = ScaledDotProductAttention::new(n_velocity_bins); 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(|_| vec![0.0; n_velocity_bins]) } #[cfg(test)] mod tests { use super::*; #[test] fn attention_bvp_output_length() { let n_subcarriers = 3; let n_velocity_bins = 8; let stft_rows: Vec> = (0..n_subcarriers) .map(|sc| (0..n_velocity_bins).map(|v| (sc * n_velocity_bins + v) as f32 * 0.1).collect()) .collect(); let sensitivity = vec![0.5_f32, 0.3, 0.8]; let result = attention_weighted_bvp(&stft_rows, &sensitivity, n_velocity_bins); assert_eq!( result.len(), n_velocity_bins, "output must have length n_velocity_bins = {n_velocity_bins}" ); } #[test] fn attention_bvp_empty_input_returns_zeros() { let result = attention_weighted_bvp(&[], &[], 8); assert_eq!(result, vec![0.0_f32; 8]); } #[test] fn attention_bvp_zero_bins_returns_empty() { let stft_rows = vec![vec![1.0_f32, 2.0]]; let sensitivity = vec![1.0_f32]; let result = attention_weighted_bvp(&stft_rows, &sensitivity, 0); assert!(result.is_empty()); } }