//! 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, /// 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, 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, } 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) -> 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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> { 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 for ChannelInfo { type Output = Channel; fn index(&self, index: usize) -> &Self::Output { &self.channels[index] } } impl std::ops::IndexMut 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)); } }