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
34#[allow(clippy::too_many_arguments)]
53pub fn r2star_arlo(
54 magnitude: &[f64],
55 mask: &[u8],
56 echo_times: &[f64],
57 nx: usize, ny: usize, nz: usize,
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 = nx * ny * nz;
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 alphas = Vec::new();
97 for i in 0..(n_echoes - 1) {
98 if signal[i] > 1e-10 {
99 let alpha = signal[i + 1] / signal[i];
100 if alpha > 0.0 && alpha <= 1.0 {
101 alphas.push(alpha);
102 }
103 }
104 }
105
106 if alphas.len() >= 2 {
107 let alpha_mean: f64 = alphas.iter().sum::<f64>() / alphas.len() as f64;
108 let r2star_val = -alpha_mean.ln() / delta_te;
109
110 if r2star_val >= 0.0 && r2star_val <= 500.0 {
111 r2star_map[v] = r2star_val;
112 s0_map[v] = signal[0] * (r2star_val * te_sorted[0]).exp();
113 continue;
114 }
115 }
116
117 let (r2, s0) = log_linear_fit(&signal, &te_sorted);
119 r2star_map[v] = r2.max(0.0);
120 s0_map[v] = s0.max(0.0);
121 }
122
123 (r2star_map, s0_map)
124}
125
126pub fn t2star_from_r2star(r2star: &[f64]) -> Vec<f64> {
130 r2star.iter().map(|&r| if r > 0.0 { 1.0 / r } else { 0.0 }).collect()
131}
132
133fn log_linear_fit(signal: &[f64], echo_times: &[f64]) -> (f64, f64) {
136 let n = signal.len();
137 let mut sum_t = 0.0;
138 let mut sum_tt = 0.0;
139 let mut sum_y = 0.0;
140 let mut sum_ty = 0.0;
141 let mut count = 0;
142
143 for i in 0..n {
144 if signal[i] > 1e-10 {
145 let y = signal[i].ln();
146 let t = echo_times[i];
147 sum_t += t;
148 sum_tt += t * t;
149 sum_y += y;
150 sum_ty += t * y;
151 count += 1;
152 }
153 }
154
155 if count < 2 {
156 return (0.0, 0.0);
157 }
158
159 let n_f = count as f64;
160 let denom = n_f * sum_tt - sum_t * sum_t;
161 if denom.abs() < 1e-15 {
162 return (0.0, 0.0);
163 }
164
165 let slope = (n_f * sum_ty - sum_t * sum_y) / denom;
166 let intercept = (sum_y - slope * sum_t) / n_f;
167
168 let r2star = -slope; let s0 = intercept.exp();
170
171 (r2star, s0)
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_use_arlo_equispaced() {
180 let te = vec![0.005, 0.010, 0.015, 0.020];
181 assert!(use_arlo(&te, 0.1e-3));
182 }
183
184 #[test]
185 fn test_use_arlo_not_equispaced() {
186 let te = vec![0.005, 0.008, 0.015];
187 assert!(!use_arlo(&te, 0.1e-3));
188 }
189
190 #[test]
191 fn test_use_arlo_too_few() {
192 let te = vec![0.005, 0.010];
193 assert!(!use_arlo(&te, 0.1e-3));
194 }
195
196 #[test]
197 fn test_r2star_arlo_synthetic() {
198 let r2star_true: f64 = 30.0; let s0_true: f64 = 1000.0;
201 let te = vec![0.005, 0.010, 0.015, 0.020, 0.025];
202 let n_echoes = te.len();
203
204 let mask = vec![1u8];
206 let magnitude: Vec<f64> = te.iter()
207 .map(|&t| s0_true * (-r2star_true * t).exp())
208 .collect();
209
210 let (r2star_map, s0_map) = r2star_arlo(&magnitude, &mask, &te, 1, 1, 1);
211
212 let r2star_err = (r2star_map[0] - r2star_true).abs() / r2star_true;
213 let s0_err = (s0_map[0] - s0_true).abs() / s0_true;
214
215 assert!(r2star_err < 0.01, "R2* error {:.4}% should be < 1%", r2star_err * 100.0);
216 assert!(s0_err < 0.01, "S0 error {:.4}% should be < 1%", s0_err * 100.0);
217 }
218
219 #[test]
220 fn test_t2star_from_r2star() {
221 let r2star = vec![50.0, 0.0, 100.0, 25.0];
222 let t2star = t2star_from_r2star(&r2star);
223 assert!((t2star[0] - 0.02).abs() < 1e-10);
224 assert_eq!(t2star[1], 0.0);
225 assert!((t2star[2] - 0.01).abs() < 1e-10);
226 assert!((t2star[3] - 0.04).abs() < 1e-10);
227 }
228
229 #[test]
230 fn test_r2star_arlo_masked_voxel() {
231 let te = vec![0.005, 0.010, 0.015];
232 let mask = vec![0u8; 1]; let magnitude = vec![100.0, 80.0, 60.0];
234
235 let (r2star_map, s0_map) = r2star_arlo(&magnitude, &mask, &te, 1, 1, 1);
236 assert_eq!(r2star_map[0], 0.0);
237 assert_eq!(s0_map[0], 0.0);
238 }
239
240 #[test]
241 fn test_r2star_arlo_multiple_voxels() {
242 let te = vec![0.005, 0.010, 0.015, 0.020];
243 let n_echoes = te.len();
244 let nx = 2; let ny = 2; let nz = 1;
245 let n_voxels = nx * ny * nz;
246
247 let r2star_values = vec![20.0, 40.0, 60.0, 80.0];
248 let s0: f64 = 500.0;
249
250 let mask = vec![1u8; n_voxels];
251 let mut magnitude = vec![0.0_f64; n_voxels * n_echoes];
252 for v in 0..n_voxels {
253 for (e, &t) in te.iter().enumerate() {
254 magnitude[v * n_echoes + e] = s0 * f64::exp(-r2star_values[v] * t);
255 }
256 }
257
258 let (r2star_map, _s0_map) = r2star_arlo(&magnitude, &mask, &te, nx, ny, nz);
259
260 for v in 0..n_voxels {
261 let err = (r2star_map[v] - r2star_values[v]).abs() / r2star_values[v];
262 assert!(err < 0.01, "Voxel {} R2* error {:.4}% should be < 1%", v, err * 100.0);
263 }
264 }
265}