Skip to main content

bids_eeg/
metadata.rs

1//! Typed EEG metadata from JSON sidecars.
2//!
3//! [`EegMetadata`] provides typed access to all EEG-specific sidecar fields
4//! including sampling frequency, channel counts, reference, placement scheme,
5//! power line frequency, recording duration, and filter descriptions.
6
7use bids_core::metadata::BidsMetadata;
8use serde::{Deserialize, Serialize};
9
10/// EEG-specific metadata from the JSON sidecar.
11///
12/// See: <https://bids-specification.readthedocs.io/en/stable/modality-specific-files/electroencephalography.html>
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "PascalCase")]
15pub struct EegMetadata {
16    /// Name of the task.
17    #[serde(default)]
18    pub task_name: Option<String>,
19    /// Description of the task.
20    #[serde(default)]
21    pub task_description: Option<String>,
22    /// Sampling frequency of the EEG recording in Hz.
23    pub sampling_frequency: f64,
24    /// Number of EEG channels.
25    #[serde(rename = "EEGChannelCount", default)]
26    pub eeg_channel_count: Option<u32>,
27    /// Number of EOG channels.
28    #[serde(rename = "EOGChannelCount", default)]
29    pub eog_channel_count: Option<u32>,
30    /// Number of ECG channels.
31    #[serde(rename = "ECGChannelCount", default)]
32    pub ecg_channel_count: Option<u32>,
33    /// Number of EMG channels.
34    #[serde(rename = "EMGChannelCount", default)]
35    pub emg_channel_count: Option<u32>,
36    /// Number of miscellaneous channels.
37    #[serde(default)]
38    pub misc_channel_count: Option<u32>,
39    /// Number of trigger channels.
40    #[serde(default)]
41    pub trigger_channel_count: Option<u32>,
42    /// EEG placement scheme (e.g., "10-20", "10-10", "10-5").
43    #[serde(rename = "EEGPlacementScheme", default)]
44    pub eeg_placement_scheme: Option<String>,
45    /// EEG reference electrode(s).
46    #[serde(rename = "EEGReference", default)]
47    pub eeg_reference: Option<String>,
48    /// EEG ground electrode.
49    #[serde(rename = "EEGGround", default)]
50    pub eeg_ground: Option<String>,
51    /// Duration of the recording in seconds.
52    #[serde(default)]
53    pub recording_duration: Option<f64>,
54    /// Type of recording: "continuous", "epoched", "discontinuous".
55    #[serde(default)]
56    pub recording_type: Option<String>,
57    /// Power line frequency in Hz (50 or 60).
58    #[serde(default)]
59    pub power_line_frequency: Option<f64>,
60    /// Software filters applied.
61    #[serde(default)]
62    pub software_filters: Option<serde_json::Value>,
63    /// Hardware filters applied.
64    #[serde(default)]
65    pub hardware_filters: Option<serde_json::Value>,
66    /// Manufacturer of the EEG system.
67    #[serde(default)]
68    pub manufacturer: Option<String>,
69    /// Manufacturer's model name.
70    #[serde(default)]
71    pub manufacturers_model_name: Option<String>,
72    /// Name of the cap (if applicable).
73    #[serde(default)]
74    pub cap_manufacturer: Option<String>,
75    /// Cap model.
76    #[serde(default)]
77    pub cap_manufacturers_model_name: Option<String>,
78    /// Institutional department name.
79    #[serde(default)]
80    pub institution_name: Option<String>,
81    /// Institutional department name.
82    #[serde(default)]
83    pub institutional_department_name: Option<String>,
84    /// Address of the institution.
85    #[serde(default)]
86    pub institution_address: Option<String>,
87    /// Subject artifact description.
88    #[serde(default)]
89    pub subject_artifact_description: Option<String>,
90}
91
92impl EegMetadata {
93    /// Try to extract EEG metadata from a generic BidsMetadata.
94    pub fn from_metadata(md: &BidsMetadata) -> Option<Self> {
95        md.deserialize_as()
96    }
97
98    /// Get the total channel count.
99    pub fn total_channel_count(&self) -> u32 {
100        self.eeg_channel_count.unwrap_or(0)
101            + self.eog_channel_count.unwrap_or(0)
102            + self.ecg_channel_count.unwrap_or(0)
103            + self.emg_channel_count.unwrap_or(0)
104            + self.misc_channel_count.unwrap_or(0)
105            + self.trigger_channel_count.unwrap_or(0)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_eeg_metadata_parse() {
115        let json = r#"{
116            "TaskName": "rest",
117            "SamplingFrequency": 256,
118            "EEGChannelCount": 64,
119            "EOGChannelCount": 2,
120            "ECGChannelCount": 1,
121            "EMGChannelCount": 0,
122            "EEGPlacementScheme": "10-20",
123            "EEGReference": "Cz",
124            "RecordingDuration": 300.0,
125            "RecordingType": "continuous",
126            "PowerLineFrequency": 50
127        }"#;
128
129        let md: EegMetadata = serde_json::from_str(json).unwrap();
130        assert_eq!(md.sampling_frequency, 256.0);
131        assert_eq!(md.eeg_channel_count, Some(64));
132        assert_eq!(md.eeg_reference.as_deref(), Some("Cz"));
133        assert_eq!(md.total_channel_count(), 67);
134    }
135}