Skip to main content

bids_motion/
lib.rs

1#![deny(unsafe_code)]
2//! Motion capture support for BIDS datasets.
3//!
4//! Provides access to motion tracking data, channels, and metadata including
5//! tracking system name, sampling frequency, and recording type.
6//!
7//! See: <https://bids-specification.readthedocs.io/en/stable/modality-specific-files/motion.html>
8
9use bids_core::error::Result;
10use bids_core::file::BidsFile;
11use bids_core::metadata::BidsMetadata;
12use bids_layout::BidsLayout;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "PascalCase")]
17pub struct MotionMetadata {
18    #[serde(default)]
19    pub sampling_frequency: Option<f64>,
20    #[serde(default)]
21    pub task_name: Option<String>,
22    #[serde(default)]
23    pub tracking_system_name: Option<String>,
24    #[serde(default)]
25    pub manufacturer: Option<String>,
26    #[serde(default)]
27    pub manufacturers_model_name: Option<String>,
28    #[serde(default)]
29    pub recording_type: Option<String>,
30}
31impl MotionMetadata {
32    pub fn from_metadata(md: &BidsMetadata) -> Option<Self> {
33        md.deserialize_as()
34    }
35}
36
37pub struct MotionLayout<'a> {
38    layout: &'a BidsLayout,
39}
40impl<'a> MotionLayout<'a> {
41    pub fn new(layout: &'a BidsLayout) -> Self {
42        Self { layout }
43    }
44    pub fn get_motion_files(&self) -> Result<Vec<BidsFile>> {
45        self.layout.get().suffix("motion").collect()
46    }
47    pub fn get_motion_files_for_subject(&self, s: &str) -> Result<Vec<BidsFile>> {
48        self.layout.get().suffix("motion").subject(s).collect()
49    }
50    pub fn get_motion_files_for_task(&self, t: &str) -> Result<Vec<BidsFile>> {
51        self.layout.get().suffix("motion").task(t).collect()
52    }
53    pub fn get_channels(&self, f: &BidsFile) -> Result<Option<Vec<bids_eeg::Channel>>> {
54        let p = f.companion("channels", "tsv");
55        if p.exists() {
56            Ok(Some(bids_eeg::read_channels_tsv(&p)?))
57        } else {
58            Ok(None)
59        }
60    }
61    pub fn get_metadata(&self, f: &BidsFile) -> Result<Option<MotionMetadata>> {
62        Ok(self.layout.get_metadata(&f.path)?.deserialize_as())
63    }
64    pub fn get_motion_subjects(&self) -> Result<Vec<String>> {
65        self.layout.get().suffix("motion").return_unique("subject")
66    }
67    pub fn get_motion_tasks(&self) -> Result<Vec<String>> {
68        self.layout.get().suffix("motion").return_unique("task")
69    }
70    pub fn summary(&self) -> Result<MotionSummary> {
71        let files = self.get_motion_files()?;
72        let subjects = self.get_motion_subjects()?;
73        let tasks = self.get_motion_tasks()?;
74        let sf = files
75            .first()
76            .and_then(|f| self.get_metadata(f).ok().flatten())
77            .and_then(|m| m.sampling_frequency);
78        let ch = files
79            .first()
80            .and_then(|f| self.get_channels(f).ok().flatten())
81            .map(|c| c.len());
82        Ok(MotionSummary {
83            n_subjects: subjects.len(),
84            n_recordings: files.len(),
85            subjects,
86            tasks,
87            sampling_frequency: sf,
88            channel_count: ch,
89        })
90    }
91}
92
93#[derive(Debug)]
94pub struct MotionSummary {
95    pub n_subjects: usize,
96    pub n_recordings: usize,
97    pub subjects: Vec<String>,
98    pub tasks: Vec<String>,
99    pub sampling_frequency: Option<f64>,
100    pub channel_count: Option<usize>,
101}
102impl std::fmt::Display for MotionSummary {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        writeln!(
105            f,
106            "Motion Summary: {} subjects, {} recordings, tasks: {:?}",
107            self.n_subjects, self.n_recordings, self.tasks
108        )?;
109        if let Some(sf) = self.sampling_frequency {
110            writeln!(f, "  Sampling: {sf} Hz")?;
111        }
112        if let Some(ch) = self.channel_count {
113            writeln!(f, "  Channels: {ch}")?;
114        }
115        Ok(())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    #[test]
123    fn test_motion_metadata() {
124        let json = r#"{"SamplingFrequency":120.0,"TrackingSystemName":"PhaseSpace","RecordingType":"continuous"}"#;
125        let md: MotionMetadata = serde_json::from_str(json).unwrap();
126        assert_eq!(md.sampling_frequency, Some(120.0));
127        assert_eq!(md.tracking_system_name.as_deref(), Some("PhaseSpace"));
128    }
129}