qsm_core/unwrap/
laplacian.rs1use std::f64::consts::PI;
15use num_complex::Complex64;
16use crate::fft::{fft3d, ifft3d};
17
18#[inline]
20fn wrap(x: f64) -> f64 {
21 let mut y = x % (2.0 * PI);
22 if y > PI {
23 y -= 2.0 * PI;
24 } else if y < -PI {
25 y += 2.0 * PI;
26 }
27 y
28}
29
30fn wrapped_laplacian_periodic(
34 phase: &[f64],
35 nx: usize, ny: usize, nz: usize,
36 vsx: f64, vsy: f64, vsz: f64,
37) -> Vec<f64> {
38 let n_total = nx * ny * nz;
39 let mut d2u = vec![0.0; n_total];
40
41 let dx2 = 1.0 / (vsx * vsx);
42 let dy2 = 1.0 / (vsy * vsy);
43 let dz2 = 1.0 / (vsz * vsz);
44
45 for k in 0..nz {
47 let km1 = if k == 0 { nz - 1 } else { k - 1 };
48 let kp1 = if k + 1 >= nz { 0 } else { k + 1 };
49
50 for j in 0..ny {
51 let jm1 = if j == 0 { ny - 1 } else { j - 1 };
52 let jp1 = if j + 1 >= ny { 0 } else { j + 1 };
53
54 for i in 0..nx {
55 let im1 = if i == 0 { nx - 1 } else { i - 1 };
56 let ip1 = if i + 1 >= nx { 0 } else { i + 1 };
57
58 let idx = i + j * nx + k * nx * ny;
59 let u_ijk = phase[idx];
60
61 let idx_im1 = im1 + j * nx + k * nx * ny;
63 let idx_ip1 = ip1 + j * nx + k * nx * ny;
64 let idx_jm1 = i + jm1 * nx + k * nx * ny;
65 let idx_jp1 = i + jp1 * nx + k * nx * ny;
66 let idx_km1 = i + j * nx + km1 * nx * ny;
67 let idx_kp1 = i + j * nx + kp1 * nx * ny;
68
69 let lap_x = (wrap(phase[idx_ip1] - u_ijk) - wrap(u_ijk - phase[idx_im1])) * dx2;
73 let lap_y = (wrap(phase[idx_jp1] - u_ijk) - wrap(u_ijk - phase[idx_jm1])) * dy2;
74 let lap_z = (wrap(phase[idx_kp1] - u_ijk) - wrap(u_ijk - phase[idx_km1])) * dz2;
75
76 d2u[idx] = lap_x + lap_y + lap_z;
77 }
78 }
79 }
80
81 d2u
82}
83
84fn solve_poisson_fft(
89 f: &[f64],
90 nx: usize, ny: usize, nz: usize,
91 vsx: f64, vsy: f64, vsz: f64,
92) -> Vec<f64> {
93 let n_total = nx * ny * nz;
94
95 let mut f_complex: Vec<Complex64> = f.iter()
97 .map(|&x| Complex64::new(x, 0.0))
98 .collect();
99 fft3d(&mut f_complex, nx, ny, nz);
100
101 let idx2 = 1.0 / (vsx * vsx);
102 let idy2 = 1.0 / (vsy * vsy);
103 let idz2 = 1.0 / (vsz * vsz);
104
105 for k in 0..nz {
108 let fk = if k <= nz / 2 { k as f64 / nz as f64 } else { (k as f64 - nz as f64) / nz as f64 };
109 let lam_z = 2.0 * ((2.0 * PI * fk).cos() - 1.0) * idz2;
110
111 for j in 0..ny {
112 let fj = if j <= ny / 2 { j as f64 / ny as f64 } else { (j as f64 - ny as f64) / ny as f64 };
113 let lam_y = 2.0 * ((2.0 * PI * fj).cos() - 1.0) * idy2;
114
115 for i in 0..nx {
116 let fi = if i <= nx / 2 { i as f64 / nx as f64 } else { (i as f64 - nx as f64) / nx as f64 };
117 let lam_x = 2.0 * ((2.0 * PI * fi).cos() - 1.0) * idx2;
118
119 let lam = lam_x + lam_y + lam_z;
120 let idx = i + j * nx + k * nx * ny;
121
122 if lam.abs() > 1e-20 {
123 f_complex[idx] /= lam;
124 } else {
125 f_complex[idx] = Complex64::new(0.0, 0.0);
127 }
128 }
129 }
130 }
131
132 ifft3d(&mut f_complex, nx, ny, nz);
134
135 f_complex.iter().map(|c| c.re).collect()
137}
138
139pub fn laplacian_unwrap(
153 phase: &[f64],
154 mask: &[u8],
155 nx: usize, ny: usize, nz: usize,
156 vsx: f64, vsy: f64, vsz: f64,
157) -> Vec<f64> {
158 let n_total = nx * ny * nz;
159
160 let d2u = wrapped_laplacian_periodic(phase, nx, ny, nz, vsx, vsy, vsz);
162
163 let d2u_masked: Vec<f64> = d2u.iter()
165 .enumerate()
166 .map(|(i, &val)| if mask[i] != 0 { val } else { 0.0 })
167 .collect();
168
169 let unwrapped = solve_poisson_fft(&d2u_masked, nx, ny, nz, vsx, vsy, vsz);
171
172 let mut result = vec![0.0; n_total];
174 for i in 0..n_total {
175 if mask[i] != 0 {
176 result[i] = unwrapped[i];
177 }
178 }
179
180 result
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_wrap() {
189 assert!((wrap(0.0) - 0.0).abs() < 1e-10);
190 assert!((wrap(PI) - PI).abs() < 1e-10);
191 assert!((wrap(-PI) - (-PI)).abs() < 1e-10);
192 assert!((wrap(2.0 * PI) - 0.0).abs() < 1e-10);
193 assert!((wrap(3.0 * PI) - PI).abs() < 1e-10);
194 assert!((wrap(-3.0 * PI) - (-PI)).abs() < 1e-10);
195 }
196
197 #[test]
198 fn test_laplacian_unwrap_constant() {
199 let n = 8;
201 let phase = vec![1.0; n * n * n];
202 let mask = vec![1u8; n * n * n];
203
204 let unwrapped = laplacian_unwrap(&phase, &mask, n, n, n, 1.0, 1.0, 1.0);
205
206 let mean: f64 = unwrapped.iter().sum::<f64>() / (n * n * n) as f64;
208 for &val in unwrapped.iter() {
209 assert!((val - mean).abs() < 1e-6, "Constant phase should unwrap to constant");
210 }
211 }
212
213 #[test]
214 fn test_laplacian_unwrap_smooth() {
215 let n = 16;
217 let mut phase = vec![0.0; n * n * n];
218 let mask = vec![1u8; n * n * n];
219
220 for k in 0..n {
222 for j in 0..n {
223 for i in 0..n {
224 let idx = i + j * n + k * n * n;
225 phase[idx] = 0.5 * (2.0 * PI * i as f64 / n as f64).sin();
227 }
228 }
229 }
230
231 let unwrapped = laplacian_unwrap(&phase, &mask, n, n, n, 1.0, 1.0, 1.0);
232
233 for (i, (&orig, &unwr)) in phase.iter().zip(unwrapped.iter()).enumerate() {
236 assert!(unwr.is_finite(), "Unwrapped should be finite at {}", i);
237 assert!((orig - unwr).abs() < 1.0,
239 "Unwrapped should be close to original for smooth phase");
240 }
241 }
242
243 #[test]
244 fn test_laplacian_unwrap_finite() {
245 let n = 8;
246 let phase: Vec<f64> = (0..n*n*n).map(|i| wrap((i as f64) * 0.1)).collect();
247 let mask = vec![1u8; n * n * n];
248
249 let unwrapped = laplacian_unwrap(&phase, &mask, n, n, n, 1.0, 1.0, 1.0);
250
251 for (i, &val) in unwrapped.iter().enumerate() {
252 assert!(val.is_finite(), "Unwrapped phase should be finite at index {}", i);
253 }
254 }
255}