1#![deny(unsafe_code)]
2use 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 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 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}