//! Real-time streaming pipeline for MEG/EEG source localization. //! //! Orchestrates the data flow from LSL input through filtering, beamforming, //! and source estimation with sub-10ms latency. use crate::error::{RealtimeError, RealtimeResult}; use crate::gpu_beamformer::GpuBeamformer; use crate::gpu_filter::{FilterType, GpuFilter, GpuFilterConfig}; use crate::latency::{LatencyMonitor, LatencyStage, LatencyStats}; use crate::ring_buffer::{SharedRingBuffer, create_shared_buffer}; use crossbeam_channel::{Receiver, Sender, TryRecvError, bounded}; use parking_lot::RwLock; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::thread::{self, JoinHandle}; use std::time::{Duration, Instant}; /// Pipeline state #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PipelineState { /// Pipeline is created but not running Idle, /// Pipeline is starting up Starting, /// Pipeline is running and processing data Running, /// Pipeline is paused (can resume) Paused, /// Pipeline is stopping Stopping, /// Pipeline has stopped Stopped, /// Pipeline encountered an error Error, } impl std::fmt::Display for PipelineState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Idle => write!(f, "Idle"), Self::Starting => write!(f, "Starting"), Self::Running => write!(f, "Running"), Self::Paused => write!(f, "Paused"), Self::Stopping => write!(f, "Stopping"), Self::Stopped => write!(f, "Stopped"), Self::Error => write!(f, "Error"), } } } /// Pipeline configuration #[derive(Debug, Clone)] pub struct PipelineConfig { /// 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 window_stride_ms: f64, /// Filter low cutoff (Hz) pub filter_low: f64, /// Filter high cutoff (Hz) pub filter_high: f64, /// Enable filtering pub enable_filtering: bool, /// Enable beamforming pub enable_beamforming: bool, /// Target latency in milliseconds pub target_latency_ms: f64, /// Output queue size pub output_queue_size: usize, } impl Default for PipelineConfig { fn default() -> Self { Self { n_channels: 64, sample_rate: 1000.0, buffer_duration_ms: 500.0, window_size_ms: 100.0, window_stride_ms: 50.0, filter_low: 1.0, filter_high: 40.0, enable_filtering: true, enable_beamforming: true, target_latency_ms: 10.0, output_queue_size: 100, } } } /// Source estimate output #[derive(Debug, Clone)] pub struct SourceOutput { /// Source data [n_sources x n_times] pub sources: Vec>, /// Timestamps for source estimates pub timestamps: Vec, /// Processing latency in microseconds pub latency_us: u64, /// Window index pub window_index: u64, } /// Real-time processing pipeline pub struct RealtimePipeline { /// Configuration config: PipelineConfig, /// Pipeline state state: Arc>, /// Input ring buffer input_buffer: SharedRingBuffer, /// GPU filter (if enabled) filter: Option>>, /// GPU beamformer (if enabled) beamformer: Option>, /// Output channel sender output_tx: Option>, /// Output channel receiver output_rx: Option>, /// Latency monitor latency_monitor: Arc, /// Running flag running: Arc, /// Processed windows counter windows_processed: Arc, /// Processing thread handle processing_thread: Option>, /// Last error message last_error: Arc>>, } impl RealtimePipeline { /// Create a new pipeline pub fn new(config: PipelineConfig) -> RealtimeResult { // Create input buffer let buffer_duration = config.buffer_duration_ms / 1000.0; let input_buffer = create_shared_buffer(buffer_duration, config.sample_rate, config.n_channels); // Create filter if enabled let filter = if config.enable_filtering { let filter_config = GpuFilterConfig { filter_type: FilterType::Bandpass, low_freq: config.filter_low, high_freq: config.filter_high, sample_rate: config.sample_rate, order: 101, transition_bw: 2.0, }; Some(Arc::new(RwLock::new(GpuFilter::new( filter_config, config.n_channels, )?))) } else { None }; // Create output channel let (output_tx, output_rx) = bounded(config.output_queue_size); // Create latency monitor let latency_monitor = Arc::new(LatencyMonitor::new(1000)); Ok(Self { config, state: Arc::new(RwLock::new(PipelineState::Idle)), input_buffer, filter, beamformer: None, output_tx: Some(output_tx), output_rx: Some(output_rx), latency_monitor, running: Arc::new(AtomicBool::new(false)), windows_processed: Arc::new(AtomicU64::new(0)), processing_thread: None, last_error: Arc::new(RwLock::new(None)), }) } /// Set the beamformer pub fn set_beamformer(&mut self, beamformer: GpuBeamformer) { self.beamformer = Some(Arc::new(beamformer)); } /// Push data into the pipeline /// /// # Arguments /// * `data` - Sample data [n_channels] /// * `timestamp` - LSL timestamp pub fn push_sample(&self, data: Vec, timestamp: f64) { self.input_buffer.push(data, timestamp); } /// Push a batch of data pub fn push_batch(&self, data: &[Vec], timestamps: &[f64]) { self.input_buffer.push_batch(data, timestamps); } /// Start the processing pipeline pub fn start(&mut self) -> RealtimeResult<()> { if *self.state.read() == PipelineState::Running { return Ok(()); } *self.state.write() = PipelineState::Starting; self.running.store(true, Ordering::SeqCst); // Clone shared resources for the processing thread let config = self.config.clone(); let state = Arc::clone(&self.state); let input_buffer = Arc::clone(&self.input_buffer); let filter = self.filter.clone(); let beamformer = self.beamformer.clone(); let output_tx = self.output_tx.clone().ok_or_else(|| { RealtimeError::PipelineState("Output channel not available".to_string()) })?; let latency_monitor = Arc::clone(&self.latency_monitor); let running = Arc::clone(&self.running); let windows_processed = Arc::clone(&self.windows_processed); let last_error = Arc::clone(&self.last_error); // Spawn processing thread let handle = thread::spawn(move || { *state.write() = PipelineState::Running; let window_samples = (config.window_size_ms / 1000.0 * config.sample_rate) as usize; let stride_samples = (config.window_stride_ms / 1000.0 * config.sample_rate) as usize; let mut last_process_time = Instant::now(); let stride_duration = Duration::from_secs_f64(config.window_stride_ms / 1000.0); while running.load(Ordering::SeqCst) { let start = Instant::now(); // Check if we have enough data if input_buffer.len() < window_samples { thread::sleep(Duration::from_millis(1)); continue; } // Wait for stride interval let elapsed = last_process_time.elapsed(); if elapsed < stride_duration { thread::sleep(stride_duration.checked_sub(elapsed).unwrap()); } last_process_time = Instant::now(); // Get data window latency_monitor.start(LatencyStage::LslReceive); let window_sec = config.window_size_ms / 1000.0; let data_matrix = input_buffer.get_recent_matrix(window_sec); let (_, timestamps) = input_buffer.get_recent(window_sec); latency_monitor.stop(LatencyStage::LslReceive); // Apply filter if enabled let filtered_data = if let Some(ref filter) = filter { latency_monitor.start(LatencyStage::GpuFilter); let result = { let mut f = filter.write(); f.apply(&data_matrix) }; latency_monitor.stop(LatencyStage::GpuFilter); match result { Ok(d) => d, Err(e) => { *last_error.write() = Some(e.to_string()); continue; } } } else { data_matrix }; // Apply beamformer if enabled let source_data = if let Some(ref bf) = beamformer { latency_monitor.start(LatencyStage::GpuBeamformer); let result = bf.apply(&filtered_data); latency_monitor.stop(LatencyStage::GpuBeamformer); match result { Ok(d) => d, Err(e) => { *last_error.write() = Some(e.to_string()); continue; } } } else { filtered_data }; // Record total latency let total_latency = start.elapsed(); latency_monitor.record(LatencyStage::Total, total_latency); // Create output let output = SourceOutput { sources: source_data, timestamps: timestamps.clone(), latency_us: total_latency.as_micros() as u64, window_index: windows_processed.fetch_add(1, Ordering::SeqCst), }; // Send output (non-blocking) if output_tx.try_send(output).is_err() { // Queue full, drop oldest } } *state.write() = PipelineState::Stopped; }); self.processing_thread = Some(handle); Ok(()) } /// Stop the pipeline pub fn stop(&mut self) -> RealtimeResult<()> { *self.state.write() = PipelineState::Stopping; self.running.store(false, Ordering::SeqCst); if let Some(handle) = self.processing_thread.take() { handle.join().map_err(|_| { RealtimeError::PipelineState("Failed to join processing thread".to_string()) })?; } *self.state.write() = PipelineState::Stopped; Ok(()) } /// Pause the pipeline pub fn pause(&mut self) -> RealtimeResult<()> { *self.state.write() = PipelineState::Paused; self.running.store(false, Ordering::SeqCst); Ok(()) } /// Resume the pipeline pub fn resume(&mut self) -> RealtimeResult<()> { if *self.state.read() == PipelineState::Paused { self.start() } else { Err(RealtimeError::PipelineState( "Pipeline is not paused".to_string(), )) } } /// Get the next source output (blocking) pub fn recv(&self) -> Option { self.output_rx.as_ref()?.recv().ok() } /// Get the next source output (non-blocking) pub fn try_recv(&self) -> Option { match self.output_rx.as_ref()?.try_recv() { Ok(output) => Some(output), Err(TryRecvError::Empty) => None, Err(TryRecvError::Disconnected) => None, } } /// Get the next source output with timeout pub fn recv_timeout(&self, timeout: Duration) -> Option { self.output_rx.as_ref()?.recv_timeout(timeout).ok() } /// Get current pipeline state pub fn state(&self) -> PipelineState { *self.state.read() } /// Get latency statistics pub fn latency_stats(&self) -> Vec { self.latency_monitor.all_stats() } /// Get latency report pub fn latency_report(&self) -> String { self.latency_monitor.report() } /// Check if latency targets are met pub fn meets_latency_targets(&self) -> bool { self.latency_monitor.all_meet_targets() } /// Get number of processed windows pub fn windows_processed(&self) -> u64 { self.windows_processed.load(Ordering::SeqCst) } /// Get input buffer fill level (0.0 - 1.0) pub fn buffer_fill_level(&self) -> f64 { self.input_buffer.fill_level() } /// Get last error message pub fn last_error(&self) -> Option { self.last_error.read().clone() } /// Clear last error pub fn clear_error(&self) { *self.last_error.write() = None; } /// Get configuration pub fn config(&self) -> &PipelineConfig { &self.config } /// Reset the pipeline (clears buffers and statistics) pub fn reset(&mut self) { self.input_buffer.clear(); self.latency_monitor.clear(); self.windows_processed.store(0, Ordering::SeqCst); self.clear_error(); } } impl Drop for RealtimePipeline { fn drop(&mut self) { self.running.store(false, Ordering::SeqCst); if let Some(handle) = self.processing_thread.take() { let _ = handle.join(); } } } /// Pipeline builder for fluent configuration pub struct PipelineBuilder { config: PipelineConfig, } impl PipelineBuilder { /// Create a new builder with default configuration pub fn new() -> Self { Self { config: PipelineConfig::default(), } } /// Set number of channels pub fn channels(mut self, n_channels: usize) -> Self { self.config.n_channels = n_channels; self } /// Set sample rate pub fn sample_rate(mut self, rate: f64) -> Self { self.config.sample_rate = rate; self } /// Set buffer duration in milliseconds pub fn buffer_duration_ms(mut self, duration: f64) -> Self { self.config.buffer_duration_ms = duration; self } /// Set processing window size in milliseconds pub fn window_size_ms(mut self, size: f64) -> Self { self.config.window_size_ms = size; self } /// Set window stride in milliseconds pub fn window_stride_ms(mut self, stride: f64) -> Self { self.config.window_stride_ms = stride; self } /// Set filter band (low, high in Hz) pub fn filter_band(mut self, low: f64, high: f64) -> Self { self.config.filter_low = low; self.config.filter_high = high; self } /// Enable or disable filtering pub fn enable_filtering(mut self, enable: bool) -> Self { self.config.enable_filtering = enable; self } /// Enable or disable beamforming pub fn enable_beamforming(mut self, enable: bool) -> Self { self.config.enable_beamforming = enable; self } /// Set target latency in milliseconds pub fn target_latency_ms(mut self, latency: f64) -> Self { self.config.target_latency_ms = latency; self } /// Build the pipeline pub fn build(self) -> RealtimeResult { RealtimePipeline::new(self.config) } } impl Default for PipelineBuilder { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_pipeline_creation() { let config = PipelineConfig::default(); let pipeline = RealtimePipeline::new(config).unwrap(); assert_eq!(pipeline.state(), PipelineState::Idle); } #[test] fn test_pipeline_builder() { let pipeline = PipelineBuilder::new() .channels(32) .sample_rate(500.0) .filter_band(8.0, 30.0) .window_size_ms(50.0) .build() .unwrap(); assert_eq!(pipeline.config().n_channels, 32); assert_eq!(pipeline.config().sample_rate, 500.0); } #[test] fn test_push_sample() { let config = PipelineConfig { n_channels: 4, sample_rate: 100.0, ..Default::default() }; let pipeline = RealtimePipeline::new(config).unwrap(); pipeline.push_sample(vec![1.0, 2.0, 3.0, 4.0], 0.0); pipeline.push_sample(vec![5.0, 6.0, 7.0, 8.0], 0.01); assert_eq!(pipeline.input_buffer.len(), 2); } #[test] fn test_push_batch() { let config = PipelineConfig { n_channels: 2, sample_rate: 100.0, ..Default::default() }; let pipeline = RealtimePipeline::new(config).unwrap(); let data = vec![vec![1.0, 2.0], vec![3.0, 4.0], vec![5.0, 6.0]]; let timestamps = vec![0.0, 0.01, 0.02]; pipeline.push_batch(&data, ×tamps); assert_eq!(pipeline.input_buffer.len(), 3); } #[test] fn test_pipeline_lifecycle() { let config = PipelineConfig { n_channels: 4, sample_rate: 100.0, window_size_ms: 100.0, enable_beamforming: false, ..Default::default() }; let mut pipeline = RealtimePipeline::new(config).unwrap(); // Start pipeline.start().unwrap(); assert_eq!(pipeline.state(), PipelineState::Starting); // Give it time to start thread::sleep(Duration::from_millis(10)); // Stop pipeline.stop().unwrap(); assert_eq!(pipeline.state(), PipelineState::Stopped); } #[test] fn test_latency_monitor() { let config = PipelineConfig::default(); let pipeline = RealtimePipeline::new(config).unwrap(); let stats = pipeline.latency_stats(); assert_eq!(stats.len(), 5); } }