1use bids_core::error::{BidsError, Result};
7use bids_io::tsv::read_tsv;
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11#[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#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct Channel {
87 pub name: String,
89 pub channel_type: ChannelType,
91 pub units: String,
93 pub description: Option<String>,
95 pub sampling_frequency: Option<f64>,
97 pub low_cutoff: Option<f64>,
99 pub high_cutoff: Option<f64>,
101 pub notch: Option<f64>,
103 pub reference: Option<String>,
105 pub status: Option<String>,
107 pub status_description: Option<String>,
109}
110
111impl Channel {
112 pub fn is_bad(&self) -> bool {
114 self.status.as_deref() == Some("bad")
115 }
116
117 pub fn is_eeg(&self) -> bool {
119 self.channel_type == ChannelType::EEG
120 }
121}
122
123pub 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
175pub 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
184pub fn eeg_channels(channels: &[Channel]) -> Vec<&Channel> {
186 channels.iter().filter(|c| c.is_eeg()).collect()
187}
188
189pub 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}