Skip to main content

bids_ieeg/
lib.rs

1#![deny(unsafe_code)]
2//! Intracranial EEG (iEEG) support for BIDS datasets.
3//!
4//! Provides typed access to iEEG-specific files including electrode positions
5//! (with size, hemisphere, group, and type), channels, events, coordinate
6//! systems, and iEEG metadata. Supports both ECoG (electrocorticography) and
7//! SEEG (stereoelectroencephalography) data.
8//!
9//! Signal data reading is supported for all BIDS-iEEG formats (EDF, BDF,
10//! BrainVision) via [`IeegLayout::read_data`], which delegates to the
11//! `bids-eeg` crate's high-performance readers. Types like [`EegData`],
12//! [`ReadOptions`], and [`Annotation`] are re-exported for convenience.
13//!
14//! See: <https://bids-specification.readthedocs.io/en/stable/modality-specific-files/intracranial-electroencephalography.html>
15
16pub mod layout;
17pub mod metadata;
18
19use bids_core::error::Result;
20use bids_io::tsv::read_tsv;
21use serde::{Deserialize, Serialize};
22
23pub use layout::IeegLayout;
24pub use metadata::IeegMetadata;
25
26// Re-export data reading types from bids-eeg (iEEG uses the same file formats)
27pub use bids_eeg::{Annotation, EegData, ReadOptions, read_brainvision, read_edf, read_eeg_data};
28
29/// An iEEG electrode with size, hemisphere, group, and type.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct IeegElectrode {
32    pub name: String,
33    pub x: Option<f64>,
34    pub y: Option<f64>,
35    pub z: Option<f64>,
36    pub size: Option<f64>,
37    pub hemisphere: Option<String>,
38    pub group: Option<String>,
39    pub electrode_type: Option<String>,
40    pub manufacturer: Option<String>,
41}
42
43impl IeegElectrode {
44    pub fn has_position(&self) -> bool {
45        self.x.is_some() && self.y.is_some() && self.z.is_some()
46    }
47    pub fn position(&self) -> Option<(f64, f64, f64)> {
48        match (self.x, self.y, self.z) {
49            (Some(x), Some(y), Some(z)) => Some((x, y, z)),
50            _ => None,
51        }
52    }
53}
54
55pub fn read_ieeg_electrodes(path: &std::path::Path) -> Result<Vec<IeegElectrode>> {
56    let rows = read_tsv(path)?;
57    Ok(rows
58        .iter()
59        .map(|r| {
60            let get = |k: &str| {
61                r.get(k)
62                    .filter(|s| !s.is_empty() && s.as_str() != "n/a")
63                    .cloned()
64            };
65            let getf = |k: &str| r.get(k).and_then(|v| v.parse().ok());
66            IeegElectrode {
67                name: r.get("name").cloned().unwrap_or_default(),
68                x: getf("x"),
69                y: getf("y"),
70                z: getf("z"),
71                size: getf("size"),
72                hemisphere: get("hemisphere"),
73                group: get("group"),
74                electrode_type: get("type"),
75                manufacturer: get("manufacturer"),
76            }
77        })
78        .collect())
79}
80
81/// iEEG coordinate system from _coordsystem.json.
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "PascalCase")]
84pub struct IeegCoordSystem {
85    #[serde(rename = "iEEGCoordinateSystem")]
86    pub coordinate_system: String,
87    #[serde(rename = "iEEGCoordinateUnits")]
88    pub coordinate_units: String,
89    #[serde(rename = "iEEGCoordinateSystemDescription", default)]
90    pub coordinate_system_description: Option<String>,
91    #[serde(rename = "iEEGCoordinateProcessingDescription", default)]
92    pub processing_description: Option<String>,
93    #[serde(rename = "IntendedFor", default)]
94    pub intended_for: Option<serde_json::Value>,
95}
96
97impl IeegCoordSystem {
98    pub fn from_file(path: &std::path::Path) -> Result<Self> {
99        let contents = std::fs::read_to_string(path)?;
100        Ok(serde_json::from_str(&contents)?)
101    }
102}
103
104#[derive(Debug)]
105pub struct IeegSummary {
106    pub n_subjects: usize,
107    pub n_recordings: usize,
108    pub subjects: Vec<String>,
109    pub tasks: Vec<String>,
110    pub sampling_frequency: Option<f64>,
111    pub channel_count: Option<usize>,
112}
113impl std::fmt::Display for IeegSummary {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        writeln!(f, "iEEG Dataset Summary:")?;
116        writeln!(f, "  Subjects: {}", self.n_subjects)?;
117        writeln!(f, "  Recordings: {}", self.n_recordings)?;
118        writeln!(f, "  Tasks: {:?}", self.tasks)?;
119        if let Some(sf) = self.sampling_frequency {
120            writeln!(f, "  Sampling Frequency: {sf} Hz")?;
121        }
122        if let Some(cc) = self.channel_count {
123            writeln!(f, "  Channels: {cc}")?;
124        }
125        Ok(())
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_ieeg_metadata() {
135        let json = r#"{"SamplingFrequency":512,"SEEGChannelCount":124,"EEGChannelCount":2,"ECGChannelCount":2}"#;
136        let md: IeegMetadata = serde_json::from_str(json).unwrap();
137        assert_eq!(md.sampling_frequency, 512.0);
138        assert_eq!(md.seeg_channel_count, Some(124));
139        assert_eq!(md.total_channel_count(), 128);
140    }
141
142    #[test]
143    fn test_ieeg_coordsystem() {
144        let json = r#"{"iEEGCoordinateSystem":"ACPC","iEEGCoordinateUnits":"mm"}"#;
145        let cs: IeegCoordSystem = serde_json::from_str(json).unwrap();
146        assert_eq!(cs.coordinate_system, "ACPC");
147        assert_eq!(cs.coordinate_units, "mm");
148    }
149}