1use num_complex::Complex64;
26use crate::Grid;
27use crate::fft::{fft3d, ifft3d};
28use crate::kernels::smv::smv_kernel;
29use crate::unwrap::laplacian::wrapped_laplacian_periodic;
30
31#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
33#[derive(Clone, Debug)]
34pub struct HarperellaParams {
35 pub radius: f64,
37 pub max_iter: usize,
39 pub tol: f64,
41}
42
43pub type IharperellaParams = HarperellaParams;
45
46impl Default for HarperellaParams {
47 fn default() -> Self {
48 Self {
49 radius: 10.0,
50 max_iter: 40,
51 tol: 1e-6,
52 }
53 }
54}
55
56pub fn harperella(
75 phase: &[f64], mask: &[u8],
76 grid: &Grid,
77 params: &HarperellaParams,
78 mut progress: impl FnMut(usize, usize),
79) -> (Vec<f64>, Vec<u8>) {
80 let (nx, ny, nz) = grid.dims;
81 let (vsx, vsy, vsz) = grid.voxel_size;
82 let n_total = nx * ny * nz;
83 let (lap_brain, interior_mask, exterior_mask) =
84 prepare_laplacian(phase, mask, nx, ny, nz, vsx, vsy, vsz);
85
86 let s_fft = compute_smv_fft(nx, ny, nz, vsx, vsy, vsz, params.radius);
88
89 let smv_lap = apply_smv(&lap_brain, &s_fft, nx, ny, nz);
91 let mut delta_sum = 0.0;
92 let mut delta_count = 0usize;
93 for i in 0..n_total {
94 if interior_mask[i] == 1 {
95 delta_sum += smv_lap[i];
96 delta_count += 1;
97 }
98 }
99 let delta_val = if delta_count > 0 { delta_sum / delta_count as f64 } else { 0.0 };
100
101 let rhs: Vec<f64> = (0..n_total).map(|i| delta_val - smv_lap[i]).collect();
104 let smv_rhs = apply_smv(&rhs, &s_fft, nx, ny, nz);
105 let atb: Vec<f64> = (0..n_total).map(|i| exterior_mask[i] * smv_rhs[i]).collect();
106
107 let lap_ext = cg_solve_masked(
108 &atb, &exterior_mask, params.max_iter, params.tol, &mut progress,
109 |x| {
110 let masked: Vec<f64> = (0..n_total).map(|i| exterior_mask[i] * x[i]).collect();
111 let sx = apply_smv(&masked, &s_fft, nx, ny, nz);
112 let ssx = apply_smv(&sx, &s_fft, nx, ny, nz);
113 (0..n_total).map(|i| exterior_mask[i] * ssx[i]).collect()
114 },
115 );
116
117 let mut lap_fov: Vec<f64> = lap_brain;
119 for i in 0..n_total { lap_fov[i] += exterior_mask[i] * lap_ext[i]; }
120 let tissue_phase = solve_poisson(&lap_fov, nx, ny, nz, vsx, vsy, vsz);
121
122 let result: Vec<f64> = tissue_phase.iter().enumerate()
123 .map(|(i, &v)| if mask[i] != 0 { v } else { 0.0 }).collect();
124 (result, mask.to_vec())
125}
126
127pub fn iharperella(
148 phase: &[f64], mask: &[u8],
149 grid: &Grid,
150 params: &HarperellaParams,
151 progress: impl FnMut(usize, usize),
152) -> (Vec<f64>, Vec<u8>) {
153 let w_brain: Vec<f64> = mask.iter().map(|&m| m as f64).collect();
154 iharperella_with_weights(phase, mask, grid,
155 params.max_iter, params.tol, &w_brain, progress)
156}
157
158pub fn iharperella_with_weights(
175 phase: &[f64], mask: &[u8],
176 grid: &Grid,
177 max_iter: usize, tol: f64,
178 w_brain: &[f64],
179 mut progress: impl FnMut(usize, usize),
180) -> (Vec<f64>, Vec<u8>) {
181 let (nx, ny, nz) = grid.dims;
182 let (vsx, vsy, vsz) = grid.voxel_size;
183 let n_total = nx * ny * nz;
184 let (lap_brain, _interior_mask, exterior_mask) =
185 prepare_laplacian(phase, mask, nx, ny, nz, vsx, vsy, vsz);
186
187 let lap_eig = compute_laplacian_eigenvalues(nx, ny, nz, vsx, vsy, vsz);
189
190 let phase_from_brain = apply_inv_lap(&lap_brain, &lap_eig, nx, ny, nz);
203 let w2_phase: Vec<f64> = (0..n_total).map(|i| w_brain[i] * w_brain[i] * phase_from_brain[i]).collect();
204 let atb_raw = apply_inv_lap(&w2_phase, &lap_eig, nx, ny, nz);
205 let atb: Vec<f64> = (0..n_total).map(|i| -exterior_mask[i] * atb_raw[i]).collect();
206
207 let lap_ext = cg_solve_masked(
208 &atb, &exterior_mask, max_iter, tol, &mut progress,
209 |x| {
210 let masked: Vec<f64> = (0..n_total).map(|i| exterior_mask[i] * x[i]).collect();
212 let inv1 = apply_inv_lap(&masked, &lap_eig, nx, ny, nz);
213 let w2_inv1: Vec<f64> = (0..n_total).map(|i| w_brain[i] * w_brain[i] * inv1[i]).collect();
214 let inv2 = apply_inv_lap(&w2_inv1, &lap_eig, nx, ny, nz);
215 (0..n_total).map(|i| exterior_mask[i] * inv2[i]).collect()
216 },
217 );
218
219 let mut lap_fov = lap_brain;
221 for i in 0..n_total { lap_fov[i] += exterior_mask[i] * lap_ext[i]; }
222 let tissue_phase = solve_poisson(&lap_fov, nx, ny, nz, vsx, vsy, vsz);
223
224 let result: Vec<f64> = tissue_phase.iter().enumerate()
225 .map(|(i, &v)| if mask[i] != 0 { v } else { 0.0 }).collect();
226 (result, mask.to_vec())
227}
228
229fn prepare_laplacian(
235 phase: &[f64], mask: &[u8],
236 nx: usize, ny: usize, nz: usize,
237 vsx: f64, vsy: f64, vsz: f64,
238) -> (Vec<f64>, Vec<u8>, Vec<f64>) {
239 let n_total = nx * ny * nz;
240
241 let lap = wrapped_laplacian_periodic(phase, nx, ny, nz, vsx, vsy, vsz);
243
244 let erode_radius = 3.0 * vsx.min(vsy).min(vsz);
246 let erode_fft = compute_smv_fft(nx, ny, nz, vsx, vsy, vsz, erode_radius);
247
248 let mask_f64: Vec<f64> = mask.iter().map(|&m| m as f64).collect();
249 let mut mask_conv: Vec<Complex64> = mask_f64.iter()
250 .map(|&x| Complex64::new(x, 0.0)).collect();
251 fft3d(&mut mask_conv, nx, ny, nz);
252 for i in 0..n_total { mask_conv[i] *= erode_fft[i]; }
253 ifft3d(&mut mask_conv, nx, ny, nz);
254
255 let delta_thresh = 1.0 - 1e-7_f64.sqrt();
256 let interior_mask: Vec<u8> = mask_conv.iter()
257 .map(|c| if c.re > delta_thresh { 1 } else { 0 }).collect();
258 let exterior_mask: Vec<f64> = mask.iter()
259 .map(|&m| if m == 0 { 1.0 } else { 0.0 }).collect();
260
261 let mut lap_brain = vec![0.0; n_total];
263 for i in 0..n_total {
264 if interior_mask[i] == 1 { lap_brain[i] = lap[i]; }
265 }
266
267 (lap_brain, interior_mask, exterior_mask)
268}
269
270fn compute_smv_fft(
272 nx: usize, ny: usize, nz: usize,
273 vsx: f64, vsy: f64, vsz: f64, radius: f64,
274) -> Vec<f64> {
275 let grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
276 let s_kernel = smv_kernel(&grid, radius);
277 let mut c: Vec<Complex64> = s_kernel.iter()
278 .map(|&x| Complex64::new(x, 0.0)).collect();
279 fft3d(&mut c, nx, ny, nz);
280 c.iter().map(|v| v.re).collect()
281}
282
283fn apply_smv(x: &[f64], s_fft: &[f64], nx: usize, ny: usize, nz: usize) -> Vec<f64> {
285 let n_total = nx * ny * nz;
286 let mut c: Vec<Complex64> = x.iter()
287 .map(|&v| Complex64::new(v, 0.0)).collect();
288 fft3d(&mut c, nx, ny, nz);
289 for i in 0..n_total { c[i] *= s_fft[i]; }
290 ifft3d(&mut c, nx, ny, nz);
291 c.iter().map(|v| v.re).collect()
292}
293
294fn compute_laplacian_eigenvalues(
296 nx: usize, ny: usize, nz: usize,
297 vsx: f64, vsy: f64, vsz: f64,
298) -> Vec<f64> {
299 use std::f64::consts::PI;
300 let n_total = nx * ny * nz;
301 let idx2 = 1.0 / (vsx * vsx);
302 let idy2 = 1.0 / (vsy * vsy);
303 let idz2 = 1.0 / (vsz * vsz);
304 let mut eig = vec![0.0; n_total];
305
306 for k in 0..nz {
307 let fk = if k <= nz / 2 { k as f64 / nz as f64 } else { (k as f64 - nz as f64) / nz as f64 };
308 let lz = 2.0 * ((2.0 * PI * fk).cos() - 1.0) * idz2;
309 for j in 0..ny {
310 let fj = if j <= ny / 2 { j as f64 / ny as f64 } else { (j as f64 - ny as f64) / ny as f64 };
311 let ly = 2.0 * ((2.0 * PI * fj).cos() - 1.0) * idy2;
312 for i in 0..nx {
313 let fi = if i <= nx / 2 { i as f64 / nx as f64 } else { (i as f64 - nx as f64) / nx as f64 };
314 let lx = 2.0 * ((2.0 * PI * fi).cos() - 1.0) * idx2;
315 eig[i + j * nx + k * nx * ny] = lx + ly + lz;
316 }
317 }
318 }
319 eig
320}
321
322fn apply_inv_lap(f: &[f64], eig: &[f64], nx: usize, ny: usize, nz: usize) -> Vec<f64> {
324 let n_total = nx * ny * nz;
325 let mut c: Vec<Complex64> = f.iter()
326 .map(|&x| Complex64::new(x, 0.0)).collect();
327 fft3d(&mut c, nx, ny, nz);
328 for i in 0..n_total {
329 if eig[i].abs() > 1e-20 {
330 c[i] /= eig[i];
331 } else {
332 c[i] = Complex64::new(0.0, 0.0);
333 }
334 }
335 ifft3d(&mut c, nx, ny, nz);
336 c.iter().map(|v| v.re).collect()
337}
338
339fn solve_poisson(
341 f: &[f64], nx: usize, ny: usize, nz: usize,
342 vsx: f64, vsy: f64, vsz: f64,
343) -> Vec<f64> {
344 let eig = compute_laplacian_eigenvalues(nx, ny, nz, vsx, vsy, vsz);
345 apply_inv_lap(f, &eig, nx, ny, nz)
346}
347
348fn cg_solve_masked<F, Op>(
350 b: &[f64],
351 _mask: &[f64],
352 max_iter: usize,
353 tol: f64,
354 callback: &mut F,
355 apply_ata: Op,
356) -> Vec<f64>
357where
358 F: FnMut(usize, usize),
359 Op: Fn(&[f64]) -> Vec<f64>,
360{
361 let n = b.len();
362 let mut x = vec![0.0; n];
363 let mut r = b.to_vec();
364 let mut p = r.clone();
365 let mut rsold: f64 = r.iter().map(|&v| v * v).sum();
366 let b_norm: f64 = b.iter().map(|&v| v * v).sum::<f64>().sqrt();
367
368 if b_norm < 1e-20 { return x; }
369
370 for iter in 0..max_iter {
371 callback(iter + 1, max_iter);
372
373 let ap = apply_ata(&p);
374 let pap: f64 = p.iter().zip(ap.iter()).map(|(&pi, &api)| pi * api).sum();
375
376 if pap.abs() < 1e-20 { break; }
377
378 let alpha = rsold / pap;
379 for i in 0..n {
380 x[i] += alpha * p[i];
381 r[i] -= alpha * ap[i];
382 }
383
384 let rsnew: f64 = r.iter().map(|&v| v * v).sum();
385 if rsnew.sqrt() < tol * b_norm { break; }
386
387 let beta = rsnew / rsold;
388 for i in 0..n { p[i] = r[i] + beta * p[i]; }
389 rsold = rsnew;
390 }
391 x
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::unwrap::laplacian::wrap;
398
399 #[test]
400 fn test_harperella_zero_phase() {
401 let n = 16;
402 let phase = vec![0.0; n * n * n];
403 let mask = vec![1u8; n * n * n];
404 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
405 let params = HarperellaParams { radius: 2.0, max_iter: 10, tol: 1e-6 };
406 let (tissue, _) = harperella(&phase, &mask, &grid, ¶ms, |_, _| {});
407 for &val in tissue.iter() {
408 assert!(val.abs() < 1e-8, "Zero phase should give zero tissue phase, got {}", val);
409 }
410 }
411
412 #[test]
413 fn test_harperella_finite() {
414 let n = 16;
415 let phase: Vec<f64> = (0..n*n*n).map(|i| wrap((i as f64) * 0.1)).collect();
416 let mask = vec![1u8; n * n * n];
417 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
418 let params = HarperellaParams { radius: 2.0, max_iter: 10, tol: 1e-6 };
419 let (tissue, _) = harperella(&phase, &mask, &grid, ¶ms, |_, _| {});
420 for (i, &val) in tissue.iter().enumerate() {
421 assert!(val.is_finite(), "Tissue phase should be finite at index {}", i);
422 }
423 }
424
425 #[test]
426 fn test_iharperella_zero_phase() {
427 let n = 16;
428 let phase = vec![0.0; n * n * n];
429 let mask = vec![1u8; n * n * n];
430 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
431 let params = HarperellaParams { radius: 2.0, max_iter: 10, tol: 1e-6 };
432 let (tissue, _) = iharperella(&phase, &mask, &grid, ¶ms, |_, _| {});
433 for &val in tissue.iter() {
434 assert!(val.abs() < 1e-8, "Zero phase should give zero tissue phase, got {}", val);
435 }
436 }
437
438 #[test]
439 fn test_iharperella_finite() {
440 let n = 16;
441 let phase: Vec<f64> = (0..n*n*n).map(|i| wrap((i as f64) * 0.1)).collect();
442 let mask = vec![1u8; n * n * n];
443 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
444 let params = HarperellaParams { radius: 2.0, max_iter: 10, tol: 1e-6 };
445 let (tissue, _) = iharperella(&phase, &mask, &grid, ¶ms, |_, _| {});
446 for (i, &val) in tissue.iter().enumerate() {
447 assert!(val.is_finite(), "Tissue phase should be finite at index {}", i);
448 }
449 }
450}