Files
rustytorch/crates/specialized/rtx-neuro-signal/src/baseline.rs
T
osobhandClaude Opus 4.6 02d382d5f6 style: apply rustfmt across all crates and demos
Consistent formatting pass: line wrapping, import sorting, trailing
whitespace removal, let-chain indentation, merged derive attributes,
and unsafe block reformatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
2026-04-12 07:01:58 -07:00

251 lines
8.3 KiB
Rust

//! Baseline correction for neuroimaging signals.
use crate::{SignalError, SignalResult};
/// Baseline correction method
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BaselineMethod {
/// Subtract mean of baseline period
#[default]
Mean,
/// Subtract median of baseline period
Median,
/// Z-score normalization (subtract mean, divide by std)
Zscore,
/// Percent change relative to baseline mean
Percent,
/// Decibel scale: 10 * log10(signal / baseline_mean)
Decibel,
/// Log ratio: log(signal / baseline_mean)
LogRatio,
}
/// Apply baseline correction to a signal.
///
/// # Arguments
/// * `signal` - Input signal samples
/// * `baseline_start` - Start index of baseline period
/// * `baseline_end` - End index of baseline period (exclusive)
/// * `method` - Baseline correction method
///
/// # Returns
/// Baseline-corrected signal
pub fn baseline_correct(
signal: &[f64],
baseline_start: usize,
baseline_end: usize,
method: BaselineMethod,
) -> SignalResult<Vec<f64>> {
if signal.is_empty() {
return Ok(Vec::new());
}
if baseline_start >= baseline_end || baseline_end > signal.len() {
return Err(SignalError::InvalidParameters(format!(
"Invalid baseline range [{baseline_start}, {baseline_end}) for signal length {}",
signal.len()
)));
}
let baseline = &signal[baseline_start..baseline_end];
match method {
BaselineMethod::Mean => {
let mean = baseline.iter().sum::<f64>() / baseline.len() as f64;
Ok(signal.iter().map(|&x| x - mean).collect())
}
BaselineMethod::Median => {
let median = compute_median(baseline);
Ok(signal.iter().map(|&x| x - median).collect())
}
BaselineMethod::Zscore => {
let mean = baseline.iter().sum::<f64>() / baseline.len() as f64;
let variance = baseline.iter().map(|&x| (x - mean).powi(2)).sum::<f64>()
/ (baseline.len() - 1) as f64;
let std = variance.sqrt();
if std < 1e-10 {
return Err(SignalError::Numerical(
"Baseline standard deviation is zero".to_string(),
));
}
Ok(signal.iter().map(|&x| (x - mean) / std).collect())
}
BaselineMethod::Percent => {
let mean = baseline.iter().sum::<f64>() / baseline.len() as f64;
if mean.abs() < 1e-10 {
return Err(SignalError::Numerical(
"Baseline mean is zero, cannot compute percent change".to_string(),
));
}
Ok(signal.iter().map(|&x| 100.0 * (x - mean) / mean).collect())
}
BaselineMethod::Decibel => {
let mean = baseline.iter().sum::<f64>() / baseline.len() as f64;
if mean <= 0.0 {
return Err(SignalError::Numerical(
"Baseline mean must be positive for decibel conversion".to_string(),
));
}
Ok(signal.iter().map(|&x| 10.0 * (x / mean).log10()).collect())
}
BaselineMethod::LogRatio => {
let mean = baseline.iter().sum::<f64>() / baseline.len() as f64;
if mean <= 0.0 {
return Err(SignalError::Numerical(
"Baseline mean must be positive for log ratio".to_string(),
));
}
Ok(signal.iter().map(|&x| (x / mean).ln()).collect())
}
}
}
/// Apply baseline correction to multiple epochs.
///
/// # Arguments
/// * `data` - Epoch data [n_epochs x n_channels x n_times] flattened
/// * `n_epochs` - Number of epochs
/// * `n_channels` - Number of channels
/// * `n_times` - Number of time points per epoch
/// * `baseline_start` - Start time index within each epoch
/// * `baseline_end` - End time index within each epoch
/// * `method` - Baseline correction method
pub fn baseline_correct_epochs(
data: &mut [f64],
n_epochs: usize,
n_channels: usize,
n_times: usize,
baseline_start: usize,
baseline_end: usize,
method: BaselineMethod,
) -> SignalResult<()> {
if baseline_start >= baseline_end || baseline_end > n_times {
return Err(SignalError::InvalidParameters(format!(
"Invalid baseline range [{baseline_start}, {baseline_end}) for n_times {n_times}"
)));
}
for epoch in 0..n_epochs {
let epoch_offset = epoch * n_channels * n_times;
for ch in 0..n_channels {
let ch_offset = epoch_offset + ch * n_times;
let ch_data = &mut data[ch_offset..ch_offset + n_times];
// Compute baseline statistics
let baseline = &ch_data[baseline_start..baseline_end];
let baseline_len = baseline.len() as f64;
match method {
BaselineMethod::Mean => {
let mean: f64 = baseline.iter().sum::<f64>() / baseline_len;
for x in ch_data.iter_mut() {
*x -= mean;
}
}
BaselineMethod::Median => {
let median = compute_median(baseline);
for x in ch_data.iter_mut() {
*x -= median;
}
}
BaselineMethod::Zscore => {
let mean: f64 = baseline.iter().sum::<f64>() / baseline_len;
let variance: f64 = baseline.iter().map(|&x| (x - mean).powi(2)).sum::<f64>()
/ (baseline_len - 1.0);
let std = variance.sqrt();
if std > 1e-10 {
for x in ch_data.iter_mut() {
*x = (*x - mean) / std;
}
}
}
BaselineMethod::Percent => {
let mean: f64 = baseline.iter().sum::<f64>() / baseline_len;
if mean.abs() > 1e-10 {
for x in ch_data.iter_mut() {
*x = 100.0 * (*x - mean) / mean;
}
}
}
BaselineMethod::Decibel => {
let mean: f64 = baseline.iter().sum::<f64>() / baseline_len;
if mean > 0.0 {
for x in ch_data.iter_mut() {
*x = 10.0 * (*x / mean).log10();
}
}
}
BaselineMethod::LogRatio => {
let mean: f64 = baseline.iter().sum::<f64>() / baseline_len;
if mean > 0.0 {
for x in ch_data.iter_mut() {
*x = (*x / mean).ln();
}
}
}
}
}
}
Ok(())
}
/// Compute the median of a slice
fn compute_median(data: &[f64]) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut sorted: Vec<f64> = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let mid = sorted.len() / 2;
if sorted.len().is_multiple_of(2) {
f64::midpoint(sorted[mid - 1], sorted[mid])
} else {
sorted[mid]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_baseline_mean() {
let signal = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
// Baseline is first 5 samples: mean = 3.0
let corrected = baseline_correct(&signal, 0, 5, BaselineMethod::Mean).unwrap();
assert!((corrected[0] - (-2.0)).abs() < 1e-10);
assert!((corrected[4] - 2.0).abs() < 1e-10);
assert!((corrected[9] - 7.0).abs() < 1e-10);
}
#[test]
fn test_baseline_zscore() {
let signal = vec![0.0, 1.0, 2.0, 3.0, 4.0]; // mean=1, std=~1.58
let corrected = baseline_correct(&signal, 0, 3, BaselineMethod::Zscore).unwrap();
// After z-scoring, baseline should have mean 0, std ~1
let baseline_mean: f64 = corrected[..3].iter().sum::<f64>() / 3.0;
assert!(baseline_mean.abs() < 1e-10);
}
#[test]
fn test_median() {
assert!((compute_median(&[1.0, 2.0, 3.0]) - 2.0).abs() < 1e-10);
assert!((compute_median(&[1.0, 2.0, 3.0, 4.0]) - 2.5).abs() < 1e-10);
}
}