1pub fn use_arlo(echo_times: &[f64], tolerance: f64) -> bool {
20 if echo_times.len() < 3 {
21 return false;
22 }
23
24 let mut te_sorted: Vec<f64> = echo_times.to_vec();
25 te_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
26
27 let diffs: Vec<f64> = te_sorted.windows(2).map(|w| w[1] - w[0]).collect();
28 let mean_diff: f64 = diffs.iter().sum::<f64>() / diffs.len() as f64;
29 let max_dev = diffs.iter().map(|&d| (d - mean_diff).abs()).fold(0.0_f64, f64::max);
30
31 max_dev <= tolerance
32}
33
34pub fn r2star_arlo(
54 magnitude: &[f64],
55 mask: &[u8],
56 echo_times: &[f64],
57 grid: &crate::Grid,
58) -> (Vec<f64>, Vec<f64>) {
59 let n_echoes = echo_times.len();
60 assert!(n_echoes >= 3, "ARLO requires at least 3 echo times");
61 let n_voxels = grid.n_total();
62 assert_eq!(magnitude.len(), n_voxels * n_echoes,
63 "magnitude length must be n_voxels * n_echoes");
64 assert_eq!(mask.len(), n_voxels, "mask length must be n_voxels");
65
66 let mut te_sorted: Vec<f64> = echo_times.to_vec();
68 let sort_indices: Vec<usize> = {
69 let mut idx: Vec<usize> = (0..n_echoes).collect();
70 idx.sort_by(|&a, &b| echo_times[a].partial_cmp(&echo_times[b]).unwrap());
71 idx
72 };
73 te_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
74 let delta_te = te_sorted[1] - te_sorted[0];
75
76 let mut r2star_map = vec![0.0_f64; n_voxels];
77 let mut s0_map = vec![0.0_f64; n_voxels];
78
79 for v in 0..n_voxels {
80 if mask[v] == 0 {
81 continue;
82 }
83
84 let signal: Vec<f64> = sort_indices.iter()
86 .map(|&ei| magnitude[v * n_echoes + ei])
87 .collect();
88
89 let sig_max = signal.iter().cloned().fold(0.0_f64, f64::max);
91 if sig_max < 1e-10 {
92 continue;
93 }
94
95 let mut num = 0.0_f64;
102 let mut den = 0.0_f64;
103 for i in 0..(n_echoes - 2) {
104 let integ = (delta_te / 3.0) * (signal[i] + 4.0 * signal[i + 1] + signal[i + 2]);
105 let diff = signal[i] - signal[i + 2];
106 num += diff * integ;
107 den += integ * integ;
108 }
109
110 if den > 1e-30 {
111 let r2star_val = num / den;
112 if (0.0..=500.0).contains(&r2star_val) {
113 r2star_map[v] = r2star_val;
114 s0_map[v] = signal[0] * (r2star_val * te_sorted[0]).exp();
115 continue;
116 }
117 }
118
119 let (r2, s0) = log_linear_fit(&signal, &te_sorted);
121 r2star_map[v] = r2.max(0.0);
122 s0_map[v] = s0.max(0.0);
123 }
124
125 (r2star_map, s0_map)
126}
127
128pub fn t2star_from_r2star(r2star: &[f64]) -> Vec<f64> {
132 r2star.iter().map(|&r| if r > 0.0 { 1.0 / r } else { 0.0 }).collect()
133}
134
135fn log_linear_fit(signal: &[f64], echo_times: &[f64]) -> (f64, f64) {
138 let n = signal.len();
139 let mut sum_t = 0.0;
140 let mut sum_tt = 0.0;
141 let mut sum_y = 0.0;
142 let mut sum_ty = 0.0;
143 let mut count = 0;
144
145 for i in 0..n {
146 if signal[i] > 1e-10 {
147 let y = signal[i].ln();
148 let t = echo_times[i];
149 sum_t += t;
150 sum_tt += t * t;
151 sum_y += y;
152 sum_ty += t * y;
153 count += 1;
154 }
155 }
156
157 if count < 2 {
158 return (0.0, 0.0);
159 }
160
161 let n_f = count as f64;
162 let denom = n_f * sum_tt - sum_t * sum_t;
163 if denom.abs() < 1e-15 {
164 return (0.0, 0.0);
165 }
166
167 let slope = (n_f * sum_ty - sum_t * sum_y) / denom;
168 let intercept = (sum_y - slope * sum_t) / n_f;
169
170 let r2star = -slope; let s0 = intercept.exp();
172
173 (r2star, s0)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::Grid;
180
181 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
182 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
183 }
184
185 #[test]
186 fn test_use_arlo_equispaced() {
187 let te = vec![0.005, 0.010, 0.015, 0.020];
188 assert!(use_arlo(&te, 0.1e-3));
189 }
190
191 #[test]
192 fn test_use_arlo_not_equispaced() {
193 let te = vec![0.005, 0.008, 0.015];
194 assert!(!use_arlo(&te, 0.1e-3));
195 }
196
197 #[test]
198 fn test_use_arlo_too_few() {
199 let te = vec![0.005, 0.010];
200 assert!(!use_arlo(&te, 0.1e-3));
201 }
202
203 #[test]
204 fn test_r2star_arlo_synthetic() {
205 let r2star_true: f64 = 30.0; let s0_true: f64 = 1000.0;
208 let te = vec![0.005, 0.010, 0.015, 0.020, 0.025];
209 let n_echoes = te.len();
210
211 let mask = vec![1u8];
213 let magnitude: Vec<f64> = te.iter()
214 .map(|&t| s0_true * (-r2star_true * t).exp())
215 .collect();
216
217 let (r2star_map, s0_map) = r2star_arlo(&magnitude, &mask, &te, &grid(1, 1, 1));
218
219 let r2star_err = (r2star_map[0] - r2star_true).abs() / r2star_true;
220 let s0_err = (s0_map[0] - s0_true).abs() / s0_true;
221
222 assert!(r2star_err < 0.01, "R2* error {:.4}% should be < 1%", r2star_err * 100.0);
223 assert!(s0_err < 0.01, "S0 error {:.4}% should be < 1%", s0_err * 100.0);
224 }
225
226 #[test]
227 fn test_t2star_from_r2star() {
228 let r2star = vec![50.0, 0.0, 100.0, 25.0];
229 let t2star = t2star_from_r2star(&r2star);
230 assert!((t2star[0] - 0.02).abs() < 1e-10);
231 assert_eq!(t2star[1], 0.0);
232 assert!((t2star[2] - 0.01).abs() < 1e-10);
233 assert!((t2star[3] - 0.04).abs() < 1e-10);
234 }
235
236 #[test]
237 fn test_r2star_arlo_masked_voxel() {
238 let te = vec![0.005, 0.010, 0.015];
239 let mask = vec![0u8; 1]; let magnitude = vec![100.0, 80.0, 60.0];
241
242 let (r2star_map, s0_map) = r2star_arlo(&magnitude, &mask, &te, &grid(1, 1, 1));
243 assert_eq!(r2star_map[0], 0.0);
244 assert_eq!(s0_map[0], 0.0);
245 }
246
247 #[test]
248 fn test_r2star_arlo_multiple_voxels() {
249 let te = vec![0.005, 0.010, 0.015, 0.020];
250 let n_echoes = te.len();
251 let nx = 2; let ny = 2; let nz = 1;
252 let n_voxels = nx * ny * nz;
253
254 let r2star_values = vec![20.0, 40.0, 60.0, 80.0];
255 let s0: f64 = 500.0;
256
257 let mask = vec![1u8; n_voxels];
258 let mut magnitude = vec![0.0_f64; n_voxels * n_echoes];
259 for v in 0..n_voxels {
260 for (e, &t) in te.iter().enumerate() {
261 magnitude[v * n_echoes + e] = s0 * f64::exp(-r2star_values[v] * t);
262 }
263 }
264
265 let (r2star_map, _s0_map) = r2star_arlo(&magnitude, &mask, &te, &grid(nx, ny, nz));
266
267 for v in 0..n_voxels {
268 let err = (r2star_map[v] - r2star_values[v]).abs() / r2star_values[v];
269 assert!(err < 0.01, "Voxel {} R2* error {:.4}% should be < 1%", v, err * 100.0);
270 }
271 }
272}