1use std::f64::consts::PI;
23
24use crate::utils::threshold::otsu_threshold;
25use crate::Grid;
26
27pub const CHOP_AMBIGUOUS_COHERENCE: f64 = 0.5;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
37pub enum ChopCorrection {
38 #[default]
40 Auto,
41 Axes([bool; 3]),
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
48pub struct ComplexToMagPhaseParams {
49 pub chop: ChopCorrection,
51 pub negate_phase: bool,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
59pub struct SliceChopping {
60 pub coherence: [f64; 3],
64 pub corrected: [bool; 3],
66 pub detected: bool,
68}
69
70impl SliceChopping {
71 pub fn ambiguous_axes(&self) -> Vec<usize> {
74 (0..3).filter(|&a| self.coherence[a].abs() < CHOP_AMBIGUOUS_COHERENCE).collect()
75 }
76}
77
78#[derive(Clone, Debug)]
80pub struct MagPhaseEchoes {
81 pub magnitudes: Vec<Vec<f64>>,
83 pub phases: Vec<Vec<f64>>,
86 pub chopping: SliceChopping,
87 pub phase_negated: bool,
89}
90
91pub fn slice_chopping_coherence(real: &[&[f64]], imag: &[&[f64]], grid: &Grid) -> [f64; 3] {
106 check_echoes(real, imag, grid);
107 let (nx, ny, nz) = grid.dims;
108 let n = grid.n_total();
109
110 let mag0: Vec<f64> = (0..n).map(|v| finite(real[0][v]).hypot(finite(imag[0][v]))).collect();
111 let threshold = otsu_threshold(&mag0, 256);
112 let mut mask: Vec<bool> = mag0.iter().map(|&m| m > threshold).collect();
113 if !mask.iter().any(|&m| m) {
114 mask.fill(true);
115 }
116
117 let strides = [1, nx, nx * ny];
118 let mut num = [0.0f64; 3];
119 let mut den = [0.0f64; 3];
120 for (re, im) in real.iter().zip(imag) {
121 for k in 0..nz {
122 for j in 0..ny {
123 for i in 0..nx {
124 let v = i + j * nx + k * nx * ny;
125 if !mask[v] {
126 continue;
127 }
128 let (a_re, a_im) = (finite(re[v]), finite(im[v]));
129 for (axis, &inside) in [i + 1 < nx, j + 1 < ny, k + 1 < nz].iter().enumerate() {
130 let w = v + strides[axis];
131 if !inside || !mask[w] {
132 continue;
133 }
134 let (b_re, b_im) = (finite(re[w]), finite(im[w]));
135 num[axis] += a_re * b_re + a_im * b_im;
137 den[axis] += a_re.hypot(a_im) * b_re.hypot(b_im);
138 }
139 }
140 }
141 }
142 }
143 [0, 1, 2].map(|a| if den[a] > 0.0 { num[a] / den[a] } else { 0.0 })
144}
145
146pub fn complex_to_mag_phase(
161 real: &[&[f64]],
162 imag: &[&[f64]],
163 grid: &Grid,
164 params: &ComplexToMagPhaseParams,
165) -> MagPhaseEchoes {
166 let coherence = slice_chopping_coherence(real, imag, grid);
167 let (corrected, detected) = match params.chop {
168 ChopCorrection::Auto => (coherence.map(|c| c < 0.0), true),
169 ChopCorrection::Axes(axes) => (axes, false),
170 };
171
172 let (nx, ny, nz) = grid.dims;
173 let n = grid.n_total();
174 let mut magnitudes = Vec::with_capacity(real.len());
175 let mut phases = Vec::with_capacity(real.len());
176 for (re, im) in real.iter().zip(imag) {
177 let mut mag = vec![0.0f64; n];
178 let mut phase = vec![0.0f64; n];
179 for k in 0..nz {
180 for j in 0..ny {
181 for i in 0..nx {
182 let v = i + j * nx + k * nx * ny;
183 let (r, q) = (finite(re[v]), finite(im[v]));
184 let q = if params.negate_phase { -q } else { q };
185 let odd = (corrected[0] && i % 2 == 1) as u8
186 + (corrected[1] && j % 2 == 1) as u8
187 + (corrected[2] && k % 2 == 1) as u8;
188 let (r, q) = if odd % 2 == 1 { (-r, -q) } else { (r, q) };
189 mag[v] = r.hypot(q);
190 phase[v] = q.atan2(r);
191 }
192 }
193 }
194 magnitudes.push(mag);
195 phases.push(phase);
196 }
197 debug_assert!(phases.iter().flatten().all(|p| p.abs() <= PI));
198
199 MagPhaseEchoes {
200 magnitudes,
201 phases,
202 chopping: SliceChopping { coherence, corrected, detected },
203 phase_negated: params.negate_phase,
204 }
205}
206
207#[inline]
208fn finite(x: f64) -> f64 {
209 if x.is_finite() { x } else { 0.0 }
210}
211
212fn check_echoes(real: &[&[f64]], imag: &[&[f64]], grid: &Grid) {
213 assert!(!real.is_empty(), "complex_to_mag_phase: no echoes");
214 assert_eq!(
215 real.len(), imag.len(),
216 "complex_to_mag_phase: {} real echoes but {} imaginary",
217 real.len(), imag.len()
218 );
219 let n = grid.n_total();
220 for (e, (re, im)) in real.iter().zip(imag).enumerate() {
221 assert!(
222 re.len() == n && im.len() == n,
223 "complex_to_mag_phase: echo {e} has {} real and {} imaginary voxels, the grid has {n}",
224 re.len(), im.len()
225 );
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 type Echoes = (Vec<Vec<f64>>, Vec<Vec<f64>>, Vec<Vec<f64>>);
237
238 fn smooth_echoes(grid: &Grid, n_echoes: usize) -> Echoes {
239 let (nx, ny, nz) = grid.dims;
240 let (mut re, mut im, mut ph) = (Vec::new(), Vec::new(), Vec::new());
241 for e in 0..n_echoes {
242 let te = (e + 1) as f64;
243 let (mut r, mut q, mut p) = (vec![0.0; grid.n_total()], vec![0.0; grid.n_total()], vec![0.0; grid.n_total()]);
244 for k in 0..nz {
245 for j in 0..ny {
246 for i in 0..nx {
247 let (x, y, z) = (i as f64 / nx as f64 - 0.5, j as f64 / ny as f64 - 0.5, k as f64 / nz as f64 - 0.5);
248 let v = i + j * nx + k * nx * ny;
249 let inside = x * x / 0.16 + y * y / 0.12 + z * z / 0.14 < 1.0;
250 let m = if inside { 100.0 * (-0.1 * te).exp() } else { 0.5 };
251 let phase = wrap(0.3 + te * (1.5 * x + 0.8 * y * y - 1.2 * z));
252 r[v] = m * phase.cos();
253 q[v] = m * phase.sin();
254 p[v] = phase;
255 }
256 }
257 }
258 re.push(r);
259 im.push(q);
260 ph.push(p);
261 }
262 (re, im, ph)
263 }
264
265 fn wrap(p: f64) -> f64 {
266 (p + PI).rem_euclid(2.0 * PI) - PI
267 }
268
269 fn chop(data: &mut [Vec<f64>], grid: &Grid, axis: usize) {
271 let (nx, ny, nz) = grid.dims;
272 for vol in data.iter_mut() {
273 for k in 0..nz {
274 for j in 0..ny {
275 for i in 0..nx {
276 if [i, j, k][axis] % 2 == 1 {
277 vol[i + j * nx + k * nx * ny] *= -1.0;
278 }
279 }
280 }
281 }
282 }
283 }
284
285 fn auto() -> ComplexToMagPhaseParams {
286 ComplexToMagPhaseParams::default()
287 }
288
289 fn axes(a: [bool; 3]) -> ComplexToMagPhaseParams {
290 ComplexToMagPhaseParams { chop: ChopCorrection::Axes(a), ..Default::default() }
291 }
292
293 fn refs(v: &[Vec<f64>]) -> Vec<&[f64]> {
294 v.iter().map(|x| x.as_slice()).collect()
295 }
296
297 fn max_phase_err(a: &[f64], b: &[f64]) -> f64 {
299 a.iter().zip(b).map(|(x, y)| wrap(x - y).abs()).fold(0.0, f64::max)
300 }
301
302 #[test]
303 fn unchopped_input_is_left_alone() {
304 let grid = Grid::new(16, 16, 16, 1.0, 1.0, 1.0);
305 let (re, im, ph) = smooth_echoes(&grid, 3);
306 let out = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &auto());
307 assert_eq!(out.chopping.corrected, [false; 3]);
308 assert!(out.chopping.detected);
309 assert!(out.chopping.coherence.iter().all(|&c| c > 0.8), "{:?}", out.chopping.coherence);
310 assert!(out.chopping.ambiguous_axes().is_empty());
311 for e in 0..3 {
312 assert!(max_phase_err(&out.phases[e], &ph[e]) < 1e-12);
313 for v in 0..grid.n_total() {
314 assert!((out.magnitudes[e][v] - re[e][v].hypot(im[e][v])).abs() < 1e-12);
315 }
316 }
317 }
318
319 #[test]
320 fn chopping_is_detected_and_undone_on_each_axis() {
321 let grid = Grid::new(16, 14, 12, 1.0, 1.0, 2.0);
322 for axis in 0..3 {
323 let (mut re, mut im, ph) = smooth_echoes(&grid, 3);
324 chop(&mut re, &grid, axis);
325 chop(&mut im, &grid, axis);
326 let out = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &auto());
327 let mut expected = [false; 3];
328 expected[axis] = true;
329 assert_eq!(out.chopping.corrected, expected, "axis {axis}: {:?}", out.chopping.coherence);
330 assert!(out.chopping.coherence[axis] < -0.8, "axis {axis}: {:?}", out.chopping.coherence);
331 for e in 0..3 {
333 assert!(max_phase_err(&out.phases[e], &ph[e]) < 1e-12, "axis {axis} echo {e}");
334 }
335 }
336 }
337
338 #[test]
339 fn chopping_on_two_axes_is_undone() {
340 let grid = Grid::new(16, 16, 16, 1.0, 1.0, 1.0);
341 let (mut re, mut im, ph) = smooth_echoes(&grid, 2);
342 for axis in [0, 2] {
343 chop(&mut re, &grid, axis);
344 chop(&mut im, &grid, axis);
345 }
346 let out = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &auto());
347 assert_eq!(out.chopping.corrected, [true, false, true]);
348 assert!(max_phase_err(&out.phases[1], &ph[1]) < 1e-12);
349 }
350
351 #[test]
354 fn one_decision_covers_every_echo() {
355 let grid = Grid::new(16, 16, 16, 1.0, 1.0, 1.0);
356 let (mut re, mut im, ph) = smooth_echoes(&grid, 2);
357 chop(&mut re, &grid, 2);
358 chop(&mut im, &grid, 2);
359 let mut seed = 12345u64;
361 for v in 0..grid.n_total() {
362 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
363 let p = (seed >> 11) as f64 / (1u64 << 53) as f64 * 2.0 * PI;
364 let m = re[1][v].hypot(im[1][v]) * 0.01;
365 re[1][v] = m * p.cos();
366 im[1][v] = m * p.sin();
367 }
368 let out = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &auto());
369 assert_eq!(out.chopping.corrected, [false, false, true]);
370 assert!(max_phase_err(&out.phases[0], &ph[0]) < 1e-12);
371 }
372
373 #[test]
374 fn override_is_obeyed_against_the_data() {
375 let grid = Grid::new(16, 16, 16, 1.0, 1.0, 1.0);
376 let (mut re, mut im, ph) = smooth_echoes(&grid, 2);
377 chop(&mut re, &grid, 1);
378 chop(&mut im, &grid, 1);
379
380 let off = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &axes([false; 3]));
381 assert_eq!(off.chopping.corrected, [false; 3]);
382 assert!(!off.chopping.detected);
383 assert!(off.chopping.coherence[1] < -0.8);
385 assert!(max_phase_err(&off.phases[0], &ph[0]) > 3.0);
386
387 let forced = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &axes([false, true, false]));
388 assert_eq!(forced.chopping.corrected, [false, true, false]);
389 assert!(max_phase_err(&forced.phases[0], &ph[0]) < 1e-12);
390 }
391
392 #[test]
393 fn negate_phase_conjugates_and_keeps_the_chop_decision() {
394 let grid = Grid::new(16, 16, 16, 1.0, 1.0, 1.0);
395 let (mut re, mut im, ph) = smooth_echoes(&grid, 2);
396 chop(&mut re, &grid, 2);
397 chop(&mut im, &grid, 2);
398 let plain = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, &auto());
399 let params = ComplexToMagPhaseParams { negate_phase: true, ..Default::default() };
400 let negated = complex_to_mag_phase(&refs(&re), &refs(&im), &grid, ¶ms);
401 assert!(!plain.phase_negated && negated.phase_negated);
402 assert_eq!(negated.chopping, plain.chopping);
403 for e in 0..2 {
404 let flipped: Vec<f64> = ph[e].iter().map(|p| -p).collect();
405 assert!(max_phase_err(&negated.phases[e], &flipped) < 1e-12);
406 assert_eq!(negated.magnitudes[e], plain.magnitudes[e]);
407 }
408 }
409
410 #[test]
411 fn noise_only_input_is_ambiguous() {
412 let grid = Grid::new(16, 16, 16, 1.0, 1.0, 1.0);
413 let mut seed = 7u64;
414 let mut next = || {
415 seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
416 (seed >> 11) as f64 / (1u64 << 53) as f64 - 0.5
417 };
418 let re: Vec<f64> = (0..grid.n_total()).map(|_| next()).collect();
419 let im: Vec<f64> = (0..grid.n_total()).map(|_| next()).collect();
420 let out = complex_to_mag_phase(&[&re], &[&im], &grid, &auto());
421 assert_eq!(out.chopping.ambiguous_axes(), vec![0, 1, 2]);
422 }
423
424 #[test]
425 fn non_finite_input_becomes_zero() {
426 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
427 let mut re = vec![1.0; 64];
428 let im = vec![0.0; 64];
429 re[5] = f64::NAN;
430 let out = complex_to_mag_phase(&[&re], &[&im], &grid, &axes([false; 3]));
431 assert_eq!(out.magnitudes[0][5], 0.0);
432 assert_eq!(out.phases[0][5], 0.0);
433 }
434
435 #[test]
436 #[should_panic(expected = "complex_to_mag_phase: echo 0 has 64 real and 63 imaginary voxels")]
437 fn mismatched_volumes_are_refused() {
438 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
439 complex_to_mag_phase(&[&[0.0; 64]], &[&[0.0; 63]], &grid, &auto());
440 }
441}