1use bids_core::error::{BidsError, Result};
28use std::path::Path;
29
30pub type Bvals = Vec<f64>;
32
33pub type Bvecs = Vec<[f64; 3]>;
35
36pub fn read_bvals(path: &Path) -> Result<Bvals> {
42 let contents = std::fs::read_to_string(path)?;
43 let values: std::result::Result<Vec<f64>, _> = contents
44 .split_whitespace()
45 .map(|s| s.parse::<f64>())
46 .collect();
47 values.map_err(|e| BidsError::DataFormat(format!("Invalid bval file {}: {e}", path.display())))
48}
49
50pub fn read_bvecs(path: &Path) -> Result<Bvecs> {
57 let contents = std::fs::read_to_string(path)?;
58 let rows: Vec<Vec<f64>> = contents
59 .lines()
60 .filter(|l| !l.trim().is_empty())
61 .map(|line| {
62 line.split_whitespace()
63 .map(|s| s.parse::<f64>())
64 .collect::<std::result::Result<Vec<f64>, _>>()
65 })
66 .collect::<std::result::Result<Vec<_>, _>>()
67 .map_err(|e| BidsError::DataFormat(format!("Invalid bvec file {}: {e}", path.display())))?;
68
69 if rows.len() != 3 {
70 return Err(BidsError::DataFormat(format!(
71 "bvec file {} has {} rows, expected 3",
72 path.display(),
73 rows.len()
74 )));
75 }
76
77 let n = rows[0].len();
78 if rows[1].len() != n || rows[2].len() != n {
79 return Err(BidsError::DataFormat(format!(
80 "bvec file {} has inconsistent row lengths ({}, {}, {})",
81 path.display(),
82 rows[0].len(),
83 rows[1].len(),
84 rows[2].len()
85 )));
86 }
87
88 let bvecs: Vec<[f64; 3]> = (0..n)
89 .map(|i| [rows[0][i], rows[1][i], rows[2][i]])
90 .collect();
91 Ok(bvecs)
92}
93
94#[derive(Debug, Clone)]
99pub struct GradientTable {
100 pub bvals: Bvals,
102 pub bvecs: Bvecs,
104}
105
106impl GradientTable {
107 pub fn new(bvals: Bvals, bvecs: Bvecs) -> Result<Self> {
113 if bvals.len() != bvecs.len() {
114 return Err(BidsError::DataFormat(format!(
115 "bval count ({}) != bvec count ({})",
116 bvals.len(),
117 bvecs.len()
118 )));
119 }
120 Ok(Self { bvals, bvecs })
121 }
122
123 pub fn from_files(bval_path: &Path, bvec_path: &Path) -> Result<Self> {
130 let bvals = read_bvals(bval_path)?;
131 let bvecs = read_bvecs(bvec_path)?;
132 Self::new(bvals, bvecs)
133 }
134
135 #[must_use]
137 pub fn n_volumes(&self) -> usize {
138 self.bvals.len()
139 }
140
141 #[must_use]
143 pub fn n_diffusion_volumes(&self) -> usize {
144 self.bvals.iter().filter(|&&b| b > 0.0).count()
145 }
146
147 #[must_use]
149 pub fn n_b0_volumes(&self) -> usize {
150 self.bvals.iter().filter(|&&b| b == 0.0).count()
151 }
152
153 #[must_use]
155 pub fn unique_bvals(&self) -> Vec<f64> {
156 let mut unique: Vec<f64> = Vec::new();
157 for &b in &self.bvals {
158 let rounded = (b * 10.0).round() / 10.0;
159 if !unique.iter().any(|&u| (u - rounded).abs() < 1.0) {
160 unique.push(rounded);
161 }
162 }
163 unique.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
164 unique
165 }
166
167 pub fn iter(&self) -> impl Iterator<Item = (f64, [f64; 3])> + '_ {
169 self.bvals.iter().copied().zip(self.bvecs.iter().copied())
170 }
171
172 #[must_use]
174 pub fn b0_indices(&self) -> Vec<usize> {
175 self.bvals
176 .iter()
177 .enumerate()
178 .filter(|(_, b)| **b == 0.0)
179 .map(|(i, _)| i)
180 .collect()
181 }
182
183 #[must_use]
186 pub fn is_normalized(&self) -> bool {
187 self.iter().all(|(b, [x, y, z])| {
188 if b == 0.0 {
189 true
190 } else {
191 let norm = (x * x + y * y + z * z).sqrt();
192 (norm - 1.0).abs() < 0.1
193 }
194 })
195 }
196}
197
198impl std::fmt::Display for GradientTable {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 let unique = self.unique_bvals();
201 write!(
202 f,
203 "GradientTable({} volumes: {} b=0, {} diffusion, b-values: {:?})",
204 self.n_volumes(),
205 self.n_b0_volumes(),
206 self.n_diffusion_volumes(),
207 unique
208 )
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use std::io::Write;
216
217 #[test]
218 fn test_read_bvals() {
219 let dir = std::env::temp_dir().join("bids_gradient_test");
220 std::fs::create_dir_all(&dir).unwrap();
221 let path = dir.join("test.bval");
222 std::fs::write(&path, "0 1000 1000 2000 0\n").unwrap();
223
224 let bvals = read_bvals(&path).unwrap();
225 assert_eq!(bvals, vec![0.0, 1000.0, 1000.0, 2000.0, 0.0]);
226 std::fs::remove_dir_all(&dir).unwrap();
227 }
228
229 #[test]
230 fn test_read_bvecs() {
231 let dir = std::env::temp_dir().join("bids_bvec_test");
232 std::fs::create_dir_all(&dir).unwrap();
233 let path = dir.join("test.bvec");
234 let mut f = std::fs::File::create(&path).unwrap();
235 writeln!(f, "1.0 0.0 0.5").unwrap();
236 writeln!(f, "0.0 1.0 0.5").unwrap();
237 writeln!(f, "0.0 0.0 0.707").unwrap();
238
239 let bvecs = read_bvecs(&path).unwrap();
240 assert_eq!(bvecs.len(), 3);
241 assert_eq!(bvecs[0], [1.0, 0.0, 0.0]);
242 assert_eq!(bvecs[1], [0.0, 1.0, 0.0]);
243 std::fs::remove_dir_all(&dir).unwrap();
244 }
245
246 #[test]
247 fn test_gradient_table() {
248 let bvals = vec![0.0, 1000.0, 1000.0, 2000.0, 0.0];
249 let bvecs = vec![
250 [0.0, 0.0, 0.0],
251 [1.0, 0.0, 0.0],
252 [0.0, 1.0, 0.0],
253 [0.0, 0.0, 1.0],
254 [0.0, 0.0, 0.0],
255 ];
256 let table = GradientTable::new(bvals, bvecs).unwrap();
257
258 assert_eq!(table.n_volumes(), 5);
259 assert_eq!(table.n_b0_volumes(), 2);
260 assert_eq!(table.n_diffusion_volumes(), 3);
261 assert_eq!(table.b0_indices(), vec![0, 4]);
262
263 let unique = table.unique_bvals();
264 assert_eq!(unique.len(), 3); }
266
267 #[test]
268 fn test_gradient_table_mismatch() {
269 let bvals = vec![0.0, 1000.0];
270 let bvecs = vec![[0.0, 0.0, 0.0]]; assert!(GradientTable::new(bvals, bvecs).is_err());
272 }
273
274 #[test]
275 fn test_display() {
276 let bvals = vec![0.0, 1000.0, 1000.0];
277 let bvecs = vec![[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
278 let table = GradientTable::new(bvals, bvecs).unwrap();
279 let s = table.to_string();
280 assert!(s.contains("3 volumes"));
281 assert!(s.contains("1 b=0"));
282 }
283}