Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,560 @@
//! 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<dyn Fn(&ControlSignal) + Send + Sync>;
/// Control signal derived from source activity
#[derive(Debug, Clone)]
pub struct ControlSignal {
/// Source indices being monitored
pub source_indices: Vec<usize>,
/// Current power values for each source
pub power: Vec<f64>,
/// Normalized control values (0.0 - 1.0)
pub control_values: Vec<f64>,
/// 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<usize>,
/// Threshold for activation detection (0.0 - 1.0)
pub threshold: f64,
/// Source weights for combined signal
pub source_weights: Vec<f64>,
/// 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<RwLock<Vec<f64>>>,
/// Current smoothed values [n_sources]
smoothed_values: Arc<RwLock<Vec<f64>>>,
/// Callback for control signal updates
callback: Arc<RwLock<Option<SourceCallback>>>,
/// Whether baseline calibration is complete
baseline_calibrated: Arc<RwLock<bool>>,
/// Baseline samples collected
baseline_samples: Arc<RwLock<Vec<Vec<f64>>>>,
}
impl BciPipeline {
/// Create a new BCI pipeline
pub fn new(config: BciConfig) -> RealtimeResult<Self> {
// 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<f64>,
data_cov: &DMatrix<f64>,
) -> 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<F>(&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<f64>, timestamp: f64) {
self.pipeline.push_sample(data, timestamp);
}
/// Push a batch of samples
pub fn push_batch(&self, data: &[Vec<f64>], 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<ControlSignal> {
let output = self.pipeline.try_recv()?;
Some(self.compute_control_signal(&output))
}
/// Process with blocking wait
pub fn process_blocking(&self) -> Option<ControlSignal> {
let output = self.pipeline.recv()?;
Some(self.compute_control_signal(&output))
}
/// Process with timeout
pub fn process_timeout(&self, timeout: Duration) -> Option<ControlSignal> {
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::<f64>()
/ 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<f64> = (0..power.len())
.map(|i| baseline_samples.iter().map(|s| s[i]).sum::<f64>() / n_samples)
.collect();
*self.baseline.write() = baseline;
*self.baseline_calibrated.write() = true;
}
}
// Normalize by baseline
let baseline = self.baseline.read();
let control_values: Vec<f64> = 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::<f64>()
/ weight_sum.max(1.0)
} else {
smoothed_values.iter().sum::<f64>() / 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<LatencyStats> {
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<f64>) {
self.config.source_weights = weights;
}
/// Get current baseline values
pub fn baseline(&self) -> Vec<f64> {
self.baseline.read().clone()
}
/// Set manual baseline values
pub fn set_baseline(&self, baseline: Vec<f64>) {
*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]);
}
}