Files
rustytorch/crates/specialized/rtx-neuro/src/channel.rs
T
2026-03-04 00:08:42 +00:00

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));
}
}