modewise_dataset/
channels.rs1use std::path::Path;
5
6use modewise_brain::{ChannelInfo, ChannelTable, Hemisphere, Region};
7use serde::Deserialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum TableFormat {
12 Tsv,
15 Schaefer,
18 Tian,
20 Names,
22}
23
24pub fn read_table(path: &Path, format: TableFormat) -> Result<ChannelTable, String> {
25 let text = std::fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
26 let lines = text.lines().map(str::trim).filter(|l| !l.is_empty());
27 let channels: Vec<ChannelInfo> = match format {
28 TableFormat::Tsv => {
29 return parse_tsv(&text)
30 .and_then(|c| ChannelTable::new(c).map_err(|e| e.to_string()))
31 .map_err(|e| format!("{}: {e}", path.display()));
32 }
33 TableFormat::Schaefer => lines
34 .filter_map(|l| l.split_whitespace().find_map(schaefer_channel))
35 .collect(),
36 TableFormat::Tian => lines.map(tian_channel).collect(),
37 TableFormat::Names => lines.map(ChannelInfo::new).collect(),
38 };
39 if channels.is_empty() {
40 return Err(format!("{}: no channel labels found", path.display()));
41 }
42 ChannelTable::new(channels).map_err(|e| format!("{}: {e}", path.display()))
43}
44
45pub fn schaefer_channel(label: &str) -> Option<ChannelInfo> {
48 let mut parts = label.split('_');
49 let prefix = parts.next()?;
50 let networks = prefix
51 .strip_suffix("Networks")
52 .or_else(|| prefix.strip_suffix("networks"))?;
53 if networks.is_empty() || !networks.bytes().all(|b| b.is_ascii_digit()) {
54 return None;
55 }
56 let hemisphere = Hemisphere::parse(parts.next()?)?;
57 let network = parts.next()?;
58 let mut rest: Vec<&str> = parts.collect();
59 if rest
60 .last()
61 .is_some_and(|p| p.bytes().all(|b| b.is_ascii_digit()))
62 {
63 rest.pop();
64 }
65 let channel = ChannelInfo::new(label)
66 .with_hemisphere(hemisphere)
67 .with_tag(format!("yeo{networks}"), network);
68 Some(if rest.is_empty() {
69 channel
70 } else {
71 channel.with_region(Region::new(rest.join("_")))
72 })
73}
74
75pub fn tian_channel(label: &str) -> ChannelInfo {
78 match label
79 .rsplit_once('-')
80 .and_then(|(region, side)| Some((region, Hemisphere::parse(side)?)))
81 {
82 Some((region, side)) => ChannelInfo::new(label)
83 .with_region(Region::new(region))
84 .with_hemisphere(side),
85 None => ChannelInfo::new(label).with_region(Region::new(label)),
86 }
87}
88
89fn parse_tsv(text: &str) -> Result<Vec<ChannelInfo>, String> {
90 let mut lines = text.lines().filter(|l| !l.trim().is_empty());
91 let header: Vec<&str> = lines
92 .next()
93 .ok_or("empty file")?
94 .split('\t')
95 .map(str::trim)
96 .collect();
97 let column = |name: &str| header.iter().position(|h| *h == name);
98 let name = column("name").ok_or("no `name` column")?;
99 let (region, hemisphere) = (column("region"), column("hemisphere"));
100 lines
101 .enumerate()
102 .map(|(row, line)| {
103 let cells: Vec<&str> = line.split('\t').map(str::trim).collect();
104 let cell = |i: usize| {
105 cells
106 .get(i)
107 .copied()
108 .filter(|v| !v.is_empty() && *v != "n/a")
109 };
110 let mut channel =
111 ChannelInfo::new(cell(name).ok_or(format!("row {}: empty name", row + 2))?);
112 if let Some(r) = region.and_then(cell) {
113 channel = channel.with_region(Region::new(r));
114 }
115 if let Some(h) = hemisphere.and_then(cell) {
116 let side = Hemisphere::parse(h).ok_or(format!(
117 "row {}: hemisphere {h:?} is not left or right",
118 row + 2
119 ))?;
120 channel = channel.with_hemisphere(side);
121 }
122 for (i, key) in header.iter().enumerate() {
123 let described = [Some(name), region, hemisphere].contains(&Some(i));
124 if !described
125 && !["index", "color"].contains(key)
126 && let Some(value) = cell(i)
127 {
128 channel = channel.with_tag(*key, value);
129 }
130 }
131 Ok(channel)
132 })
133 .collect()
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn schaefer_labels_give_hemisphere_network_and_region() {
142 let c = schaefer_channel("17networks_LH_DefaultB_PFCm_1").unwrap();
143 assert_eq!(
144 (
145 c.hemisphere(),
146 c.tag("yeo17"),
147 c.region().map(Region::as_str)
148 ),
149 (Some(Hemisphere::Left), Some("DefaultB"), Some("PFCm"))
150 );
151 let striate = schaefer_channel("17networks_RH_VisCent_Striate").unwrap();
152 assert_eq!(
153 striate.region().map(Region::as_str),
154 Some("Striate"),
155 "no trailing index"
156 );
157 let sommot = schaefer_channel("17networks_LH_SomMotB_7").unwrap();
158 assert_eq!(
159 (sommot.region(), sommot.tag("yeo17")),
160 (None, Some("SomMotB")),
161 "no region, only a network"
162 );
163 let seven = schaefer_channel("7Networks_RH_Default_pCunPCC_3").unwrap();
164 assert_eq!(
165 (seven.tag("yeo7"), seven.hemisphere()),
166 (Some("Default"), Some(Hemisphere::Right))
167 );
168 let two_part = schaefer_channel("17Networks_LH_ContA_PFCl_post_2").unwrap();
169 assert_eq!(two_part.region().map(Region::as_str), Some("PFCl_post"));
170 for not_a_label in [
171 "aHIP-rh",
172 "Networks_LH_X_1",
173 "17Networks_XH_X_1",
174 "17Networks_LH",
175 "1 12 48 255 255",
176 ] {
177 assert!(schaefer_channel(not_a_label).is_none(), "{not_a_label}");
178 }
179 }
180
181 #[test]
182 fn tian_labels_give_region_and_hemisphere() {
183 let c = tian_channel("NAc-shell-rh");
184 assert_eq!(
185 (c.region().map(Region::as_str), c.hemisphere()),
186 (Some("NAc-shell"), Some(Hemisphere::Right))
187 );
188 let c = tian_channel("lAMY-lh");
189 assert_eq!(
190 (c.region().map(Region::as_str), c.hemisphere()),
191 (Some("lAMY"), Some(Hemisphere::Left))
192 );
193 let c = tian_channel("brainstem");
194 assert_eq!(
195 (c.region().map(Region::as_str), c.hemisphere()),
196 (Some("brainstem"), None)
197 );
198 }
199
200 #[test]
201 fn tsv_tables_read_columns_and_tags() {
202 let text = "index\tname\tregion\themisphere\tcolor\tlobe\n1\tFp1\tfrontopolar\tL\t#fff\tfrontal\n2\tCz\tcentral\tn/a\t#000\t\n";
203 let channels = parse_tsv(text).unwrap();
204 assert_eq!(channels[0].tag("lobe"), Some("frontal"));
205 assert_eq!(channels[0].tag("color"), None);
206 assert_eq!(channels[0].hemisphere(), Some(Hemisphere::Left));
207 assert_eq!(
208 (channels[1].hemisphere(), channels[1].tag("lobe")),
209 (None, None)
210 );
211 assert!(parse_tsv("label\n").unwrap_err().contains("`name`"));
212 assert!(
213 parse_tsv("name\themisphere\nx\tup\n")
214 .unwrap_err()
215 .contains("row 2")
216 );
217 assert!(
218 parse_tsv("name\tregion\n\tPFC\n")
219 .unwrap_err()
220 .contains("empty name")
221 );
222 assert!(parse_tsv("").is_err());
223 }
224}