1use std::f64::consts::PI;
63#[cfg(test)]
64use num_complex::Complex64;
65#[cfg(test)]
66use crate::fft::{fft3d, ifft3d};
67use crate::Grid;
68
69#[inline]
71pub(crate) fn wrap(x: f64) -> f64 {
72 let mut y = x % (2.0 * PI);
73 if y > PI {
74 y -= 2.0 * PI;
75 } else if y < -PI {
76 y += 2.0 * PI;
77 }
78 y
79}
80
81pub(crate) fn wrapped_laplacian_periodic(
85 phase: &[f64],
86 nx: usize, ny: usize, nz: usize,
87 vsx: f64, vsy: f64, vsz: f64,
88) -> Vec<f64> {
89 let n_total = nx * ny * nz;
90 let mut d2u = vec![0.0; n_total];
91
92 let dx2 = 1.0 / (vsx * vsx);
93 let dy2 = 1.0 / (vsy * vsy);
94 let dz2 = 1.0 / (vsz * vsz);
95
96 for k in 0..nz {
97 let km1 = if k == 0 { nz - 1 } else { k - 1 };
98 let kp1 = if k + 1 >= nz { 0 } else { k + 1 };
99
100 for j in 0..ny {
101 let jm1 = if j == 0 { ny - 1 } else { j - 1 };
102 let jp1 = if j + 1 >= ny { 0 } else { j + 1 };
103
104 for i in 0..nx {
105 let im1 = if i == 0 { nx - 1 } else { i - 1 };
106 let ip1 = if i + 1 >= nx { 0 } else { i + 1 };
107
108 let idx = i + j * nx + k * nx * ny;
109 let u_ijk = phase[idx];
110
111 let idx_im1 = im1 + j * nx + k * nx * ny;
112 let idx_ip1 = ip1 + j * nx + k * nx * ny;
113 let idx_jm1 = i + jm1 * nx + k * nx * ny;
114 let idx_jp1 = i + jp1 * nx + k * nx * ny;
115 let idx_km1 = i + j * nx + km1 * nx * ny;
116 let idx_kp1 = i + j * nx + kp1 * nx * ny;
117
118 let lap_x = (wrap(phase[idx_ip1] - u_ijk) - wrap(u_ijk - phase[idx_im1])) * dx2;
119 let lap_y = (wrap(phase[idx_jp1] - u_ijk) - wrap(u_ijk - phase[idx_jm1])) * dy2;
120 let lap_z = (wrap(phase[idx_kp1] - u_ijk) - wrap(u_ijk - phase[idx_km1])) * dz2;
121
122 d2u[idx] = lap_x + lap_y + lap_z;
123 }
124 }
125 }
126
127 d2u
128}
129
130#[cfg(test)]
135pub(crate) fn solve_poisson_fft(
136 f: &[f64],
137 nx: usize, ny: usize, nz: usize,
138 vsx: f64, vsy: f64, vsz: f64,
139) -> Vec<f64> {
140 let mut f_complex: Vec<Complex64> = f.iter()
141 .map(|&x| Complex64::new(x, 0.0))
142 .collect();
143 fft3d(&mut f_complex, nx, ny, nz);
144
145 let idx2 = 1.0 / (vsx * vsx);
146 let idy2 = 1.0 / (vsy * vsy);
147 let idz2 = 1.0 / (vsz * vsz);
148
149 for k in 0..nz {
150 let fk = if k <= nz / 2 { k as f64 / nz as f64 } else { (k as f64 - nz as f64) / nz as f64 };
151 let lam_z = 2.0 * ((2.0 * PI * fk).cos() - 1.0) * idz2;
152
153 for j in 0..ny {
154 let fj = if j <= ny / 2 { j as f64 / ny as f64 } else { (j as f64 - ny as f64) / ny as f64 };
155 let lam_y = 2.0 * ((2.0 * PI * fj).cos() - 1.0) * idy2;
156
157 for i in 0..nx {
158 let fi = if i <= nx / 2 { i as f64 / nx as f64 } else { (i as f64 - nx as f64) / nx as f64 };
159 let lam_x = 2.0 * ((2.0 * PI * fi).cos() - 1.0) * idx2;
160
161 let lam = lam_x + lam_y + lam_z;
162 let idx = i + j * nx + k * nx * ny;
163
164 if lam.abs() > 1e-20 {
165 f_complex[idx] /= lam;
166 } else {
167 f_complex[idx] = Complex64::new(0.0, 0.0);
168 }
169 }
170 }
171 }
172
173 ifft3d(&mut f_complex, nx, ny, nz);
174 f_complex.iter().map(|c| c.re).collect()
175}
176
177fn solve_poisson_dirichlet_roi(
230 f: &[f64],
231 mask: &[u8],
232 grid: &Grid,
233 tol: f64,
234 max_iter: usize,
235) -> Vec<f64> {
236 let (nx, ny, nz) = grid.dims;
237 let (vsx, vsy, vsz) = grid.voxel_size;
238 let (dx2, dy2, dz2) = (1.0 / (vsx * vsx), 1.0 / (vsy * vsy), 1.0 / (vsz * vsz));
239 let diag = -2.0 * (dx2 + dy2 + dz2);
240 let omega = 1.5;
241
242 let mut u = vec![0.0f64; nx * ny * nz];
243
244 let scale = f.iter().map(|v| v.abs()).fold(0.0f64, f64::max).max(1e-30);
246 let scaled_tol = tol * scale / diag.abs();
247
248 for _ in 0..max_iter {
249 let mut max_change = 0.0f64;
250 for k in 1..nz - 1 {
251 for j in 1..ny - 1 {
252 for i in 1..nx - 1 {
253 let idx = i + j * nx + k * nx * ny;
254 if mask[idx] == 0 {
255 continue; }
257 let sum = dx2 * (u[idx - 1] + u[idx + 1])
258 + dy2 * (u[idx - nx] + u[idx + nx])
259 + dz2 * (u[idx - nx * ny] + u[idx + nx * ny]);
260 let target = (f[idx] - sum) / diag;
262 let old = u[idx];
263 let next = old + omega * (target - old);
264 max_change = max_change.max((next - old).abs());
265 u[idx] = next;
266 }
267 }
268 }
269 if max_change < scaled_tol {
270 break;
271 }
272 }
273 u
274}
275
276#[deprecated(
277 since = "0.35.0",
278 note = "unwrap with `laplacian_unwrap` and then remove background with a `bgremove` \
279 method (`lbv` reproduces this; `vsharp` is more robust at long TE). That route \
280 is more accurate at every echo time measured and composes with any background \
281 removal. Kept for parity with QSM.jl's `unwrap_laplacian(solver = :mgpcg)`."
282)]
283pub fn laplacian_unwrap_bfr(
284 phase: &[f64],
285 mask: &[u8],
286 grid: &Grid,
287) -> Vec<f64> {
288 let (nx, ny, nz) = grid.dims;
289 let (vsx, vsy, vsz) = grid.voxel_size;
290 let n_total = nx * ny * nz;
291
292 let d2u = wrapped_laplacian_periodic(phase, nx, ny, nz, vsx, vsy, vsz);
293
294 let d2u_masked: Vec<f64> = d2u.iter()
295 .enumerate()
296 .map(|(i, &val)| if mask[i] != 0 { val } else { 0.0 })
297 .collect();
298
299 let max_iter = (3 * nx.max(ny).max(nz)).min(500);
303 let unwrapped = solve_poisson_dirichlet_roi(&d2u_masked, mask, grid, 1e-6, max_iter);
304
305 let mut result = vec![0.0; n_total];
306 for i in 0..n_total {
307 if mask[i] != 0 {
308 result[i] = unwrapped[i];
309 }
310 }
311
312 result
313}
314
315pub(crate) fn wrapped_laplacian_neumann(
321 phase: &[f64],
322 nx: usize, ny: usize, nz: usize,
323 vsx: f64, vsy: f64, vsz: f64,
324) -> Vec<f64> {
325 let n_total = nx * ny * nz;
326 let mut d2u = vec![0.0; n_total];
327 let (dx2, dy2, dz2) = (1.0 / (vsx * vsx), 1.0 / (vsy * vsy), 1.0 / (vsz * vsz));
328
329 for k in 0..nz {
330 let km1 = k.saturating_sub(1);
331 let kp1 = if k + 1 >= nz { k } else { k + 1 };
332 for j in 0..ny {
333 let jm1 = j.saturating_sub(1);
334 let jp1 = if j + 1 >= ny { j } else { j + 1 };
335 for i in 0..nx {
336 let im1 = i.saturating_sub(1);
337 let ip1 = if i + 1 >= nx { i } else { i + 1 };
338 let idx = i + j * nx + k * nx * ny;
339 let u = phase[idx];
340 let lap_x = (wrap(phase[ip1 + j * nx + k * nx * ny] - u) - wrap(u - phase[im1 + j * nx + k * nx * ny])) * dx2;
341 let lap_y = (wrap(phase[i + jp1 * nx + k * nx * ny] - u) - wrap(u - phase[i + jm1 * nx + k * nx * ny])) * dy2;
342 let lap_z = (wrap(phase[i + j * nx + kp1 * nx * ny] - u) - wrap(u - phase[i + j * nx + km1 * nx * ny])) * dz2;
343 d2u[idx] = lap_x + lap_y + lap_z;
344 }
345 }
346 }
347 d2u
348}
349
350pub(crate) fn solve_poisson_dct(
360 f: &[f64],
361 nx: usize, ny: usize, nz: usize,
362 vsx: f64, vsy: f64, vsz: f64,
363) -> Vec<f64> {
364 use rustdct::DctPlanner;
365 let nxy = nx * ny;
366 let mut data = f.to_vec();
367 let mut planner = DctPlanner::<f64>::new();
368
369 let dct2_x = planner.plan_dct2(nx);
371 let dct2_y = planner.plan_dct2(ny);
372 let dct2_z = planner.plan_dct2(nz);
373 {
374 let mut scratch = vec![0.0; dct2_x.get_scratch_len()];
375 for row in data.chunks_mut(nx) {
376 dct2_x.process_dct2_with_scratch(row, &mut scratch);
377 }
378 }
379 {
380 let mut buf = vec![0.0; ny];
381 let mut scratch = vec![0.0; dct2_y.get_scratch_len()];
382 for k in 0..nz {
383 for i in 0..nx {
384 for j in 0..ny { buf[j] = data[i + j * nx + k * nxy]; }
385 dct2_y.process_dct2_with_scratch(&mut buf, &mut scratch);
386 for j in 0..ny { data[i + j * nx + k * nxy] = buf[j]; }
387 }
388 }
389 }
390 {
391 let mut buf = vec![0.0; nz];
392 let mut scratch = vec![0.0; dct2_z.get_scratch_len()];
393 for j in 0..ny {
394 for i in 0..nx {
395 for k in 0..nz { buf[k] = data[i + j * nx + k * nxy]; }
396 dct2_z.process_dct2_with_scratch(&mut buf, &mut scratch);
397 for k in 0..nz { data[i + j * nx + k * nxy] = buf[k]; }
398 }
399 }
400 }
401
402 let (ix2, iy2, iz2) = (1.0 / (vsx * vsx), 1.0 / (vsy * vsy), 1.0 / (vsz * vsz));
404 let lam_x: Vec<f64> = (0..nx).map(|i| 2.0 * ((PI * i as f64 / nx as f64).cos() - 1.0) * ix2).collect();
405 let lam_y: Vec<f64> = (0..ny).map(|j| 2.0 * ((PI * j as f64 / ny as f64).cos() - 1.0) * iy2).collect();
406 let lam_z: Vec<f64> = (0..nz).map(|k| 2.0 * ((PI * k as f64 / nz as f64).cos() - 1.0) * iz2).collect();
407 for (k, &lz) in lam_z.iter().enumerate() {
408 for (j, &ly) in lam_y.iter().enumerate() {
409 let row = j * nx + k * nxy;
410 for (i, &lx) in lam_x.iter().enumerate() {
411 let lam = lx + ly + lz;
412 let idx = row + i;
413 data[idx] = if lam.abs() > 1e-20 { data[idx] / lam } else { 0.0 };
414 }
415 }
416 }
417
418 let dct3_x = planner.plan_dct3(nx);
420 let dct3_y = planner.plan_dct3(ny);
421 let dct3_z = planner.plan_dct3(nz);
422 {
423 let mut buf = vec![0.0; nz];
424 let mut scratch = vec![0.0; dct3_z.get_scratch_len()];
425 for j in 0..ny {
426 for i in 0..nx {
427 for k in 0..nz { buf[k] = data[i + j * nx + k * nxy]; }
428 dct3_z.process_dct3_with_scratch(&mut buf, &mut scratch);
429 for k in 0..nz { data[i + j * nx + k * nxy] = buf[k]; }
430 }
431 }
432 }
433 {
434 let mut buf = vec![0.0; ny];
435 let mut scratch = vec![0.0; dct3_y.get_scratch_len()];
436 for k in 0..nz {
437 for i in 0..nx {
438 for j in 0..ny { buf[j] = data[i + j * nx + k * nxy]; }
439 dct3_y.process_dct3_with_scratch(&mut buf, &mut scratch);
440 for j in 0..ny { data[i + j * nx + k * nxy] = buf[j]; }
441 }
442 }
443 }
444 {
445 let mut scratch = vec![0.0; dct3_x.get_scratch_len()];
446 for row in data.chunks_mut(nx) {
447 dct3_x.process_dct3_with_scratch(row, &mut scratch);
448 }
449 }
450 let norm = 8.0 / (nx as f64 * ny as f64 * nz as f64);
451 for v in data.iter_mut() { *v *= norm; }
452 data
453}
454
455#[cfg(test)]
459fn laplacian_unwrap_even_extended_reference(
460 phase: &[f64],
461 mask: &[u8],
462 grid: &Grid,
463) -> Vec<f64> {
464 let (nx, ny, nz) = grid.dims;
465 let (vsx, vsy, vsz) = grid.voxel_size;
466 let n_total = nx * ny * nz;
467
468 let (px, py, pz) = (2 * nx, 2 * ny, 2 * nz);
471 let mut ext = vec![0.0f64; px * py * pz];
472 for k in 0..pz {
473 let sk = if k < nz { k } else { 2 * nz - 1 - k };
474 for j in 0..py {
475 let sj = if j < ny { j } else { 2 * ny - 1 - j };
476 for i in 0..px {
477 let si = if i < nx { i } else { 2 * nx - 1 - i };
478 ext[i + j * px + k * px * py] = phase[si + sj * nx + sk * nx * ny];
479 }
480 }
481 }
482
483 let d2u = wrapped_laplacian_periodic(&ext, px, py, pz, vsx, vsy, vsz);
486 let solved = solve_poisson_fft(&d2u, px, py, pz, vsx, vsy, vsz);
487
488 let mut result = vec![0.0; n_total];
489 for k in 0..nz {
490 for j in 0..ny {
491 for i in 0..nx {
492 let dst = i + j * nx + k * nx * ny;
493 if mask[dst] != 0 {
494 result[dst] = solved[i + j * px + k * px * py];
495 }
496 }
497 }
498 }
499 result
500}
501
502pub fn laplacian_unwrap(
530 phase: &[f64],
531 mask: &[u8],
532 grid: &Grid,
533) -> Vec<f64> {
534 let (nx, ny, nz) = grid.dims;
535 let (vsx, vsy, vsz) = grid.voxel_size;
536 let d2u = wrapped_laplacian_neumann(phase, nx, ny, nz, vsx, vsy, vsz);
537 let u = solve_poisson_dct(&d2u, nx, ny, nz, vsx, vsy, vsz);
538 u.iter().zip(mask).map(|(&v, &m)| if m != 0 { v } else { 0.0 }).collect()
539}
540
541#[cfg(test)]
542#[allow(deprecated)]
543mod tests {
544 use super::*;
545
546 fn grid(n: usize) -> Grid {
547 Grid::new(n, n, n, 1.0, 1.0, 1.0)
548 }
549
550 #[test]
551 fn test_wrap() {
552 assert!((wrap(0.0) - 0.0).abs() < 1e-10);
553 assert!((wrap(PI) - PI).abs() < 1e-10);
554 assert!((wrap(-PI) - (-PI)).abs() < 1e-10);
555 assert!((wrap(2.0 * PI) - 0.0).abs() < 1e-10);
556 assert!((wrap(3.0 * PI) - PI).abs() < 1e-10);
557 assert!((wrap(-3.0 * PI) - (-PI)).abs() < 1e-10);
558 }
559
560 #[test]
561 fn test_laplacian_unwrap_bfr_constant() {
562 let n = 8;
563 let phase = vec![1.0; n * n * n];
564 let mask = vec![1u8; n * n * n];
565
566 let unwrapped = laplacian_unwrap_bfr(&phase, &mask, &grid(n));
567
568 let mean: f64 = unwrapped.iter().sum::<f64>() / (n * n * n) as f64;
569 for &val in unwrapped.iter() {
570 assert!((val - mean).abs() < 1e-6, "Constant phase should unwrap to constant");
571 }
572 }
573
574 #[test]
575 fn test_laplacian_unwrap_bfr_smooth() {
576 let n = 16;
577 let mut phase = vec![0.0; n * n * n];
578 let mask = vec![1u8; n * n * n];
579
580 for k in 0..n {
581 for j in 0..n {
582 for i in 0..n {
583 let idx = i + j * n + k * n * n;
584 phase[idx] = 0.5 * (2.0 * PI * i as f64 / n as f64).sin();
585 }
586 }
587 }
588
589 let unwrapped = laplacian_unwrap_bfr(&phase, &mask, &grid(n));
590
591 assert!(unwrapped.iter().all(|v| v.is_finite()), "output must be finite");
600 let peak = unwrapped.iter().fold(0.0f64, |m, &v| m.max(v.abs()));
601 assert!(peak <= 0.5 + 1e-6, "output should not exceed the input amplitude, got {peak}");
602 }
603
604 fn corr_slope(obs: &[f64], want: &[f64], mask: &[u8]) -> (f64, f64) {
606 let (mut sa, mut sb, mut n) = (0.0, 0.0, 0usize);
607 for i in 0..obs.len() {
608 if mask[i] != 0 { sa += obs[i]; sb += want[i]; n += 1; }
609 }
610 let nf = n as f64;
611 let (ma, mb) = (sa / nf, sb / nf);
612 let (mut cov, mut va, mut vb) = (0.0, 0.0, 0.0);
613 for i in 0..obs.len() {
614 if mask[i] == 0 { continue; }
615 let (da, db) = (obs[i] - ma, want[i] - mb);
616 cov += da * db;
617 va += da * da;
618 vb += db * db;
619 }
620 (cov / (va.sqrt() * vb.sqrt()), cov / va)
621 }
622
623 fn ramp_plus_blob(n: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>, Vec<u8>) {
627 let c = n as f64 / 2.0;
628 let total = n * n * n;
629 let (mut truth, mut ramp, mut blob) = (vec![0.0; total], vec![0.0; total], vec![0.0; total]);
630 let mut mask = vec![0u8; total];
631 for k in 0..n {
632 for j in 0..n {
633 for i in 0..n {
634 let idx = i + j * n + k * n * n;
635 let (x, y, z) = (i as f64 - c, j as f64 - c, k as f64 - c);
636 let r = (x * x + y * y + z * z).sqrt();
637 ramp[idx] = 0.35 * x + 0.20 * y + 0.15 * z;
638 blob[idx] = 12.0 * (-(r * r) / (2.0 * 8.0f64.powi(2))).exp();
639 truth[idx] = ramp[idx] + blob[idx];
640 if r < 22.0 { mask[idx] = 1; }
641 }
642 }
643 }
644 let wrapped: Vec<f64> = truth.iter().map(|&v| wrap(v)).collect();
645 (wrapped, truth, ramp, blob, mask)
646 }
647
648 #[test]
649 fn laplacian_unwrap_recovers_the_whole_field_including_background() {
650 let n = 64;
651 let (wrapped, truth, _, _, mask) = ramp_plus_blob(n);
652
653 let out = laplacian_unwrap(&wrapped, &mask, &grid(n));
654 let (r, slope) = corr_slope(&out, &truth, &mask);
655
656 assert!(r > 0.99, "should recover the total field, got r = {r}");
657 assert!((slope - 1.0).abs() < 0.05, "expected unit slope, got {slope}");
658 }
659
660 #[test]
661 fn laplacian_unwrap_bfr_removes_the_harmonic_component() {
662 let n = 64;
670 let (wrapped, _, ramp, blob, mask) = ramp_plus_blob(n);
671
672 let out = laplacian_unwrap_bfr(&wrapped, &mask, &grid(n));
673
674 let (r_blob, slope_blob) = corr_slope(&out, &blob, &mask);
675 assert!(r_blob > 0.99, "non-harmonic part should survive, got r = {r_blob}");
676 assert!((slope_blob - 1.0).abs() < 0.05, "expected unit slope, got {slope_blob}");
677
678 let (r_ramp, _) = corr_slope(&out, &ramp, &mask);
679 assert!(r_ramp.abs() < 0.05, "harmonic part should be discarded, got r = {r_ramp}");
680 }
681
682 #[test]
683 fn dct_solve_matches_the_even_extended_solve() {
684 let n = 64;
687 let (wrapped, _, _, _, mask) = ramp_plus_blob(n);
688 let padded = laplacian_unwrap_even_extended_reference(&wrapped, &mask, &grid(n));
689 let dct = laplacian_unwrap(&wrapped, &mask, &grid(n));
690 let scale = padded.iter().fold(0.0f64, |m, &v| m.max(v.abs()));
691 let max_diff = padded.iter().zip(&dct).fold(0.0f64, |m, (&a, &b)| m.max((a - b).abs()));
692 assert!(max_diff < 1e-9 * scale, "DCT and padded solves differ: max |diff| = {max_diff} (scale {scale})");
693 }
694
695 #[test]
696 fn the_two_variants_disagree_by_the_background_field() {
697 let n = 64;
700 let (wrapped, _, ramp, _, mask) = ramp_plus_blob(n);
701
702 let pure = laplacian_unwrap(&wrapped, &mask, &grid(n));
703 let combined = laplacian_unwrap_bfr(&wrapped, &mask, &grid(n));
704 let diff: Vec<f64> = pure.iter().zip(&combined).map(|(a, b)| a - b).collect();
705
706 let (r, _) = corr_slope(&diff, &ramp, &mask);
707 assert!(r > 0.95, "their difference should be the harmonic field, got r = {r}");
708 }
709
710 #[test]
711 fn test_laplacian_unwrap_bfr_finite() {
712 let n = 8;
713 let phase: Vec<f64> = (0..n*n*n).map(|i| wrap((i as f64) * 0.1)).collect();
714 let mask = vec![1u8; n * n * n];
715
716 let unwrapped = laplacian_unwrap_bfr(&phase, &mask, &grid(n));
717
718 for (i, &val) in unwrapped.iter().enumerate() {
719 assert!(val.is_finite(), "Unwrapped phase should be finite at index {}", i);
720 }
721 }
722}