Skip to main content

bids_eeg/
channels.rs

1//! EEG channel definitions and `_channels.tsv` parsing.
2//!
3//! Provides [`Channel`] (name, type, units, sampling frequency, status) and
4//! [`ChannelType`] covering all BIDS-defined EEG/MEG/iEEG channel types.
5
6use bids_core::error::{BidsError, Result};
7use bids_io::tsv::read_tsv;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11/// Type of EEG channel.
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum ChannelType {
14    EEG,
15    EOG,
16    ECG,
17    EMG,
18    MISC,
19    TRIG,
20    REF,
21    MEGMAG,
22    MEGGRAD,
23    MEGREF,
24    ECOG,
25    SEEG,
26    DBS,
27    VEOG,
28    HEOG,
29    Audio,
30    PD,
31    EYEGAZE,
32    PUPIL,
33    SysClock,
34    ADC,
35    DAC,
36    HLU,
37    FITERR,
38    Other(String),
39}
40
41impl ChannelType {
42    pub fn parse(s: &str) -> Self {
43        match s.to_uppercase().as_str() {
44            "EEG" => Self::EEG,
45            "EOG" => Self::EOG,
46            "ECG" | "EKG" => Self::ECG,
47            "EMG" => Self::EMG,
48            "MISC" => Self::MISC,
49            "TRIG" | "TRIGGER" => Self::TRIG,
50            "REF" => Self::REF,
51            "MEGMAG" => Self::MEGMAG,
52            "MEGGRAD" => Self::MEGGRAD,
53            "MEGREF" | "MEGREFMAG" | "MEGREFGRAD" => Self::MEGREF,
54            "ECOG" => Self::ECOG,
55            "SEEG" => Self::SEEG,
56            "DBS" => Self::DBS,
57            "VEOG" => Self::VEOG,
58            "HEOG" => Self::HEOG,
59            "AUDIO" => Self::Audio,
60            "PD" | "PHOTODIODE" => Self::PD,
61            "EYEGAZE" => Self::EYEGAZE,
62            "PUPIL" => Self::PUPIL,
63            "SYSCLOCK" => Self::SysClock,
64            "ADC" => Self::ADC,
65            "DAC" => Self::DAC,
66            "HLU" => Self::HLU,
67            "FITERR" => Self::FITERR,
68            other => Self::Other(other.to_string()),
69        }
70    }
71}
72
73impl std::fmt::Display for ChannelType {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Self::Other(s) => write!(f, "{s}"),
77            _ => write!(f, "{self:?}"),
78        }
79    }
80}
81
82/// A single EEG channel definition from _channels.tsv.
83///
84/// See: <https://bids-specification.readthedocs.io/en/stable/modality-specific-files/electroencephalography.html>
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Channel {
87    /// Channel name (required).
88    pub name: String,
89    /// Channel type (required): EEG, EOG, ECG, EMG, MISC, TRIG, etc.
90    pub channel_type: ChannelType,
91    /// Units of the channel data (required): e.g., "µV", "microV", "mV".
92    pub units: String,
93    /// Description of the channel.
94    pub description: Option<String>,
95    /// Sampling frequency in Hz (if different from the main recording).
96    pub sampling_frequency: Option<f64>,
97    /// Low cutoff frequency of the hardware filter in Hz.
98    pub low_cutoff: Option<f64>,
99    /// High cutoff frequency of the hardware filter in Hz.
100    pub high_cutoff: Option<f64>,
101    /// Notch filter frequency in Hz.
102    pub notch: Option<f64>,
103    /// Reference for this channel.
104    pub reference: Option<String>,
105    /// Status: "good" or "bad".
106    pub status: Option<String>,
107    /// Description of why the channel is bad.
108    pub status_description: Option<String>,
109}
110
111impl Channel {
112    /// Whether this channel is marked as "bad".
113    pub fn is_bad(&self) -> bool {
114        self.status.as_deref() == Some("bad")
115    }
116
117    /// Whether this is an EEG-type channel.
118    pub fn is_eeg(&self) -> bool {
119        self.channel_type == ChannelType::EEG
120    }
121}
122
123/// Read channels from a BIDS _channels.tsv file.
124pub fn read_channels_tsv(path: &Path) -> Result<Vec<Channel>> {
125    let rows = read_tsv(path)?;
126    let mut channels = Vec::with_capacity(rows.len());
127
128    for row in &rows {
129        let name = row
130            .get("name")
131            .ok_or_else(|| BidsError::Csv("Missing 'name' column in channels.tsv".into()))?
132            .trim()
133            .to_string();
134
135        let channel_type = row
136            .get("type")
137            .map(|s| ChannelType::parse(s.trim()))
138            .unwrap_or(ChannelType::MISC);
139
140        let units = row
141            .get("units")
142            .cloned()
143            .unwrap_or_else(|| "n/a".to_string())
144            .trim()
145            .to_string();
146
147        let channel = Channel {
148            name,
149            channel_type,
150            units,
151            description: non_empty(row.get("description")),
152            sampling_frequency: parse_f64(row.get("sampling_frequency")),
153            low_cutoff: parse_f64(row.get("low_cutoff")),
154            high_cutoff: parse_f64(row.get("high_cutoff")),
155            notch: parse_f64(row.get("notch")),
156            reference: non_empty(row.get("reference")),
157            status: non_empty(row.get("status")),
158            status_description: non_empty(row.get("status_description")),
159        };
160        channels.push(channel);
161    }
162
163    Ok(channels)
164}
165
166fn non_empty(val: Option<&String>) -> Option<String> {
167    val.filter(|s| !s.is_empty() && *s != "n/a")
168        .map(|s| s.trim().to_string())
169}
170
171fn parse_f64(val: Option<&String>) -> Option<f64> {
172    val.and_then(|s| s.trim().parse().ok())
173}
174
175/// Count channels by type.
176pub fn count_by_type(channels: &[Channel]) -> std::collections::HashMap<ChannelType, usize> {
177    let mut counts = std::collections::HashMap::new();
178    for ch in channels {
179        *counts.entry(ch.channel_type.clone()).or_insert(0) += 1;
180    }
181    counts
182}
183
184/// Get only the EEG channels.
185pub fn eeg_channels(channels: &[Channel]) -> Vec<&Channel> {
186    channels.iter().filter(|c| c.is_eeg()).collect()
187}
188
189/// Get only the bad channels.
190pub fn bad_channels(channels: &[Channel]) -> Vec<&Channel> {
191    channels.iter().filter(|c| c.is_bad()).collect()
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use std::io::Write;
198
199    #[test]
200    fn test_read_channels_tsv() {
201        let dir = std::env::temp_dir().join("bids_eeg_channels_test");
202        std::fs::create_dir_all(&dir).unwrap();
203        let path = dir.join("channels.tsv");
204        let mut f = std::fs::File::create(&path).unwrap();
205        writeln!(
206            f,
207            "name\ttype\tunits\tsampling_frequency\tlow_cutoff\thigh_cutoff\tnotch"
208        )
209        .unwrap();
210        writeln!(f, "Fp1\tEEG\tmicroV\t256\t0.5\t100\t50").unwrap();
211        writeln!(f, "Fp2\tEEG\tmicroV\t256\t0.5\t100\t50").unwrap();
212        writeln!(f, "EOG1\tEOG\tmicroV\t256\tn/a\tn/a\tn/a").unwrap();
213
214        let channels = read_channels_tsv(&path).unwrap();
215        assert_eq!(channels.len(), 3);
216        assert_eq!(channels[0].name, "Fp1");
217        assert_eq!(channels[0].channel_type, ChannelType::EEG);
218        assert_eq!(channels[0].sampling_frequency, Some(256.0));
219        assert_eq!(channels[2].channel_type, ChannelType::EOG);
220        assert_eq!(channels[2].notch, None);
221
222        assert_eq!(eeg_channels(&channels).len(), 2);
223        assert_eq!(bad_channels(&channels).len(), 0);
224
225        std::fs::remove_dir_all(&dir).unwrap();
226    }
227}