1use bids_core::error::Result;
8use bids_core::file::BidsFile;
9use bids_io::tsv::read_tsv_gz;
10use bids_layout::BidsLayout;
11
12use crate::channels::{Channel, read_channels_tsv};
13use crate::coordsystem::CoordinateSystem;
14use crate::data::{Annotation, EegData, ReadOptions, read_brainvision_markers, read_eeg_data};
15use crate::electrodes::{Electrode, read_electrodes_tsv};
16use crate::events::{EegEvent, read_events_tsv};
17use crate::metadata::EegMetadata;
18
19pub struct EegLayout<'a> {
21 layout: &'a BidsLayout,
22}
23
24impl<'a> EegLayout<'a> {
25 pub fn new(layout: &'a BidsLayout) -> Self {
26 Self { layout }
27 }
28
29 pub fn get_eeg_files(&self) -> Result<Vec<BidsFile>> {
30 self.layout.get().suffix("eeg").collect()
31 }
32 pub fn get_eeg_files_for_subject(&self, subject: &str) -> Result<Vec<BidsFile>> {
33 self.layout.get().suffix("eeg").subject(subject).collect()
34 }
35 pub fn get_eeg_files_for_task(&self, task: &str) -> Result<Vec<BidsFile>> {
36 self.layout.get().suffix("eeg").task(task).collect()
37 }
38
39 pub fn get_channels(&self, f: &BidsFile) -> Result<Option<Vec<Channel>>> {
40 bids_core::try_read_companion(&f.companion("channels", "tsv"), read_channels_tsv)
41 }
42 pub fn get_electrodes(&self, f: &BidsFile) -> Result<Option<Vec<Electrode>>> {
43 bids_core::try_read_companion(&f.companion("electrodes", "tsv"), read_electrodes_tsv)
44 }
45 pub fn get_events(&self, f: &BidsFile) -> Result<Option<Vec<EegEvent>>> {
46 bids_core::try_read_companion(&f.companion("events", "tsv"), read_events_tsv)
47 }
48 pub fn get_eeg_metadata(&self, f: &BidsFile) -> Result<Option<EegMetadata>> {
49 let md = self.layout.get_metadata(&f.path)?;
50 Ok(EegMetadata::from_metadata(&md))
51 }
52 pub fn get_coordsystem(&self, f: &BidsFile) -> Result<Option<CoordinateSystem>> {
53 let p = f.companion("coordsystem", "json");
54 if p.exists() {
55 return Ok(Some(CoordinateSystem::from_file(&p)?));
56 }
57 let stem = f.filename.split('.').next().unwrap_or("");
59 let base_parts: Vec<&str> = stem
60 .split('_')
61 .filter(|p| p.starts_with("sub-") || p.starts_with("ses-") || p.starts_with("acq-"))
62 .collect();
63 if !base_parts.is_empty() {
64 let relaxed = f
65 .dirname
66 .join(format!("{}_coordsystem.json", base_parts.join("_")));
67 if relaxed.exists() {
68 return Ok(Some(CoordinateSystem::from_file(&relaxed)?));
69 }
70 }
71 Ok(None)
72 }
73
74 pub fn get_physio(&self, f: &BidsFile) -> Result<Option<PhysioData>> {
75 let p = f.companion("physio", "tsv.gz");
76 if !p.exists() {
77 return Ok(None);
78 }
79 let rows = read_tsv_gz(&p)?;
80 let json_path = p.with_extension("").with_extension("json");
81 let md = if json_path.exists() {
82 Some(bids_io::json::read_json_sidecar(&json_path)?)
83 } else {
84 None
85 };
86 let sampling_freq = md
87 .as_ref()
88 .and_then(|m| m.get_f64("SamplingFrequency"))
89 .unwrap_or(1.0);
90 let start_time = md
91 .as_ref()
92 .and_then(|m| m.get_f64("StartTime"))
93 .unwrap_or(0.0);
94 let columns: Vec<String> = md
95 .as_ref()
96 .and_then(|m| m.get_array("Columns"))
97 .map(|arr| {
98 arr.iter()
99 .filter_map(|v| v.as_str().map(String::from))
100 .collect()
101 })
102 .unwrap_or_default();
103 Ok(Some(PhysioData {
104 rows,
105 sampling_frequency: sampling_freq,
106 start_time,
107 columns,
108 }))
109 }
110
111 pub fn read_edf_header(&self, f: &BidsFile) -> Result<Option<EdfHeader>> {
112 if !f.filename.ends_with(".edf") && !f.filename.ends_with(".bdf") {
113 return Ok(None);
114 }
115 EdfHeader::from_file(&f.path).map(Some)
116 }
117
118 pub fn read_data(&self, f: &BidsFile, opts: &ReadOptions) -> Result<EegData> {
141 read_eeg_data(&f.path, opts)
142 }
143
144 pub fn read_data_channels(&self, f: &BidsFile, channels: &[&str]) -> Result<EegData> {
149 let opts = ReadOptions::new().with_channels(
150 channels
151 .iter()
152 .map(std::string::ToString::to_string)
153 .collect(),
154 );
155 self.read_data(f, &opts)
156 }
157
158 pub fn read_data_time_range(&self, f: &BidsFile, start: f64, end: f64) -> Result<EegData> {
163 let opts = ReadOptions::new().with_time_range(start, end);
164 self.read_data(f, &opts)
165 }
166
167 pub fn get_brainvision_markers(
170 &self,
171 f: &BidsFile,
172 sampling_rate: f64,
173 ) -> Result<Vec<Annotation>> {
174 let vmrk_path = f.path.with_extension("vmrk");
175 if vmrk_path.exists() {
176 read_brainvision_markers(&vmrk_path, sampling_rate)
177 } else {
178 Ok(Vec::new())
179 }
180 }
181
182 pub fn get_all_channels_files(&self) -> Result<Vec<BidsFile>> {
183 self.layout
184 .get()
185 .suffix("channels")
186 .datatype("eeg")
187 .extension("tsv")
188 .collect()
189 }
190 pub fn get_all_electrodes_files(&self) -> Result<Vec<BidsFile>> {
191 self.layout
192 .get()
193 .suffix("electrodes")
194 .datatype("eeg")
195 .extension("tsv")
196 .collect()
197 }
198 pub fn get_all_events_files(&self) -> Result<Vec<BidsFile>> {
199 self.layout
200 .get()
201 .suffix("events")
202 .datatype("eeg")
203 .extension("tsv")
204 .collect()
205 }
206 pub fn get_eeg_subjects(&self) -> Result<Vec<String>> {
207 self.layout.get().suffix("eeg").return_unique("subject")
208 }
209 pub fn get_eeg_tasks(&self) -> Result<Vec<String>> {
210 self.layout.get().suffix("eeg").return_unique("task")
211 }
212
213 pub fn summary(&self) -> Result<EegDatasetSummary> {
214 let eeg_files = self.get_eeg_files()?;
215 let subjects = self.get_eeg_subjects()?;
216 let tasks = self.get_eeg_tasks()?;
217 let sampling_frequency = eeg_files
218 .first()
219 .and_then(|f| self.get_eeg_metadata(f).ok().flatten())
220 .map(|m| m.sampling_frequency);
221 let channel_count = eeg_files
222 .first()
223 .and_then(|f| self.get_channels(f).ok().flatten())
224 .map(|c| c.len());
225 Ok(EegDatasetSummary {
226 n_subjects: subjects.len(),
227 n_recordings: eeg_files.len(),
228 subjects,
229 tasks,
230 sampling_frequency,
231 channel_count,
232 })
233 }
234}
235
236#[derive(Debug)]
237pub struct EegDatasetSummary {
238 pub n_subjects: usize,
239 pub n_recordings: usize,
240 pub subjects: Vec<String>,
241 pub tasks: Vec<String>,
242 pub sampling_frequency: Option<f64>,
243 pub channel_count: Option<usize>,
244}
245
246impl std::fmt::Display for EegDatasetSummary {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 writeln!(f, "EEG Dataset Summary:")?;
249 writeln!(f, " Subjects: {}", self.n_subjects)?;
250 writeln!(f, " Recordings: {}", self.n_recordings)?;
251 writeln!(f, " Tasks: {:?}", self.tasks)?;
252 if let Some(sf) = self.sampling_frequency {
253 writeln!(f, " Sampling Frequency: {sf} Hz")?;
254 }
255 if let Some(cc) = self.channel_count {
256 writeln!(f, " Channels: {cc}")?;
257 }
258 Ok(())
259 }
260}
261
262#[derive(Debug)]
263pub struct PhysioData {
264 pub rows: Vec<std::collections::HashMap<String, String>>,
265 pub sampling_frequency: f64,
266 pub start_time: f64,
267 pub columns: Vec<String>,
268}
269
270#[derive(Debug)]
271pub struct EdfHeader {
272 pub version: String,
273 pub patient_id: String,
274 pub recording_id: String,
275 pub n_channels: usize,
276 pub n_records: i64,
277 pub record_duration: f64,
278 pub sampling_rates: Vec<f64>,
279 pub channel_labels: Vec<String>,
280}
281
282impl EdfHeader {
283 pub fn from_file(path: &std::path::Path) -> Result<Self> {
284 use std::io::Read;
285 let mut file = std::fs::File::open(path)?;
286 let mut header = vec![0u8; 256];
287 file.read_exact(&mut header)?;
288 let version = String::from_utf8_lossy(&header[0..8]).trim().to_string();
289 let patient_id = String::from_utf8_lossy(&header[8..88]).trim().to_string();
290 let recording_id = String::from_utf8_lossy(&header[88..168]).trim().to_string();
291 let n_channels: usize = String::from_utf8_lossy(&header[252..256])
292 .trim()
293 .parse()
294 .unwrap_or(0);
295 let n_records: i64 = String::from_utf8_lossy(&header[236..244])
296 .trim()
297 .parse()
298 .unwrap_or(-1);
299 let record_duration: f64 = String::from_utf8_lossy(&header[244..252])
300 .trim()
301 .parse()
302 .unwrap_or(1.0);
303 let ext_size = n_channels * 256;
304 let mut ext_header = vec![0u8; ext_size];
305 file.read_exact(&mut ext_header)?;
306 let mut channel_labels = Vec::new();
307 for i in 0..n_channels {
308 let offset = i * 16;
309 if offset + 16 <= ext_header.len() {
310 channel_labels.push(
311 String::from_utf8_lossy(&ext_header[offset..offset + 16])
312 .trim()
313 .to_string(),
314 );
315 }
316 }
317 let mut sampling_rates = Vec::new();
318 let samples_offset = n_channels * 216;
319 for i in 0..n_channels {
320 let offset = samples_offset + i * 8;
321 if offset + 8 <= ext_header.len() {
322 let samples: f64 = String::from_utf8_lossy(&ext_header[offset..offset + 8])
323 .trim()
324 .parse()
325 .unwrap_or(0.0);
326 if record_duration > 0.0 {
327 sampling_rates.push(samples / record_duration);
328 }
329 }
330 }
331 Ok(Self {
332 version,
333 patient_id,
334 recording_id,
335 n_channels,
336 n_records,
337 record_duration,
338 sampling_rates,
339 channel_labels,
340 })
341 }
342 pub fn duration(&self) -> f64 {
343 if self.n_records > 0 {
344 self.n_records as f64 * self.record_duration
345 } else {
346 0.0
347 }
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn test_companion() {
357 let bf = BidsFile::new("/data/sub-01/eeg/sub-01_task-rest_eeg.edf");
358 assert_eq!(
359 bf.companion("channels", "tsv").to_string_lossy(),
360 "/data/sub-01/eeg/sub-01_task-rest_channels.tsv"
361 );
362 assert_eq!(
363 bf.companion("events", "tsv").to_string_lossy(),
364 "/data/sub-01/eeg/sub-01_task-rest_events.tsv"
365 );
366 }
367}