332 lines
8.7 KiB
Rust
332 lines
8.7 KiB
Rust
//! Channel information and types for MEG/EEG recordings.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Type of recording channel
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum ChannelType {
|
|
/// Scalp EEG electrode
|
|
EegScalp,
|
|
/// Intracranial EEG (iEEG, sEEG, ECoG)
|
|
EegIntracranial,
|
|
/// MEG gradiometer
|
|
MegGrad,
|
|
/// MEG magnetometer
|
|
MegMag,
|
|
/// MEG reference channel
|
|
MegRef,
|
|
/// Electrooculogram (eye movement)
|
|
Eog,
|
|
/// Electrocardiogram (heart)
|
|
Ecg,
|
|
/// Electromyogram (muscle)
|
|
Emg,
|
|
/// Stimulus/trigger channel
|
|
Stim,
|
|
/// Miscellaneous channel
|
|
Misc,
|
|
/// System channel (e.g., head position)
|
|
System,
|
|
/// Functional Near-Infrared Spectroscopy
|
|
Fnirs,
|
|
/// Other/custom channel type
|
|
Other(u8),
|
|
}
|
|
|
|
impl ChannelType {
|
|
/// Returns true if this is an EEG channel (scalp or intracranial)
|
|
#[must_use]
|
|
pub fn is_eeg(&self) -> bool {
|
|
matches!(self, Self::EegScalp | Self::EegIntracranial)
|
|
}
|
|
|
|
/// Returns true if this is an MEG channel (grad, mag, or ref)
|
|
#[must_use]
|
|
pub fn is_meg(&self) -> bool {
|
|
matches!(self, Self::MegGrad | Self::MegMag | Self::MegRef)
|
|
}
|
|
|
|
/// Returns true if this is a data channel (EEG, MEG, or FNIRS)
|
|
#[must_use]
|
|
pub fn is_data(&self) -> bool {
|
|
self.is_eeg() || self.is_meg() || matches!(self, Self::Fnirs)
|
|
}
|
|
|
|
/// Returns true if this is a physiological channel (EOG, ECG, EMG)
|
|
#[must_use]
|
|
pub fn is_physio(&self) -> bool {
|
|
matches!(self, Self::Eog | Self::Ecg | Self::Emg)
|
|
}
|
|
|
|
/// Returns the default unit for this channel type
|
|
#[must_use]
|
|
pub fn default_unit(&self) -> &'static str {
|
|
match self {
|
|
Self::EegScalp | Self::EegIntracranial => "uV",
|
|
Self::MegGrad => "fT/cm",
|
|
Self::MegMag => "fT",
|
|
Self::MegRef => "fT",
|
|
Self::Eog => "uV",
|
|
Self::Ecg => "uV",
|
|
Self::Emg => "uV",
|
|
Self::Stim => "V",
|
|
Self::Fnirs => "mol/L",
|
|
_ => "AU",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ChannelType {
|
|
fn default() -> Self {
|
|
Self::EegScalp
|
|
}
|
|
}
|
|
|
|
/// Information about a single channel
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Channel {
|
|
/// Channel name/label (e.g., "Fz", "MEG0111")
|
|
pub name: String,
|
|
/// Channel type
|
|
pub ch_type: ChannelType,
|
|
/// Physical unit (e.g., "uV", "fT")
|
|
pub unit: String,
|
|
/// 3D position in head coordinates [x, y, z] in meters
|
|
pub loc: Option<[f64; 3]>,
|
|
/// Orientation vector for MEG sensors [x, y, z]
|
|
pub orientation: Option<[f64; 3]>,
|
|
/// Reference electrode name (for EEG)
|
|
pub reference: Option<String>,
|
|
/// Physical minimum value
|
|
pub physical_min: f64,
|
|
/// Physical maximum value
|
|
pub physical_max: f64,
|
|
/// Digital minimum value
|
|
pub digital_min: i32,
|
|
/// Digital maximum value
|
|
pub digital_max: i32,
|
|
/// Whether this channel is marked as bad
|
|
pub bad: bool,
|
|
}
|
|
|
|
impl Channel {
|
|
/// Create a new channel with the given name and type
|
|
#[must_use]
|
|
pub fn new(name: impl Into<String>, ch_type: ChannelType) -> Self {
|
|
let unit = ch_type.default_unit().to_string();
|
|
Self {
|
|
name: name.into(),
|
|
ch_type,
|
|
unit,
|
|
loc: None,
|
|
orientation: None,
|
|
reference: None,
|
|
physical_min: -3200.0,
|
|
physical_max: 3200.0,
|
|
digital_min: -32768,
|
|
digital_max: 32767,
|
|
bad: false,
|
|
}
|
|
}
|
|
|
|
/// Set the 3D location of this channel
|
|
#[must_use]
|
|
pub fn with_location(mut self, loc: [f64; 3]) -> Self {
|
|
self.loc = Some(loc);
|
|
self
|
|
}
|
|
|
|
/// Mark this channel as bad
|
|
pub fn mark_bad(&mut self) {
|
|
self.bad = true;
|
|
}
|
|
|
|
/// Mark this channel as good
|
|
pub fn mark_good(&mut self) {
|
|
self.bad = false;
|
|
}
|
|
|
|
/// Compute the scaling factor to convert digital to physical values
|
|
#[must_use]
|
|
pub fn scale_factor(&self) -> f64 {
|
|
let digital_range = f64::from(self.digital_max - self.digital_min);
|
|
let physical_range = self.physical_max - self.physical_min;
|
|
if digital_range.abs() < f64::EPSILON {
|
|
1.0
|
|
} else {
|
|
physical_range / digital_range
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collection of channels with metadata
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChannelInfo {
|
|
/// List of channels
|
|
pub channels: Vec<Channel>,
|
|
}
|
|
|
|
impl ChannelInfo {
|
|
/// Create a new empty channel info
|
|
#[must_use]
|
|
pub fn new() -> Self {
|
|
Self {
|
|
channels: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Create channel info from a list of channels
|
|
#[must_use]
|
|
pub fn from_channels(channels: Vec<Channel>) -> Self {
|
|
Self { channels }
|
|
}
|
|
|
|
/// Number of channels
|
|
#[must_use]
|
|
pub fn len(&self) -> usize {
|
|
self.channels.len()
|
|
}
|
|
|
|
/// Returns true if there are no channels
|
|
#[must_use]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.channels.is_empty()
|
|
}
|
|
|
|
/// Get channel names
|
|
#[must_use]
|
|
pub fn names(&self) -> Vec<&str> {
|
|
self.channels.iter().map(|c| c.name.as_str()).collect()
|
|
}
|
|
|
|
/// Get channel types
|
|
#[must_use]
|
|
pub fn types(&self) -> Vec<ChannelType> {
|
|
self.channels.iter().map(|c| c.ch_type).collect()
|
|
}
|
|
|
|
/// Find channel index by name
|
|
#[must_use]
|
|
pub fn find_by_name(&self, name: &str) -> Option<usize> {
|
|
self.channels.iter().position(|c| c.name == name)
|
|
}
|
|
|
|
/// Get indices of channels matching a type
|
|
#[must_use]
|
|
pub fn pick_types(&self, ch_type: ChannelType) -> Vec<usize> {
|
|
self.channels
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, c)| if c.ch_type == ch_type { Some(i) } else { None })
|
|
.collect()
|
|
}
|
|
|
|
/// Get indices of EEG channels
|
|
#[must_use]
|
|
pub fn pick_eeg(&self) -> Vec<usize> {
|
|
self.channels
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, c)| if c.ch_type.is_eeg() { Some(i) } else { None })
|
|
.collect()
|
|
}
|
|
|
|
/// Get indices of MEG channels
|
|
#[must_use]
|
|
pub fn pick_meg(&self) -> Vec<usize> {
|
|
self.channels
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, c)| if c.ch_type.is_meg() { Some(i) } else { None })
|
|
.collect()
|
|
}
|
|
|
|
/// Get indices of bad channels
|
|
#[must_use]
|
|
pub fn bad_channels(&self) -> Vec<usize> {
|
|
self.channels
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, c)| if c.bad { Some(i) } else { None })
|
|
.collect()
|
|
}
|
|
|
|
/// Get indices of good channels
|
|
#[must_use]
|
|
pub fn good_channels(&self) -> Vec<usize> {
|
|
self.channels
|
|
.iter()
|
|
.enumerate()
|
|
.filter_map(|(i, c)| if !c.bad { Some(i) } else { None })
|
|
.collect()
|
|
}
|
|
|
|
/// Add a channel
|
|
pub fn add_channel(&mut self, channel: Channel) {
|
|
self.channels.push(channel);
|
|
}
|
|
|
|
/// Get channel positions as a matrix [n_channels x 3]
|
|
#[must_use]
|
|
pub fn get_positions(&self) -> Option<Vec<[f64; 3]>> {
|
|
let positions: Vec<_> = self.channels.iter().filter_map(|c| c.loc).collect();
|
|
if positions.len() == self.channels.len() {
|
|
Some(positions)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ChannelInfo {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl std::ops::Index<usize> for ChannelInfo {
|
|
type Output = Channel;
|
|
|
|
fn index(&self, index: usize) -> &Self::Output {
|
|
&self.channels[index]
|
|
}
|
|
}
|
|
|
|
impl std::ops::IndexMut<usize> for ChannelInfo {
|
|
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
|
|
&mut self.channels[index]
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_channel_type_classification() {
|
|
assert!(ChannelType::EegScalp.is_eeg());
|
|
assert!(ChannelType::EegIntracranial.is_eeg());
|
|
assert!(!ChannelType::MegGrad.is_eeg());
|
|
|
|
assert!(ChannelType::MegGrad.is_meg());
|
|
assert!(ChannelType::MegMag.is_meg());
|
|
assert!(!ChannelType::EegScalp.is_meg());
|
|
|
|
assert!(ChannelType::Eog.is_physio());
|
|
assert!(ChannelType::Ecg.is_physio());
|
|
}
|
|
|
|
#[test]
|
|
fn test_channel_info_pick() {
|
|
let mut info = ChannelInfo::new();
|
|
info.add_channel(Channel::new("Fz", ChannelType::EegScalp));
|
|
info.add_channel(Channel::new("Cz", ChannelType::EegScalp));
|
|
info.add_channel(Channel::new("EOG", ChannelType::Eog));
|
|
info.add_channel(Channel::new("MEG0111", ChannelType::MegGrad));
|
|
|
|
assert_eq!(info.pick_eeg(), vec![0, 1]);
|
|
assert_eq!(info.pick_meg(), vec![3]);
|
|
assert_eq!(info.find_by_name("Cz"), Some(1));
|
|
}
|
|
}
|