1use num_complex::Complex64;
67use crate::Grid;
68use crate::error::{check_len, InputError};
69use crate::fft::{fft3d, ifft3d};
70use crate::inversion::ilsqr::lsqr_matlab;
71use crate::kernels::smv::smv_kernel;
72use crate::utils::padding::{pad3d, unpad3d};
73
74#[derive(Clone, Copy, Debug)]
76pub enum HarperellaInput<'a> {
77 FieldPpm(&'a [f64]),
79 WrappedPhase {
82 phase: &'a [f64],
84 te_s: f64,
86 b0_t: f64,
88 },
89}
90
91impl HarperellaInput<'_> {
92 fn data(&self, mask: &[u8], grid: &Grid) -> Result<(&[f64], Option<f64>), InputError> {
95 let n = grid.n_total();
96 check_len("mask", mask.len(), n)?;
97 match *self {
98 HarperellaInput::FieldPpm(field) => {
99 check_len("field", field.len(), n)?;
100 Ok((field, None))
101 }
102 HarperellaInput::WrappedPhase { phase, te_s, b0_t } => {
103 check_len("phase", phase.len(), n)?;
104 if !(te_s.is_finite() && te_s > 0.0 && b0_t.is_finite() && b0_t > 0.0) {
105 return Err(InputError(format!(
106 "wrapped-phase input needs a positive echo time and field strength, \
107 got TE = {te_s} s, B0 = {b0_t} T"
108 )));
109 }
110 Ok((phase, Some(crate::inversion::rad_per_ppm(te_s, b0_t))))
111 }
112 }
113 }
114}
115
116#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
118#[derive(Clone, Debug, PartialEq)]
119pub struct HarperellaParams {
120 pub radius: f64,
122 pub max_iter: usize,
124 pub tol: f64,
126 pub pad: [usize; 3],
128}
129
130impl Default for HarperellaParams {
131 fn default() -> Self {
132 Self { radius: 10.0, max_iter: 40, tol: 1e-3, pad: [0, 0, 0] }
133 }
134}
135
136#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
138#[derive(Clone, Debug, PartialEq)]
139pub struct IharperellaParams {
140 pub max_iter: usize,
142 pub tol: f64,
144 pub pad: [usize; 3],
146}
147
148impl Default for IharperellaParams {
149 fn default() -> Self {
150 Self { max_iter: 40, tol: 1e-3, pad: [0, 0, 0] }
151 }
152}
153
154struct Work {
156 dims: (usize, usize, usize),
157 vs: (f64, f64, f64),
158 mask: Vec<u8>,
159 k2: Vec<f64>,
160 lap: Vec<f64>,
161 rim: Vec<u8>,
162}
163
164impl Work {
165 fn new(data: &[f64], mask: &[u8], grid: &Grid, pad: [usize; 3], wrapped: bool) -> Self {
166 let (nx, ny, nz) = grid.dims;
167 let dims = (nx + 2 * pad[0], ny + 2 * pad[1], nz + 2 * pad[2]);
168 let vs = grid.voxel_size;
169 let pd = pad_centred(data, grid.dims, dims, pad);
170 let pm = pad_centred(mask, grid.dims, dims, pad);
171 let k2 = k2_kernel(dims, vs);
172 let mut lap = if wrapped {
173 let (sin, cos): (Vec<f64>, Vec<f64>) = pd.iter().map(|p| p.sin_cos()).unzip();
174 let ls = apply_kernel(&sin, &k2, dims);
175 let lc = apply_kernel(&cos, &k2, dims);
176 (0..pd.len()).map(|i| cos[i] * ls[i] - sin[i] * lc[i]).collect()
177 } else {
178 apply_kernel(&pd, &k2, dims)
179 };
180 for (l, &m) in lap.iter_mut().zip(&pm) {
181 if m == 0 { *l = 0.0; }
182 }
183 let rim = sti_rim(&pm, dims);
184 drop_large_rim_values(&mut lap, &rim);
185 Self { dims, vs, mask: pm, k2, lap, rim }
186 }
187
188 fn n(&self) -> usize { self.mask.len() }
189
190 fn finish(&self, lap_fov: &[f64], mask: &[u8], grid: &Grid, pad: [usize; 3], rad_per_ppm: Option<f64>) -> Vec<f64> {
192 let tissue = apply_inv_kernel(lap_fov, &self.k2, self.dims);
193 let tissue = unpad_centred(&tissue, self.dims, grid.dims, pad);
194 tissue.iter().zip(mask)
195 .map(|(&v, &m)| match (m != 0, rad_per_ppm) {
196 (false, _) => 0.0,
197 (true, None) => v,
198 (true, Some(c)) => v / c,
199 })
200 .collect()
201 }
202}
203
204pub fn harperella(
239 input: HarperellaInput,
240 mask: &[u8],
241 grid: &Grid,
242 params: &HarperellaParams,
243 mut progress: impl FnMut(usize, usize),
244) -> Result<(Vec<f64>, Vec<u8>), InputError> {
245 let (data, rad_per_ppm) = input.data(mask, grid)?;
246 let wrapped = rad_per_ppm.is_some();
247 let w = Work::new(data, mask, grid, params.pad, wrapped);
248 let (nx, ny, nz) = w.dims;
249 let (vsx, vsy, vsz) = w.vs;
250 let n = w.n();
251
252 let s_fft = compute_smv_fft(nx, ny, nz, vsx, vsy, vsz, params.radius);
253 let smv_lap = apply_smv(&w.lap, &s_fft, nx, ny, nz);
254
255 let interior = erode(&w.mask, nx, ny, nz, vsx, vsy, vsz, params.radius);
257 let (sum, count) = smv_lap.iter().zip(&interior)
258 .filter(|(_, &i)| i == 1)
259 .fold((0.0, 0usize), |(s, c), (&v, _)| (s + v, c + 1));
260 let delta = if count > 0 { sum / count as f64 } else { 0.0 };
261
262 let ext: Vec<f64> = w.mask.iter().map(|&m| if m == 0 { 1.0 } else { 0.0 }).collect();
264 let b: Vec<Complex64> = smv_lap.iter().map(|&v| Complex64::new(delta - v, 0.0)).collect();
265 let mut it = 0usize;
266 let max_iter = params.max_iter;
267 let (x, _) = lsqr_matlab(
268 |v| {
269 it += 1;
270 progress(it.min(max_iter), max_iter);
271 let ev: Vec<f64> = (0..n).map(|i| ext[i] * v[i].re).collect();
272 to_complex(apply_smv(&ev, &s_fft, nx, ny, nz))
273 },
274 |u| {
275 let re: Vec<f64> = u.iter().map(|c| c.re).collect();
276 let su = apply_smv(&re, &s_fft, nx, ny, nz);
277 (0..n).map(|i| Complex64::new(ext[i] * su[i], 0.0)).collect()
278 },
279 &b,
280 params.tol,
281 max_iter,
282 );
283
284 let lap_fov: Vec<f64> = (0..n).map(|i| w.lap[i] + ext[i] * x[i].re).collect();
285 Ok((w.finish(&lap_fov, mask, grid, params.pad, rad_per_ppm), mask.to_vec()))
286}
287
288pub fn iharperella(
312 input: HarperellaInput,
313 mask: &[u8],
314 grid: &Grid,
315 params: &IharperellaParams,
316 mut progress: impl FnMut(usize, usize),
317) -> Result<(Vec<f64>, Vec<u8>), InputError> {
318 let (data, rad_per_ppm) = input.data(mask, grid)?;
319 let w = Work::new(data, mask, grid, params.pad, rad_per_ppm.is_some());
320 let dims = w.dims;
321 let n = w.n();
322 let ext: Vec<f64> = w.mask.iter().map(|&m| if m == 0 { 1.0 } else { 0.0 }).collect();
323 let wt: Vec<f64> = (0..n).map(|i| w.mask[i] as f64 + w.rim[i] as f64).collect();
324
325 let inv_lap = apply_inv_kernel(&w.lap, &w.k2, dims);
326 let b: Vec<Complex64> = (0..n).map(|i| Complex64::new(wt[i] * inv_lap[i], 0.0)).collect();
327 let mut it = 0usize;
328 let max_iter = params.max_iter;
329 let (x, _) = lsqr_matlab(
330 |v| {
331 it += 1;
332 progress(it.min(max_iter), max_iter);
333 let ev: Vec<f64> = (0..n).map(|i| ext[i] * v[i].re).collect();
334 let f = apply_inv_kernel(&ev, &w.k2, dims);
335 (0..n).map(|i| Complex64::new(wt[i] * f[i], 0.0)).collect()
336 },
337 |u| {
338 let wu: Vec<f64> = (0..n).map(|i| wt[i] * u[i].re).collect();
339 let f = apply_inv_kernel(&wu, &w.k2, dims);
340 (0..n).map(|i| Complex64::new(ext[i] * f[i], 0.0)).collect()
341 },
342 &b,
343 params.tol,
344 max_iter,
345 );
346
347 let lap_fov: Vec<f64> = (0..n).map(|i| w.lap[i] - x[i].re).collect();
348 Ok((w.finish(&lap_fov, mask, grid, params.pad, rad_per_ppm), mask.to_vec()))
349}
350
351fn to_complex(v: Vec<f64>) -> Vec<Complex64> {
356 v.into_iter().map(|x| Complex64::new(x, 0.0)).collect()
357}
358
359fn pad_centred<T: Copy + Default>(
361 x: &[T], dims: (usize, usize, usize), new: (usize, usize, usize), pad: [usize; 3],
362) -> Vec<T> {
363 if pad == [0, 0, 0] { return x.to_vec(); }
364 let corner = pad3d(x, dims, new);
365 shift(&corner, new, [pad[0] as isize, pad[1] as isize, pad[2] as isize])
366}
367
368fn unpad_centred(x: &[f64], padded: (usize, usize, usize), dims: (usize, usize, usize), pad: [usize; 3]) -> Vec<f64> {
370 if pad == [0, 0, 0] { return x.to_vec(); }
371 let back = shift(x, padded, [-(pad[0] as isize), -(pad[1] as isize), -(pad[2] as isize)]);
372 unpad3d(&back, padded, dims)
373}
374
375fn shift<T: Copy + Default>(x: &[T], dims: (usize, usize, usize), s: [isize; 3]) -> Vec<T> {
377 let (nx, ny, nz) = dims;
378 let mut out = vec![T::default(); x.len()];
379 let w = |i: usize, s: isize, n: usize| ((i as isize + s).rem_euclid(n as isize)) as usize;
380 for k in 0..nz {
381 let kk = w(k, s[2], nz);
382 for j in 0..ny {
383 let jj = w(j, s[1], ny);
384 for i in 0..nx {
385 out[w(i, s[0], nx) + jj * nx + kk * nx * ny] = x[i + j * nx + k * nx * ny];
386 }
387 }
388 }
389 out
390}
391
392fn k2_kernel(dims: (usize, usize, usize), vs: (f64, f64, f64)) -> Vec<f64> {
394 let (nx, ny, nz) = dims;
395 let f = |i: usize, n: usize, v: f64| {
396 let k = if i <= n / 2 { i as f64 } else { i as f64 - n as f64 };
397 let k = k / (n as f64 * v);
398 k * k
399 };
400 let mut out = vec![0.0; nx * ny * nz];
401 for k in 0..nz {
402 let kz = f(k, nz, vs.2);
403 for j in 0..ny {
404 let ky = f(j, ny, vs.1);
405 for i in 0..nx {
406 out[i + j * nx + k * nx * ny] = f(i, nx, vs.0) + ky + kz;
407 }
408 }
409 }
410 out
411}
412
413fn apply_kernel(x: &[f64], k: &[f64], dims: (usize, usize, usize)) -> Vec<f64> {
415 let (nx, ny, nz) = dims;
416 let mut c: Vec<Complex64> = x.iter().map(|&v| Complex64::new(v, 0.0)).collect();
417 fft3d(&mut c, nx, ny, nz);
418 for (ci, &ki) in c.iter_mut().zip(k) { *ci *= ki; }
419 ifft3d(&mut c, nx, ny, nz);
420 c.iter().map(|v| v.re).collect()
421}
422
423fn apply_inv_kernel(x: &[f64], k: &[f64], dims: (usize, usize, usize)) -> Vec<f64> {
425 let (nx, ny, nz) = dims;
426 let mut c: Vec<Complex64> = x.iter().map(|&v| Complex64::new(v, 0.0)).collect();
427 fft3d(&mut c, nx, ny, nz);
428 for (ci, &ki) in c.iter_mut().zip(k) {
429 *ci = if ki > 0.0 { *ci / ki } else { Complex64::new(0.0, 0.0) };
430 }
431 ifft3d(&mut c, nx, ny, nz);
432 c.iter().map(|v| v.re).collect()
433}
434
435fn sti_rim(mask: &[u8], dims: (usize, usize, usize)) -> Vec<u8> {
439 let (nx, ny, nz) = dims;
440 let mut offs = Vec::new();
441 for dz in -3i64..=3 {
442 for dy in -3i64..=3 {
443 for dx in -3i64..=3 {
444 let r2 = dx * dx + dy * dy + dz * dz;
445 if (1..=10).contains(&r2) { offs.push((dx, dy, dz)); }
446 }
447 }
448 }
449 debug_assert_eq!(offs.len(), 146);
450 let total = offs.len() as f64;
451 let w = |i: usize, d: i64, n: usize| (i as i64 + d).rem_euclid(n as i64) as usize;
452 let mut rim = vec![0u8; mask.len()];
453 for k in 0..nz {
454 for j in 0..ny {
455 for i in 0..nx {
456 let idx = i + j * nx + k * nx * ny;
457 if mask[idx] == 0 { continue; }
458 let inside = offs.iter()
459 .filter(|&&(dx, dy, dz)| mask[w(i, dx, nx) + w(j, dy, ny) * nx + w(k, dz, nz) * nx * ny] != 0)
460 .count();
461 rim[idx] = u8::from((inside as f64) / total < 0.99);
462 }
463 }
464 }
465 rim
466}
467
468fn drop_large_rim_values(lap: &mut [f64], rim: &[u8]) {
471 let mut mags: Vec<f64> = lap.iter().zip(rim).filter(|(_, &r)| r != 0).map(|(l, _)| l.abs()).collect();
472 if mags.is_empty() { return; }
473 let k = ((0.8 * mags.len() as f64).round() as usize).clamp(1, mags.len()) - 1;
474 let thr = *mags.select_nth_unstable_by(k, |a, b| a.total_cmp(b)).1;
475 for (l, &r) in lap.iter_mut().zip(rim) {
476 if r != 0 && l.abs() > thr { *l = 0.0; }
477 }
478}
479
480#[allow(clippy::too_many_arguments)]
486fn erode(
487 mask: &[u8],
488 nx: usize, ny: usize, nz: usize,
489 vsx: f64, vsy: f64, vsz: f64,
490 radius: f64,
491) -> Vec<u8> {
492 let erode_fft = compute_smv_fft(nx, ny, nz, vsx, vsy, vsz, radius * (1.0 + 1e-6));
493 let mut mask_conv: Vec<Complex64> = mask.iter()
494 .map(|&m| Complex64::new(m as f64, 0.0)).collect();
495 fft3d(&mut mask_conv, nx, ny, nz);
496 for (c, &e) in mask_conv.iter_mut().zip(&erode_fft) { *c *= e; }
497 ifft3d(&mut mask_conv, nx, ny, nz);
498 let thresh = 1.0 - 1e-7_f64.sqrt();
499 mask_conv.iter().map(|c| u8::from(c.re > thresh)).collect()
500}
501
502fn compute_smv_fft(
504 nx: usize, ny: usize, nz: usize,
505 vsx: f64, vsy: f64, vsz: f64, radius: f64,
506) -> Vec<f64> {
507 let grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
508 let s_kernel = smv_kernel(&grid, radius);
509 let mut c: Vec<Complex64> = s_kernel.iter()
510 .map(|&x| Complex64::new(x, 0.0)).collect();
511 fft3d(&mut c, nx, ny, nz);
512 c.iter().map(|v| v.re).collect()
513}
514
515fn apply_smv(x: &[f64], s_fft: &[f64], nx: usize, ny: usize, nz: usize) -> Vec<f64> {
517 apply_kernel(x, s_fft, (nx, ny, nz))
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 use crate::unwrap::laplacian::wrap;
524
525 const N: usize = 16;
526
527 fn grid() -> Grid {
528 Grid::new(N, N, N, 1.0, 1.0, 1.0)
529 }
530
531 fn sphere_mask() -> Vec<u8> {
533 let c = (N as f64 - 1.0) / 2.0;
534 (0..N * N * N)
535 .map(|idx| {
536 let (i, j, k) = (idx % N, (idx / N) % N, idx / (N * N));
537 let r2 = (i as f64 - c).powi(2) + (j as f64 - c).powi(2) + (k as f64 - c).powi(2);
538 u8::from(r2 <= 36.0)
539 })
540 .collect()
541 }
542
543 fn field_ppm(scale: f64) -> Vec<f64> {
546 let c = (N as f64 - 1.0) / 2.0;
547 (0..N * N * N)
548 .map(|idx| {
549 let (x, y, z) = ((idx % N) as f64 - c, ((idx / N) % N) as f64 - c, (idx / (N * N)) as f64 - c);
550 let blob = 1.5 * (-(x * x + y * y + z * z) / 16.0).exp();
551 let bg = 0.3 * (2.0 * std::f64::consts::PI * x / N as f64).sin();
552 scale * (blob + bg)
553 })
554 .collect()
555 }
556
557 fn max_abs_diff(a: &[f64], b: &[f64]) -> f64 {
558 a.iter().zip(b).map(|(x, y)| (x - y).abs()).fold(0.0, f64::max)
559 }
560
561 fn max_abs(a: &[f64]) -> f64 {
562 a.iter().fold(0.0f64, |m, v| m.max(v.abs()))
563 }
564
565 fn run_h(input: HarperellaInput, mask: &[u8]) -> Vec<f64> {
566 let p = HarperellaParams { radius: 3.0, max_iter: 30, ..Default::default() };
567 harperella(input, mask, &grid(), &p, |_, _| {}).unwrap().0
568 }
569
570 fn run_ih(input: HarperellaInput, mask: &[u8]) -> Vec<f64> {
571 let p = IharperellaParams { max_iter: 30, ..Default::default() };
572 iharperella(input, mask, &grid(), &p, |_, _| {}).unwrap().0
573 }
574
575 #[test]
576 fn zero_field_gives_zero() {
577 let zero = vec![0.0; N * N * N];
578 let mask = sphere_mask();
579 for out in [run_h(HarperellaInput::FieldPpm(&zero), &mask), run_ih(HarperellaInput::FieldPpm(&zero), &mask)] {
580 assert!(max_abs(&out) < 1e-12);
581 }
582 }
583
584 #[test]
585 fn wrapped_phase_is_finite() {
586 let phase: Vec<f64> = (0..N * N * N).map(|i| wrap((i as f64) * 0.1)).collect();
587 let mask = vec![1u8; N * N * N];
588 let input = HarperellaInput::WrappedPhase { phase: &phase, te_s: 0.02, b0_t: 3.0 };
589 assert!(run_h(input, &mask).iter().all(|v| v.is_finite()));
590 assert!(run_ih(input, &mask).iter().all(|v| v.is_finite()));
591 }
592
593 #[test]
595 fn spectral_laplacian_inverts() {
596 let dims = (N, N, 12);
597 let k2 = k2_kernel(dims, (1.0, 0.9, 1.5));
598 let x: Vec<f64> = (0..N * N * 12).map(|i| ((i * 37 % 101) as f64).sin()).collect();
599 let mean = x.iter().sum::<f64>() / x.len() as f64;
600 let back = apply_inv_kernel(&apply_kernel(&x, &k2, dims), &k2, dims);
601 let centred: Vec<f64> = x.iter().map(|v| v - mean).collect();
602 assert!(max_abs_diff(&back, ¢red) < 1e-10);
603 let wave: Vec<f64> = (0..N * N * 12).map(|i| (2.0 * std::f64::consts::PI * 2.0 * (i % N) as f64 / N as f64).cos()).collect();
605 let lw = apply_kernel(&wave, &k2, dims);
606 let expect: Vec<f64> = wave.iter().map(|v| v * (2.0f64 / 16.0).powi(2)).collect();
607 assert!(max_abs_diff(&lw, &expect) < 1e-12);
608 }
609
610 #[test]
612 fn rim_needs_two_outside_neighbours() {
613 let dims = (N, N, N);
614 let mut mask = vec![1u8; N * N * N];
615 let at = |i: usize, j: usize, k: usize| i + j * N + k * N * N;
616 mask[at(3, 8, 8)] = 0;
617 let rim = sti_rim(&mask, dims);
618 assert_eq!(rim.iter().filter(|&&r| r != 0).count(), 0, "one hole: 145/146 > 0.99");
619 mask[at(4, 8, 8)] = 0;
620 let rim = sti_rim(&mask, dims);
621 assert!(rim[at(6, 8, 8)] == 1 && rim[at(1, 8, 8)] == 1 && rim[at(8, 8, 8)] == 0);
623 }
624
625 #[test]
627 fn rim_drops_the_largest_fifth() {
628 for n in [1usize, 4, 5, 9, 13] {
629 let mut lap: Vec<f64> = (0..n + 3).map(|i| if i % 2 == 0 { i as f64 } else { -(i as f64) }).collect();
630 let mut rim = vec![1u8; n];
631 rim.extend([0u8; 3]);
632 let outside_before: Vec<f64> = lap[n..].to_vec();
633 drop_large_rim_values(&mut lap, &rim);
634 let kept = lap[..n].iter().filter(|&&v| v != 0.0).count() + usize::from(n > 0); assert_eq!(kept, ((0.8 * n as f64).round() as usize).max(1), "n = {n}");
636 assert_eq!(&lap[n..], &outside_before[..], "outside the rim is untouched");
637 }
638 }
639
640 #[test]
641 fn padding_round_trips() {
642 let dims = (5, 4, 3);
643 let x: Vec<f64> = (0..60).map(|i| i as f64).collect();
644 let pad = [2, 1, 3];
645 let new = (9, 6, 9);
646 let p = pad_centred(&x, dims, new, pad);
647 assert_eq!(p[2 + new.0 + 3 * new.0 * new.1], x[0]);
648 assert_eq!(unpad_centred(&p, new, dims, pad), x);
649 }
650
651 #[test]
654 fn wrapped_phase_agrees_with_field_ppm() {
655 let field = field_ppm(1.0);
656 let mask = sphere_mask();
657 let te = 0.007;
658 let b0 = 3.0;
659 let c = crate::inversion::rad_per_ppm(te, b0);
660 let phase: Vec<f64> = field.iter().map(|f| wrap(f * c)).collect();
661 assert!(max_abs(&field) * c > 2.0 * std::f64::consts::PI, "test field does not wrap");
662 let input = HarperellaInput::WrappedPhase { phase: &phase, te_s: te, b0_t: b0 };
663 for (from_phase, from_field, name) in [
664 (run_ih(input, &mask), run_ih(HarperellaInput::FieldPpm(&field), &mask), "iHARPERELLA"),
665 (run_h(input, &mask), run_h(HarperellaInput::FieldPpm(&field), &mask), "HARPERELLA"),
666 ] {
667 let d = max_abs_diff(&from_phase, &from_field) / max_abs(&from_field);
668 assert!(d < 0.1, "{name}: phase vs field differ by {d:e}");
669 }
670 }
671
672 #[test]
675 fn field_input_is_scale_equivariant() {
676 let mask = sphere_mask();
677 let (f1, f2) = (field_ppm(1.0), field_ppm(16.0));
678 for (run, name) in [(run_h as fn(HarperellaInput, &[u8]) -> Vec<f64>, "HARPERELLA"), (run_ih, "iHARPERELLA")] {
679 let a: Vec<f64> = run(HarperellaInput::FieldPpm(&f1), &mask).iter().map(|v| v * 16.0).collect();
680 let b = run(HarperellaInput::FieldPpm(&f2), &mask);
681 let d = max_abs_diff(&a, &b) / max_abs(&b);
682 assert!(d < 1e-12, "{name}: not scale-equivariant ({d:e})");
683 }
684 }
685
686 #[test]
689 fn padding_is_applied() {
690 let mask = sphere_mask();
691 let field = field_ppm(1.0);
692 let p0 = IharperellaParams { max_iter: 10, ..Default::default() };
693 let p4 = IharperellaParams { pad: [4, 4, 4], ..p0.clone() };
694 let a = iharperella(HarperellaInput::FieldPpm(&field), &mask, &grid(), &p0, |_, _| {}).unwrap().0;
695 let b = iharperella(HarperellaInput::FieldPpm(&field), &mask, &grid(), &p4, |_, _| {}).unwrap().0;
696 assert_eq!(b.len(), N * N * N);
697 assert!(max_abs_diff(&a, &b) > 1e-6);
698 assert!(b.iter().zip(&mask).all(|(&v, &m)| m != 0 || v == 0.0));
699 }
700
701 #[test]
704 fn erosion_ignores_voxel_size_rounding() {
705 let mask = sphere_mask();
706 for radius in [2.0, 3.0] {
707 let exact = erode(&mask, N, N, N, 1.0, 1.0, 1.0, radius);
708 let rounded = erode(&mask, N, N, N, 1.0, 1.0, 0.999_999_94, radius * 0.999_999_94);
709 assert!(exact.contains(&1), "radius {radius}: nothing left");
710 assert_eq!(exact, rounded, "radius {radius}");
711 }
712 }
713
714 #[test]
718 fn empty_interior_gives_zero_delta() {
719 let mask = sphere_mask();
720 assert!(erode(&mask, N, N, N, 1.0, 1.0, 1.0, 6.5).iter().all(|&v| v == 0));
721 assert!(erode(&mask, N, N, N, 1.0, 1.0, 1.0, 3.0).contains(&1));
722 let field = field_ppm(1.0);
723 let p = HarperellaParams { radius: 6.5, max_iter: 30, ..Default::default() };
724 let out = harperella(HarperellaInput::FieldPpm(&field), &mask, &grid(), &p, |_, _| {}).unwrap().0;
725 assert!(out.iter().all(|v| v.is_finite()) && max_abs(&out) > 1e-3);
726 }
727
728 #[test]
729 fn bad_input_is_an_error_not_a_panic() {
730 let n = N * N * N;
731 let (field, mask) = (vec![0.0; n], vec![1u8; n]);
732 let (g, hp, ip) = (grid(), HarperellaParams::default(), IharperellaParams::default());
733 let short = vec![0.0; n - 1];
734 assert!(harperella(HarperellaInput::FieldPpm(&short), &mask, &g, &hp, |_, _| {}).is_err());
735 assert!(iharperella(HarperellaInput::FieldPpm(&field), &mask[1..], &g, &ip, |_, _| {}).is_err());
736 for (te_s, b0_t) in [(0.0, 3.0), (0.02, 0.0), (f64::NAN, 3.0), (-0.01, 3.0)] {
737 let input = HarperellaInput::WrappedPhase { phase: &field, te_s, b0_t };
738 let err = harperella(input, &mask, &g, &hp, |_, _| {}).unwrap_err();
739 assert!(err.to_string().contains("positive echo time"), "{err}");
740 assert!(iharperella(input, &mask, &g, &ip, |_, _| {}).is_err());
741 }
742 }
743}