Skip to main content

bids_perf/
lib.rs

1#![deny(unsafe_code)]
2//! Perfusion imaging (ASL) support for BIDS datasets.
3//!
4//! Provides access to Arterial Spin Labeling data, M0 scans, ASL context
5//! files, and perfusion-specific metadata (labeling type, post-labeling delay,
6//! background suppression, etc.).
7//!
8//! See: <https://bids-specification.readthedocs.io/en/stable/modality-specific-files/magnetic-resonance-imaging-data.html#arterial-spin-labeling-perfusion-data>
9
10use bids_core::error::Result;
11use bids_core::file::BidsFile;
12use bids_core::metadata::BidsMetadata;
13use bids_layout::BidsLayout;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "PascalCase")]
18pub struct PerfMetadata {
19    #[serde(default)]
20    pub arterial_spin_labeling_type: Option<String>,
21    #[serde(default)]
22    pub post_labeling_delay: Option<serde_json::Value>,
23    #[serde(default)]
24    pub background_suppression: Option<bool>,
25    #[serde(default)]
26    pub m0_type: Option<String>,
27    #[serde(default)]
28    pub repetition_time_preparation: Option<f64>,
29    #[serde(default)]
30    pub echo_time: Option<f64>,
31    #[serde(default)]
32    pub flip_angle: Option<f64>,
33    #[serde(default)]
34    pub labeling_duration: Option<f64>,
35    #[serde(default)]
36    pub total_acquired_pairs: Option<u32>,
37    #[serde(default)]
38    pub vascular_crushing: Option<bool>,
39    #[serde(default)]
40    pub manufacturer: Option<String>,
41}
42impl PerfMetadata {
43    pub fn from_metadata(md: &BidsMetadata) -> Option<Self> {
44        md.deserialize_as()
45    }
46}
47
48pub struct PerfLayout<'a> {
49    layout: &'a BidsLayout,
50}
51impl<'a> PerfLayout<'a> {
52    pub fn new(layout: &'a BidsLayout) -> Self {
53        Self { layout }
54    }
55    pub fn get_asl_files(&self) -> Result<Vec<BidsFile>> {
56        self.layout.get().suffix("asl").collect()
57    }
58    pub fn get_asl_files_for_subject(&self, s: &str) -> Result<Vec<BidsFile>> {
59        self.layout.get().suffix("asl").subject(s).collect()
60    }
61    pub fn get_m0scan_files(&self) -> Result<Vec<BidsFile>> {
62        self.layout.get().suffix("m0scan").collect()
63    }
64    pub fn get_aslcontext(&self, f: &BidsFile) -> Result<Option<Vec<String>>> {
65        let p = f.companion("aslcontext", "tsv");
66        if !p.exists() {
67            return Ok(None);
68        }
69        let rows = bids_io::tsv::read_tsv(&p)?;
70        Ok(Some(
71            rows.iter()
72                .filter_map(|r| r.get("volume_type").cloned())
73                .collect(),
74        ))
75    }
76    pub fn get_metadata(&self, f: &BidsFile) -> Result<Option<PerfMetadata>> {
77        Ok(self.layout.get_metadata(&f.path)?.deserialize_as())
78    }
79
80    /// Load the ASL/M0 NIfTI image data.
81    pub fn read_image(
82        &self,
83        f: &BidsFile,
84    ) -> std::result::Result<bids_nifti::NiftiImage, bids_nifti::NiftiError> {
85        bids_nifti::NiftiImage::from_file(&f.path)
86    }
87    /// Read only the NIfTI header (fast, no data loading).
88    pub fn read_header(
89        &self,
90        f: &BidsFile,
91    ) -> std::result::Result<bids_nifti::NiftiHeader, bids_nifti::NiftiError> {
92        bids_nifti::NiftiHeader::from_file(&f.path)
93    }
94
95    pub fn get_perf_subjects(&self) -> Result<Vec<String>> {
96        self.layout.get().suffix("asl").return_unique("subject")
97    }
98    pub fn summary(&self) -> Result<PerfSummary> {
99        let files = self.get_asl_files()?;
100        let subjects = self.get_perf_subjects()?;
101        let md = files
102            .first()
103            .and_then(|f| self.get_metadata(f).ok().flatten());
104        let asl_type = md.and_then(|m| m.arterial_spin_labeling_type);
105        Ok(PerfSummary {
106            n_subjects: subjects.len(),
107            n_scans: files.len(),
108            subjects,
109            asl_type,
110        })
111    }
112}
113
114#[derive(Debug)]
115pub struct PerfSummary {
116    pub n_subjects: usize,
117    pub n_scans: usize,
118    pub subjects: Vec<String>,
119    pub asl_type: Option<String>,
120}
121impl std::fmt::Display for PerfSummary {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        writeln!(
124            f,
125            "Perfusion Summary: {} subjects, {} scans",
126            self.n_subjects, self.n_scans
127        )?;
128        if let Some(ref t) = self.asl_type {
129            writeln!(f, "  ASL Type: {t}")?;
130        }
131        Ok(())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    #[test]
139    fn test_perf_metadata() {
140        let json = r#"{"ArterialSpinLabelingType":"PCASL","PostLabelingDelay":1.8,"BackgroundSuppression":true}"#;
141        let md: PerfMetadata = serde_json::from_str(json).unwrap();
142        assert_eq!(md.arterial_spin_labeling_type.as_deref(), Some("PCASL"));
143        assert_eq!(md.background_suppression, Some(true));
144    }
145}