//! High-level Brain-Computer Interface API. //! //! Provides a simple interface for BCI applications with source-level //! feedback and control signal generation. use crate::error::RealtimeResult; use crate::gpu_beamformer::{GpuBeamformer, GpuBeamformerConfig}; use crate::latency::LatencyStats; use crate::pipeline::{PipelineConfig, PipelineState, RealtimePipeline, SourceOutput}; use nalgebra::DMatrix; use parking_lot::RwLock; use std::sync::Arc; use std::time::Duration; /// Callback type for source updates pub type SourceCallback = Box; /// Control signal derived from source activity #[derive(Debug, Clone)] pub struct ControlSignal { /// Source indices being monitored pub source_indices: Vec, /// Current power values for each source pub power: Vec, /// Normalized control values (0.0 - 1.0) pub control_values: Vec, /// Combined control signal (weighted average) pub combined_signal: f64, /// Threshold crossing (true if any source exceeds threshold) pub threshold_crossed: bool, /// Timestamp pub timestamp: f64, /// Latency in milliseconds pub latency_ms: f64, } impl Default for ControlSignal { fn default() -> Self { Self { source_indices: vec![], power: vec![], control_values: vec![], combined_signal: 0.0, threshold_crossed: false, timestamp: 0.0, latency_ms: 0.0, } } } /// BCI configuration #[derive(Debug, Clone)] pub struct BciConfig { /// Number of channels pub n_channels: usize, /// Sample rate in Hz pub sample_rate: f64, /// Buffer duration in milliseconds pub buffer_duration_ms: f64, /// Processing window size in milliseconds pub window_size_ms: f64, /// Window stride in milliseconds pub stride_ms: f64, /// Filter low cutoff (Hz) pub filter_low: f64, /// Filter high cutoff (Hz) pub filter_high: f64, /// Source indices to monitor pub monitor_sources: Vec, /// Threshold for activation detection (0.0 - 1.0) pub threshold: f64, /// Source weights for combined signal pub source_weights: Vec, /// Baseline period in seconds (for normalization) pub baseline_duration: f64, /// Smoothing factor (0.0 = no smoothing, 1.0 = full smoothing) pub smoothing: f64, } impl Default for BciConfig { fn default() -> Self { Self { n_channels: 64, sample_rate: 1000.0, buffer_duration_ms: 500.0, window_size_ms: 100.0, stride_ms: 50.0, filter_low: 8.0, filter_high: 30.0, monitor_sources: vec![0], threshold: 0.5, source_weights: vec![1.0], baseline_duration: 2.0, smoothing: 0.3, } } } /// High-level BCI pipeline pub struct BciPipeline { /// Configuration config: BciConfig, /// Underlying real-time pipeline pipeline: RealtimePipeline, /// Baseline power estimates [n_sources] baseline: Arc>>, /// Current smoothed values [n_sources] smoothed_values: Arc>>, /// Callback for control signal updates callback: Arc>>, /// Whether baseline calibration is complete baseline_calibrated: Arc>, /// Baseline samples collected baseline_samples: Arc>>>, } impl BciPipeline { /// Create a new BCI pipeline pub fn new(config: BciConfig) -> RealtimeResult { // Build the underlying pipeline let pipeline_config = PipelineConfig { n_channels: config.n_channels, sample_rate: config.sample_rate, buffer_duration_ms: config.buffer_duration_ms, window_size_ms: config.window_size_ms, window_stride_ms: config.stride_ms, filter_low: config.filter_low, filter_high: config.filter_high, enable_filtering: true, enable_beamforming: false, // Will set beamformer separately target_latency_ms: 10.0, output_queue_size: 100, }; let pipeline = RealtimePipeline::new(pipeline_config)?; let n_sources = config.monitor_sources.len().max(1); Ok(Self { config, pipeline, baseline: Arc::new(RwLock::new(vec![1.0; n_sources])), smoothed_values: Arc::new(RwLock::new(vec![0.0; n_sources])), callback: Arc::new(RwLock::new(None)), baseline_calibrated: Arc::new(RwLock::new(false)), baseline_samples: Arc::new(RwLock::new(Vec::new())), }) } /// Set the beamformer pub fn set_beamformer(&mut self, beamformer: GpuBeamformer) { self.pipeline.set_beamformer(beamformer); } /// Create beamformer from gain and covariance matrices pub fn create_beamformer( &mut self, gain: &DMatrix, data_cov: &DMatrix, ) -> RealtimeResult<()> { let n_sources = gain.ncols(); let bf_config = GpuBeamformerConfig { n_channels: self.config.n_channels, n_sources, free_orientation: false, regularization: 0.05, normalize_power: true, }; let beamformer = GpuBeamformer::from_forward_model(bf_config, gain, data_cov)?; self.pipeline.set_beamformer(beamformer); Ok(()) } /// Set callback for control signal updates pub fn on_source_update(&self, callback: F) where F: Fn(&ControlSignal) + Send + Sync + 'static, { *self.callback.write() = Some(Box::new(callback)); } /// Clear the callback pub fn clear_callback(&self) { *self.callback.write() = None; } /// Push a sample into the pipeline pub fn push_sample(&self, data: Vec, timestamp: f64) { self.pipeline.push_sample(data, timestamp); } /// Push a batch of samples pub fn push_batch(&self, data: &[Vec], timestamps: &[f64]) { self.pipeline.push_batch(data, timestamps); } /// Start the BCI pipeline pub fn start(&mut self) -> RealtimeResult<()> { self.pipeline.start() } /// Stop the pipeline pub fn stop(&mut self) -> RealtimeResult<()> { self.pipeline.stop() } /// Start baseline calibration pub fn start_baseline_calibration(&self) { *self.baseline_calibrated.write() = false; self.baseline_samples.write().clear(); } /// Check if baseline is calibrated pub fn is_baseline_calibrated(&self) -> bool { *self.baseline_calibrated.read() } /// Process pending outputs and generate control signals pub fn process(&self) -> Option { let output = self.pipeline.try_recv()?; Some(self.compute_control_signal(&output)) } /// Process with blocking wait pub fn process_blocking(&self) -> Option { let output = self.pipeline.recv()?; Some(self.compute_control_signal(&output)) } /// Process with timeout pub fn process_timeout(&self, timeout: Duration) -> Option { let output = self.pipeline.recv_timeout(timeout)?; Some(self.compute_control_signal(&output)) } /// Compute control signal from source output fn compute_control_signal(&self, output: &SourceOutput) -> ControlSignal { let n_sources = output.sources.len(); let monitor_sources = &self.config.monitor_sources; // Compute power for each monitored source let mut power = Vec::with_capacity(monitor_sources.len()); for &src_idx in monitor_sources { if src_idx < n_sources && !output.sources[src_idx].is_empty() { let src_power: f64 = output.sources[src_idx] .iter() .map(|x| x.powi(2)) .sum::() / output.sources[src_idx].len() as f64; power.push(src_power.sqrt()); } else { power.push(0.0); } } // Update baseline if calibrating if !*self.baseline_calibrated.read() { let mut baseline_samples = self.baseline_samples.write(); baseline_samples.push(power.clone()); let required_samples = (self.config.baseline_duration * 1000.0 / self.config.stride_ms) as usize; if baseline_samples.len() >= required_samples { // Compute baseline as mean power let n_samples = baseline_samples.len() as f64; let baseline: Vec = (0..power.len()) .map(|i| baseline_samples.iter().map(|s| s[i]).sum::() / n_samples) .collect(); *self.baseline.write() = baseline; *self.baseline_calibrated.write() = true; } } // Normalize by baseline let baseline = self.baseline.read(); let control_values: Vec = power .iter() .enumerate() .map(|(i, &p)| { if baseline[i] > 1e-10 { (p / baseline[i]).min(2.0) // Clamp to 2x baseline } else { 0.0 } }) .collect(); // Apply smoothing let mut smoothed = self.smoothed_values.write(); for i in 0..control_values.len().min(smoothed.len()) { smoothed[i] = self.config.smoothing * smoothed[i] + (1.0 - self.config.smoothing) * control_values[i]; } let smoothed_values = smoothed.clone(); drop(smoothed); // Compute combined signal let weights = &self.config.source_weights; let combined_signal = if !weights.is_empty() { let weight_sum: f64 = weights.iter().sum(); smoothed_values .iter() .enumerate() .map(|(i, &v)| { let w = weights.get(i).copied().unwrap_or(1.0); v * w }) .sum::() / weight_sum.max(1.0) } else { smoothed_values.iter().sum::() / smoothed_values.len().max(1) as f64 }; // Check threshold let threshold_crossed = smoothed_values.iter().any(|&v| v > self.config.threshold); // Get timestamp let timestamp = output.timestamps.last().copied().unwrap_or(0.0); let latency_ms = output.latency_us as f64 / 1000.0; let signal = ControlSignal { source_indices: monitor_sources.clone(), power, control_values: smoothed_values, combined_signal, threshold_crossed, timestamp, latency_ms, }; // Call callback if set if let Some(ref callback) = *self.callback.read() { callback(&signal); } signal } /// Get current pipeline state pub fn state(&self) -> PipelineState { self.pipeline.state() } /// Get latency statistics pub fn latency_stats(&self) -> Vec { self.pipeline.latency_stats() } /// Get latency report pub fn latency_report(&self) -> String { self.pipeline.latency_report() } /// Check if latency targets are met pub fn meets_latency_targets(&self) -> bool { self.pipeline.meets_latency_targets() } /// Get configuration pub fn config(&self) -> &BciConfig { &self.config } /// Set threshold pub fn set_threshold(&mut self, threshold: f64) { self.config.threshold = threshold; } /// Set smoothing factor pub fn set_smoothing(&mut self, smoothing: f64) { self.config.smoothing = smoothing.clamp(0.0, 1.0); } /// Set source weights pub fn set_source_weights(&mut self, weights: Vec) { self.config.source_weights = weights; } /// Get current baseline values pub fn baseline(&self) -> Vec { self.baseline.read().clone() } /// Set manual baseline values pub fn set_baseline(&self, baseline: Vec) { *self.baseline.write() = baseline; *self.baseline_calibrated.write() = true; } /// Reset baseline calibration pub fn reset_baseline(&self) { *self.baseline_calibrated.write() = false; self.baseline_samples.write().clear(); } } /// Motor imagery BCI configuration preset pub fn motor_imagery_config(n_channels: usize, sample_rate: f64) -> BciConfig { BciConfig { n_channels, sample_rate, buffer_duration_ms: 500.0, window_size_ms: 200.0, stride_ms: 50.0, filter_low: 8.0, // Mu/beta band filter_high: 30.0, monitor_sources: vec![0, 1], // Left/right motor cortex threshold: 0.6, source_weights: vec![1.0, -1.0], // Lateralized response baseline_duration: 3.0, smoothing: 0.4, } } /// SSVEP BCI configuration preset pub fn ssvep_config(n_channels: usize, sample_rate: f64) -> BciConfig { BciConfig { n_channels, sample_rate, buffer_duration_ms: 500.0, window_size_ms: 500.0, // Longer window for frequency resolution stride_ms: 100.0, filter_low: 5.0, // SSVEP typically 5-20 Hz filter_high: 25.0, monitor_sources: vec![0], // Visual cortex threshold: 0.7, source_weights: vec![1.0], baseline_duration: 2.0, smoothing: 0.5, } } /// P300 speller BCI configuration preset pub fn p300_config(n_channels: usize, sample_rate: f64) -> BciConfig { BciConfig { n_channels, sample_rate, buffer_duration_ms: 800.0, window_size_ms: 600.0, // Capture P300 component stride_ms: 100.0, filter_low: 0.5, // Include slow components filter_high: 15.0, monitor_sources: vec![0], // Parietal source threshold: 0.5, source_weights: vec![1.0], baseline_duration: 1.0, smoothing: 0.2, // Less smoothing for transient detection } } #[cfg(test)] mod tests { use super::*; #[test] fn test_bci_config_default() { let config = BciConfig::default(); assert_eq!(config.n_channels, 64); assert_eq!(config.sample_rate, 1000.0); } #[test] fn test_bci_pipeline_creation() { let config = BciConfig { n_channels: 32, sample_rate: 500.0, ..Default::default() }; let pipeline = BciPipeline::new(config).unwrap(); assert_eq!(pipeline.state(), PipelineState::Idle); } #[test] fn test_motor_imagery_preset() { let config = motor_imagery_config(64, 1000.0); assert_eq!(config.monitor_sources, vec![0, 1]); assert_eq!(config.source_weights, vec![1.0, -1.0]); } #[test] fn test_ssvep_preset() { let config = ssvep_config(32, 500.0); assert_eq!(config.window_size_ms, 500.0); } #[test] fn test_p300_preset() { let config = p300_config(16, 250.0); assert_eq!(config.filter_low, 0.5); } #[test] fn test_control_signal_default() { let signal = ControlSignal::default(); assert_eq!(signal.combined_signal, 0.0); assert!(!signal.threshold_crossed); } #[test] fn test_baseline_calibration() { let config = BciConfig { n_channels: 4, sample_rate: 100.0, baseline_duration: 0.1, // Short for testing stride_ms: 10.0, ..Default::default() }; let pipeline = BciPipeline::new(config).unwrap(); assert!(!pipeline.is_baseline_calibrated()); pipeline.start_baseline_calibration(); assert!(!pipeline.is_baseline_calibrated()); } #[test] fn test_set_threshold() { let config = BciConfig::default(); let mut pipeline = BciPipeline::new(config).unwrap(); pipeline.set_threshold(0.8); assert_eq!(pipeline.config().threshold, 0.8); } #[test] fn test_set_smoothing() { let config = BciConfig::default(); let mut pipeline = BciPipeline::new(config).unwrap(); pipeline.set_smoothing(0.5); assert_eq!(pipeline.config().smoothing, 0.5); // Test clamping pipeline.set_smoothing(1.5); assert_eq!(pipeline.config().smoothing, 1.0); pipeline.set_smoothing(-0.5); assert_eq!(pipeline.config().smoothing, 0.0); } #[test] fn test_manual_baseline() { let config = BciConfig { monitor_sources: vec![0, 1], ..Default::default() }; let pipeline = BciPipeline::new(config).unwrap(); pipeline.set_baseline(vec![1.5, 2.0]); assert!(pipeline.is_baseline_calibrated()); assert_eq!(pipeline.baseline(), vec![1.5, 2.0]); } }