1#![deny(unsafe_code)]
2#[cfg(feature = "snirf")]
15pub mod data;
16
17use bids_core::error::Result;
18use bids_core::file::BidsFile;
19use bids_core::metadata::BidsMetadata;
20use bids_io::tsv::read_tsv;
21use bids_layout::BidsLayout;
22use serde::{Deserialize, Serialize};
23
24#[cfg(feature = "snirf")]
25pub use data::{NirsData, read_snirf};
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(rename_all = "PascalCase")]
29pub struct NirsMetadata {
30 #[serde(default)]
31 pub sampling_frequency: Option<f64>,
32 #[serde(rename = "NIRSChannelCount", default)]
33 pub nirs_channel_count: Option<u32>,
34 #[serde(default)]
35 pub task_name: Option<String>,
36 #[serde(default)]
37 pub manufacturer: Option<String>,
38 #[serde(rename = "NIRSSourceOptodeCount", default)]
39 pub source_optode_count: Option<u32>,
40 #[serde(rename = "NIRSDetectorOptodeCount", default)]
41 pub detector_optode_count: Option<u32>,
42}
43impl NirsMetadata {
44 pub fn from_metadata(md: &BidsMetadata) -> Option<Self> {
45 md.deserialize_as()
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Optode {
51 pub name: String,
52 pub optode_type: String,
53 pub x: Option<f64>,
54 pub y: Option<f64>,
55 pub z: Option<f64>,
56}
57impl Optode {
58 pub fn has_position(&self) -> bool {
59 self.x.is_some() && self.y.is_some() && self.z.is_some()
60 }
61}
62
63pub fn read_optodes_tsv(path: &std::path::Path) -> Result<Vec<Optode>> {
64 let rows = read_tsv(path)?;
65 Ok(rows
66 .iter()
67 .map(|r| Optode {
68 name: r.get("name").cloned().unwrap_or_default(),
69 optode_type: r.get("type").cloned().unwrap_or_default(),
70 x: r.get("x").and_then(|v| v.parse().ok()),
71 y: r.get("y").and_then(|v| v.parse().ok()),
72 z: r.get("z").and_then(|v| v.parse().ok()),
73 })
74 .collect())
75}
76
77pub struct NirsLayout<'a> {
78 layout: &'a BidsLayout,
79}
80impl<'a> NirsLayout<'a> {
81 pub fn new(layout: &'a BidsLayout) -> Self {
82 Self { layout }
83 }
84 pub fn get_nirs_files(&self) -> Result<Vec<BidsFile>> {
85 self.layout.get().suffix("nirs").collect()
86 }
87 pub fn get_nirs_files_for_subject(&self, s: &str) -> Result<Vec<BidsFile>> {
88 self.layout.get().suffix("nirs").subject(s).collect()
89 }
90 pub fn get_nirs_files_for_task(&self, t: &str) -> Result<Vec<BidsFile>> {
91 self.layout.get().suffix("nirs").task(t).collect()
92 }
93 pub fn get_channels(&self, f: &BidsFile) -> Result<Option<Vec<bids_eeg::Channel>>> {
94 let p = f.companion("channels", "tsv");
95 if p.exists() {
96 Ok(Some(bids_eeg::read_channels_tsv(&p)?))
97 } else {
98 Ok(None)
99 }
100 }
101 pub fn get_optodes(&self, f: &BidsFile) -> Result<Option<Vec<Optode>>> {
102 let p = f.companion("optodes", "tsv");
103 if p.exists() {
104 Ok(Some(read_optodes_tsv(&p)?))
105 } else {
106 Ok(None)
107 }
108 }
109 pub fn get_events(&self, f: &BidsFile) -> Result<Option<Vec<bids_eeg::EegEvent>>> {
110 let p = f.companion("events", "tsv");
111 if p.exists() {
112 Ok(Some(bids_eeg::read_events_tsv(&p)?))
113 } else {
114 Ok(None)
115 }
116 }
117 pub fn get_coordsystem(&self, f: &BidsFile) -> Result<Option<bids_eeg::CoordinateSystem>> {
118 let p = f.companion("coordsystem", "json");
119 if p.exists() {
120 Ok(Some(bids_eeg::CoordinateSystem::from_file(&p)?))
121 } else {
122 Ok(None)
123 }
124 }
125 pub fn get_metadata(&self, f: &BidsFile) -> Result<Option<NirsMetadata>> {
126 Ok(self.layout.get_metadata(&f.path)?.deserialize_as())
127 }
128
129 #[cfg(feature = "snirf")]
133 pub fn read_data(&self, f: &BidsFile) -> Result<NirsData> {
134 read_snirf(&f.path)
135 }
136 pub fn get_all_channels_files(&self) -> Result<Vec<BidsFile>> {
137 self.layout
138 .get()
139 .suffix("channels")
140 .datatype("nirs")
141 .extension("tsv")
142 .collect()
143 }
144 pub fn get_all_optodes_files(&self) -> Result<Vec<BidsFile>> {
145 self.layout
146 .get()
147 .suffix("optodes")
148 .datatype("nirs")
149 .extension("tsv")
150 .collect()
151 }
152 pub fn get_nirs_subjects(&self) -> Result<Vec<String>> {
153 self.layout.get().suffix("nirs").return_unique("subject")
154 }
155 pub fn get_nirs_tasks(&self) -> Result<Vec<String>> {
156 self.layout.get().suffix("nirs").return_unique("task")
157 }
158 pub fn summary(&self) -> Result<NirsSummary> {
159 let files = self.get_nirs_files()?;
160 let subjects = self.get_nirs_subjects()?;
161 let tasks = self.get_nirs_tasks()?;
162 let md = files
163 .first()
164 .and_then(|f| self.get_metadata(f).ok().flatten());
165 let sf = md.as_ref().and_then(|m| m.sampling_frequency);
166 let n_ch = md.as_ref().and_then(|m| m.nirs_channel_count);
167 Ok(NirsSummary {
168 n_subjects: subjects.len(),
169 n_recordings: files.len(),
170 subjects,
171 tasks,
172 sampling_frequency: sf,
173 channel_count: n_ch.map(|c| c as usize),
174 })
175 }
176}
177
178#[derive(Debug)]
179pub struct NirsSummary {
180 pub n_subjects: usize,
181 pub n_recordings: usize,
182 pub subjects: Vec<String>,
183 pub tasks: Vec<String>,
184 pub sampling_frequency: Option<f64>,
185 pub channel_count: Option<usize>,
186}
187impl std::fmt::Display for NirsSummary {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 writeln!(
190 f,
191 "NIRS Summary: {} subjects, {} recordings, tasks: {:?}",
192 self.n_subjects, self.n_recordings, self.tasks
193 )?;
194 if let Some(sf) = self.sampling_frequency {
195 writeln!(f, " Sampling: {sf} Hz")?;
196 }
197 if let Some(ch) = self.channel_count {
198 writeln!(f, " Channels: {ch}")?;
199 }
200 Ok(())
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 #[test]
208 fn test_nirs_metadata() {
209 let json = r#"{"SamplingFrequency":10.0,"NIRSChannelCount":36,"NIRSSourceOptodeCount":8,"NIRSDetectorOptodeCount":8}"#;
210 let md: NirsMetadata = serde_json::from_str(json).unwrap();
211 assert_eq!(md.sampling_frequency, Some(10.0));
212 assert_eq!(md.nirs_channel_count, Some(36));
213 assert_eq!(md.source_optode_count, Some(8));
214 }
215}