Skip to main content

bids_meg/
lib.rs

1#![deny(unsafe_code)]
2//! Magnetoencephalography (MEG) support for BIDS datasets.
3//!
4//! Provides typed access to MEG-specific files, channels, events, headshape data,
5//! coordinate systems, and MEG metadata (sampling frequency, channel counts,
6//! dewar position, digitized landmarks).
7//!
8//! ## Feature flags
9//!
10//! - **`fiff`** — Enable FIFF data reading for Elekta/Neuromag MEG files (`.fif`).
11//!   Adds the `data` module with `MegData` and `read_fiff`.
12//!
13//! See: <https://bids-specification.readthedocs.io/en/stable/modality-specific-files/magnetoencephalography.html>
14
15pub mod ctf;
16#[cfg(feature = "fiff")]
17pub mod data;
18pub mod headshape;
19pub mod metadata;
20
21use bids_core::error::Result;
22use bids_core::file::BidsFile;
23use bids_layout::BidsLayout;
24pub use ctf::{CtfHeader, read_ctf_header};
25#[cfg(feature = "fiff")]
26pub use data::{MegData, read_fiff};
27pub use headshape::{DigPoint, PointKind, read_headshape_pos};
28pub use metadata::MegMetadata;
29
30pub struct MegLayout<'a> {
31    layout: &'a BidsLayout,
32}
33impl<'a> MegLayout<'a> {
34    pub fn new(layout: &'a BidsLayout) -> Self {
35        Self { layout }
36    }
37    pub fn get_meg_files(&self) -> Result<Vec<BidsFile>> {
38        self.layout.get().suffix("meg").collect()
39    }
40    pub fn get_meg_files_for_subject(&self, s: &str) -> Result<Vec<BidsFile>> {
41        self.layout.get().suffix("meg").subject(s).collect()
42    }
43    pub fn get_meg_files_for_task(&self, t: &str) -> Result<Vec<BidsFile>> {
44        self.layout.get().suffix("meg").task(t).collect()
45    }
46    pub fn get_channels(&self, f: &BidsFile) -> Result<Option<Vec<bids_eeg::Channel>>> {
47        bids_core::try_read_companion(&f.companion("channels", "tsv"), bids_eeg::read_channels_tsv)
48    }
49    pub fn get_events(&self, f: &BidsFile) -> Result<Option<Vec<bids_eeg::EegEvent>>> {
50        bids_core::try_read_companion(&f.companion("events", "tsv"), bids_eeg::read_events_tsv)
51    }
52    /// Get the headshape file path for a MEG recording.
53    pub fn get_headshape_path(&self, f: &BidsFile) -> Option<std::path::PathBuf> {
54        let p = f.companion("headshape", "pos");
55        if p.exists() { Some(p) } else { None }
56    }
57
58    /// Read and parse headshape digitization points from the companion `.pos` file.
59    pub fn get_headshape(&self, f: &BidsFile) -> Result<Option<Vec<DigPoint>>> {
60        let p = f.companion("headshape", "pos");
61        if p.exists() {
62            Ok(Some(read_headshape_pos(&p)?))
63        } else {
64            Ok(None)
65        }
66    }
67    pub fn get_coordsystem(&self, f: &BidsFile) -> Result<Option<bids_eeg::CoordinateSystem>> {
68        let p = f.companion("coordsystem", "json");
69        if p.exists() {
70            Ok(Some(bids_eeg::CoordinateSystem::from_file(&p)?))
71        } else {
72            Ok(None)
73        }
74    }
75    pub fn get_metadata(&self, f: &BidsFile) -> Result<Option<MegMetadata>> {
76        Ok(self.layout.get_metadata(&f.path)?.deserialize_as())
77    }
78
79    /// Read raw MEG signal data from a FIFF file.
80    ///
81    /// Requires the `fiff` feature flag.
82    #[cfg(feature = "fiff")]
83    pub fn read_data(&self, f: &BidsFile) -> Result<MegData> {
84        read_fiff(&f.path)
85    }
86    pub fn get_all_channels_files(&self) -> Result<Vec<BidsFile>> {
87        self.layout
88            .get()
89            .suffix("channels")
90            .datatype("meg")
91            .extension("tsv")
92            .collect()
93    }
94    pub fn get_all_events_files(&self) -> Result<Vec<BidsFile>> {
95        self.layout
96            .get()
97            .suffix("events")
98            .datatype("meg")
99            .extension("tsv")
100            .collect()
101    }
102    pub fn get_meg_subjects(&self) -> Result<Vec<String>> {
103        self.layout.get().suffix("meg").return_unique("subject")
104    }
105    pub fn get_meg_tasks(&self) -> Result<Vec<String>> {
106        self.layout.get().suffix("meg").return_unique("task")
107    }
108    pub fn summary(&self) -> Result<MegSummary> {
109        let files = self.get_meg_files()?;
110        let subjects = self.get_meg_subjects()?;
111        let tasks = self.get_meg_tasks()?;
112        let sf = files
113            .first()
114            .and_then(|f| self.get_metadata(f).ok().flatten())
115            .map(|m| m.sampling_frequency);
116        let ch = files
117            .first()
118            .and_then(|f| self.get_channels(f).ok().flatten())
119            .map(|c| c.len());
120        Ok(MegSummary {
121            n_subjects: subjects.len(),
122            n_recordings: files.len(),
123            subjects,
124            tasks,
125            sampling_frequency: sf,
126            channel_count: ch,
127        })
128    }
129}
130/// Summary statistics for MEG data in a BIDS dataset.
131#[derive(Debug, Clone)]
132pub struct MegSummary {
133    pub n_subjects: usize,
134    pub n_recordings: usize,
135    pub subjects: Vec<String>,
136    pub tasks: Vec<String>,
137    pub sampling_frequency: Option<f64>,
138    pub channel_count: Option<usize>,
139}
140impl std::fmt::Display for MegSummary {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        writeln!(
143            f,
144            "MEG Summary: {} subjects, {} recordings, tasks: {:?}",
145            self.n_subjects, self.n_recordings, self.tasks
146        )?;
147        if let Some(sf) = self.sampling_frequency {
148            writeln!(f, "  Sampling: {sf} Hz")?;
149        }
150        if let Some(ch) = self.channel_count {
151            writeln!(f, "  Channels: {ch}")?;
152        }
153        Ok(())
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160    #[test]
161    fn test_meg_metadata() {
162        let json = r#"{"SamplingFrequency":2400,"MEGChannelCount":274,"MEGREFChannelCount":26,"DewarPosition":"Upright"}"#;
163        let md: MegMetadata = serde_json::from_str(json).unwrap();
164        assert_eq!(md.sampling_frequency, 2400.0);
165        assert_eq!(md.meg_channel_count, Some(274));
166        assert_eq!(md.dewar_position.as_deref(), Some("Upright"));
167    }
168}