//! GPU-accelerated LCMV beamformer for real-time source localization. //! //! Provides sub-millisecond beamformer computation using pre-computed spatial filters. use crate::error::{RealtimeError, RealtimeResult}; use nalgebra::{DMatrix, DVector}; use std::sync::Arc; /// GPU beamformer configuration #[derive(Debug, Clone)] pub struct GpuBeamformerConfig { /// Number of sensor channels pub n_channels: usize, /// Number of source locations pub n_sources: usize, /// Whether sources have free orientation (3 components per source) pub free_orientation: bool, /// Regularization parameter (fraction of trace) pub regularization: f64, /// Power normalization enabled pub normalize_power: bool, } impl Default for GpuBeamformerConfig { fn default() -> Self { Self { n_channels: 64, n_sources: 1000, free_orientation: false, regularization: 0.05, normalize_power: true, } } } /// Pre-computed beamformer weights for GPU processing #[derive(Debug, Clone)] pub struct BeamformerWeights { /// Spatial filters [n_source_components x n_channels] pub weights: DMatrix, /// Power normalization factors [n_source_components] pub power_norm: DVector, /// Source indices pub source_indices: Vec, /// Number of source components (n_sources * 3 if free orientation) pub n_components: usize, } impl BeamformerWeights { /// Create from pre-computed weights pub fn new( weights: DMatrix, power_norm: Option>, source_indices: Vec, ) -> Self { let n_components = weights.nrows(); let power_norm = power_norm.unwrap_or_else(|| DVector::from_element(n_components, 1.0)); Self { weights, power_norm, source_indices, n_components, } } /// Number of channels pub fn n_channels(&self) -> usize { self.weights.ncols() } /// Number of source components pub fn n_components(&self) -> usize { self.n_components } } /// GPU-accelerated LCMV beamformer #[derive(Debug)] pub struct GpuBeamformer { /// Configuration config: GpuBeamformerConfig, /// Pre-computed beamformer weights weights: Arc, /// Cached covariance inverse (for weight updates) cov_inv: Option>, } impl GpuBeamformer { /// Create a new GPU beamformer from pre-computed weights /// /// # Arguments /// * `config` - Beamformer configuration /// * `weights` - Pre-computed spatial filter weights pub fn new(config: GpuBeamformerConfig, weights: BeamformerWeights) -> RealtimeResult { // Validate dimensions if weights.n_channels() != config.n_channels { return Err(RealtimeError::DimensionMismatch(format!( "Weights have {} channels, config expects {}", weights.n_channels(), config.n_channels ))); } Ok(Self { config, weights: Arc::new(weights), cov_inv: None, }) } /// Create a beamformer from gain matrix and covariance /// /// # Arguments /// * `config` - Beamformer configuration /// * `gain` - Forward model gain matrix [n_channels x n_sources] /// * `data_cov` - Data covariance matrix [n_channels x n_channels] pub fn from_forward_model( config: GpuBeamformerConfig, gain: &DMatrix, data_cov: &DMatrix, ) -> RealtimeResult { let n_channels = gain.nrows(); let n_source_cols = gain.ncols(); if data_cov.nrows() != n_channels || data_cov.ncols() != n_channels { return Err(RealtimeError::DimensionMismatch( "Covariance matrix dimension mismatch".to_string(), )); } // Regularize and invert covariance let mut c = data_cov.clone(); let trace: f64 = (0..n_channels).map(|i| c[(i, i)]).sum(); let reg_val = config.regularization * trace / n_channels as f64; for i in 0..n_channels { c[(i, i)] += reg_val; } let c_inv = Self::invert_matrix(&c)?; // Compute beamformer weights: w_i = C^-1 * g_i / (g_i^T * C^-1 * g_i) let mut weights = DMatrix::zeros(n_source_cols, n_channels); let mut power_norm = DVector::zeros(n_source_cols); for src in 0..n_source_cols { let g_i = gain.column(src); let c_inv_g = &c_inv * g_i; let norm = g_i.dot(&c_inv_g); if norm > 1e-30 { for ch in 0..n_channels { weights[(src, ch)] = c_inv_g[ch] / norm; } power_norm[src] = 1.0 / norm.sqrt(); } } let source_indices: Vec = if config.free_orientation { (0..n_source_cols / 3).collect() } else { (0..n_source_cols).collect() }; let bf_weights = BeamformerWeights::new(weights, Some(power_norm), source_indices); let mut bf = Self::new(config, bf_weights)?; bf.cov_inv = Some(c_inv); Ok(bf) } /// Apply beamformer to sensor data /// /// # Arguments /// * `data` - Sensor data [n_channels x n_times] /// /// # Returns /// Source estimates [n_sources x n_times] pub fn apply(&self, data: &[Vec]) -> RealtimeResult>> { if data.len() != self.config.n_channels { return Err(RealtimeError::DimensionMismatch(format!( "Expected {} channels, got {}", self.config.n_channels, data.len() ))); } if data.is_empty() || data[0].is_empty() { return Ok(vec![vec![]; self.weights.n_components]); } let n_times = data[0].len(); let n_comp = self.weights.n_components; // Convert to matrix [n_channels x n_times] let data_mat = DMatrix::from_fn(self.config.n_channels, n_times, |i, j| data[i][j]); // Apply spatial filters: S = W * M let source_mat = &self.weights.weights * &data_mat; // Apply power normalization if enabled let output: Vec> = if self.config.normalize_power { (0..n_comp) .map(|i| { let norm = self.weights.power_norm[i]; (0..n_times).map(|j| source_mat[(i, j)] * norm).collect() }) .collect() } else { (0..n_comp) .map(|i| (0..n_times).map(|j| source_mat[(i, j)]).collect()) .collect() }; // If free orientation, combine xyz components into power if self.config.free_orientation { let n_sources = n_comp / 3; let combined: Vec> = (0..n_sources) .map(|s| { let x = &output[s * 3]; let y = &output[s * 3 + 1]; let z = &output[s * 3 + 2]; (0..n_times) .map(|t| (x[t].powi(2) + y[t].powi(2) + z[t].powi(2)).sqrt()) .collect() }) .collect(); Ok(combined) } else { Ok(output) } } /// Apply beamformer using nalgebra matrix (faster for batch processing) pub fn apply_matrix(&self, data: &DMatrix) -> RealtimeResult> { if data.nrows() != self.config.n_channels { return Err(RealtimeError::DimensionMismatch(format!( "Expected {} channels, got {}", self.config.n_channels, data.nrows() ))); } // Apply spatial filters: S = W * M let mut source_mat = &self.weights.weights * data; // Apply power normalization if self.config.normalize_power { for i in 0..source_mat.nrows() { let norm = self.weights.power_norm[i]; for j in 0..source_mat.ncols() { source_mat[(i, j)] *= norm; } } } Ok(source_mat) } /// Compute Neural Activity Index (NAI) for each source /// /// NAI = (w^T * C_data * w) / (w^T * C_noise * w) pub fn compute_nai( &self, data_cov: &DMatrix, noise_cov: &DMatrix, ) -> RealtimeResult> { let n_comp = self.weights.n_components; let n_channels = self.config.n_channels; if data_cov.nrows() != n_channels || noise_cov.nrows() != n_channels { return Err(RealtimeError::DimensionMismatch( "Covariance dimension mismatch".to_string(), )); } let mut nai = Vec::with_capacity(n_comp); for src in 0..n_comp { let w = self.weights.weights.row(src); // w^T * C * w let compute_power = |cov: &DMatrix| -> f64 { (0..n_channels) .map(|i| { (0..n_channels) .map(|j| w[i] * cov[(i, j)] * w[j]) .sum::() }) .sum() }; let signal_power = compute_power(data_cov); let noise_power = compute_power(noise_cov); if noise_power > 1e-30 { nai.push(signal_power / noise_power); } else { nai.push(0.0); } } Ok(nai) } /// Get the spatial filter weights pub fn weights(&self) -> &BeamformerWeights { &self.weights } /// Get configuration pub fn config(&self) -> &GpuBeamformerConfig { &self.config } /// Update beamformer weights with new covariance (adaptive beamforming) pub fn update_covariance(&mut self, new_cov: &DMatrix) -> RealtimeResult<()> { // This would require re-computing the weights // For real-time use, this should be done asynchronously Err(RealtimeError::NotInitialized( "Adaptive beamforming not yet implemented".to_string(), )) } /// Invert matrix using SVD fn invert_matrix(m: &DMatrix) -> RealtimeResult> { let n = m.nrows(); let svd = m.clone().svd(true, true); let u = svd .u .ok_or_else(|| RealtimeError::Beamformer("SVD failed to compute U".to_string()))?; let vt = svd .v_t .ok_or_else(|| RealtimeError::Beamformer("SVD failed to compute V^T".to_string()))?; let s = svd.singular_values; let tol = 1e-10 * s[0]; let s_inv = DMatrix::from_diagonal(&DVector::from_fn(n, |i, _| { if s[i] > tol { 1.0 / s[i] } else { 0.0 } })); Ok(vt.transpose() * &s_inv * u.transpose()) } } /// Create a beamformer for a subset of sources (ROI-based) pub fn create_roi_beamformer( full_beamformer: &GpuBeamformer, source_indices: &[usize], ) -> RealtimeResult { let full_weights = &full_beamformer.weights; let mut config = full_beamformer.config.clone(); let n_channels = full_weights.n_channels(); let n_selected = source_indices.len(); // Extract weights for selected sources let mut selected_weights = DMatrix::zeros(n_selected, n_channels); let mut selected_norm = DVector::zeros(n_selected); for (i, &src_idx) in source_indices.iter().enumerate() { if src_idx >= full_weights.n_components { return Err(RealtimeError::DimensionMismatch(format!( "Source index {} out of range (max {})", src_idx, full_weights.n_components - 1 ))); } for ch in 0..n_channels { selected_weights[(i, ch)] = full_weights.weights[(src_idx, ch)]; } selected_norm[i] = full_weights.power_norm[src_idx]; } config.n_sources = n_selected; config.free_orientation = false; let roi_weights = BeamformerWeights::new( selected_weights, Some(selected_norm), source_indices.to_vec(), ); GpuBeamformer::new(config, roi_weights) } /// Compute source power (envelope) from beamformer output pub fn compute_source_power(sources: &[Vec], window_samples: usize) -> Vec> { sources .iter() .map(|source| { source .windows(window_samples) .map(|window| { let sum_sq: f64 = window.iter().map(|x| x.powi(2)).sum(); (sum_sq / window_samples as f64).sqrt() }) .collect() }) .collect() } #[cfg(test)] mod tests { use super::*; fn create_test_gain() -> DMatrix { // Simple 4-channel, 3-source gain matrix DMatrix::from_row_slice( 4, 3, &[1.0, 0.5, 0.2, 0.5, 1.0, 0.5, 0.2, 0.5, 1.0, 0.1, 0.3, 0.6], ) } fn create_test_covariance() -> DMatrix { // Simple identity-like covariance DMatrix::from_diagonal(&DVector::from_vec(vec![1.0, 1.0, 1.0, 1.0])) } #[test] fn test_beamformer_creation() { let config = GpuBeamformerConfig { n_channels: 4, n_sources: 3, free_orientation: false, regularization: 0.05, normalize_power: true, }; let gain = create_test_gain(); let cov = create_test_covariance(); let bf = GpuBeamformer::from_forward_model(config, &gain, &cov).unwrap(); assert_eq!(bf.weights.n_channels(), 4); assert_eq!(bf.weights.n_components(), 3); } #[test] fn test_beamformer_apply() { let config = GpuBeamformerConfig { n_channels: 4, n_sources: 3, free_orientation: false, regularization: 0.05, normalize_power: false, }; let gain = create_test_gain(); let cov = create_test_covariance(); let bf = GpuBeamformer::from_forward_model(config, &gain, &cov).unwrap(); // Test data: 4 channels, 10 time points let data: Vec> = (0..4) .map(|ch| (0..10).map(|t| (ch * 10 + t) as f64).collect()) .collect(); let sources = bf.apply(&data).unwrap(); assert_eq!(sources.len(), 3); assert_eq!(sources[0].len(), 10); } #[test] fn test_apply_matrix() { let config = GpuBeamformerConfig { n_channels: 4, n_sources: 3, free_orientation: false, regularization: 0.05, normalize_power: false, }; let gain = create_test_gain(); let cov = create_test_covariance(); let bf = GpuBeamformer::from_forward_model(config, &gain, &cov).unwrap(); let data = DMatrix::from_fn(4, 10, |i, j| (i * 10 + j) as f64); let sources = bf.apply_matrix(&data).unwrap(); assert_eq!(sources.nrows(), 3); assert_eq!(sources.ncols(), 10); } #[test] fn test_compute_nai() { let config = GpuBeamformerConfig { n_channels: 4, n_sources: 3, free_orientation: false, regularization: 0.05, normalize_power: false, }; let gain = create_test_gain(); let cov = create_test_covariance(); let bf = GpuBeamformer::from_forward_model(config, &gain, &cov).unwrap(); // Data cov with higher signal let data_cov = DMatrix::from_diagonal(&DVector::from_vec(vec![2.0, 2.0, 2.0, 2.0])); let noise_cov = DMatrix::from_diagonal(&DVector::from_vec(vec![1.0, 1.0, 1.0, 1.0])); let nai = bf.compute_nai(&data_cov, &noise_cov).unwrap(); assert_eq!(nai.len(), 3); // NAI should be positive with higher signal than noise for n in &nai { assert!(*n >= 0.0); } } #[test] fn test_roi_beamformer() { let config = GpuBeamformerConfig { n_channels: 4, n_sources: 3, free_orientation: false, regularization: 0.05, normalize_power: false, }; let gain = create_test_gain(); let cov = create_test_covariance(); let bf = GpuBeamformer::from_forward_model(config, &gain, &cov).unwrap(); // Create ROI beamformer for sources 0 and 2 let roi_bf = create_roi_beamformer(&bf, &[0, 2]).unwrap(); assert_eq!(roi_bf.weights.n_components(), 2); } #[test] fn test_source_power() { let sources = vec![vec![1.0, 2.0, 3.0, 4.0, 5.0], vec![0.5, 1.0, 1.5, 2.0, 2.5]]; let power = compute_source_power(&sources, 3); assert_eq!(power.len(), 2); assert_eq!(power[0].len(), 3); // 5 - 3 + 1 = 3 windows } }