1#[cfg(feature = "parallel")]
168use rayon::prelude::*;
169
170fn erode_mask(mask: &[u8], nx: usize, ny: usize, nz: usize, skip_outside: bool) -> Vec<u8> {
177 let edge = skip_outside;
178 let nxy = nx * ny;
179 let mut out = vec![0u8; mask.len()];
180 for k in 0..nz {
181 for j in 0..ny {
182 for i in 0..nx {
183 let l = i + j * nx + k * nxy;
184 out[l] = u8::from(
185 mask[l] != 0
186 && (if i == 0 { edge } else { mask[l - 1] != 0 })
187 && (if i + 1 == nx { edge } else { mask[l + 1] != 0 })
188 && (if j == 0 { edge } else { mask[l - nx] != 0 })
189 && (if j + 1 == ny { edge } else { mask[l + nx] != 0 })
190 && (if k == 0 { edge } else { mask[l - nxy] != 0 })
191 && (if k + 1 == nz { edge } else { mask[l + nxy] != 0 }),
192 );
193 }
194 }
195 }
196 out
197}
198
199#[allow(clippy::too_many_arguments)] fn symdiv_rows_f32(
210 divx: &mut [f32], divy: &mut [f32], divz: &mut [f32],
211 sxx: &[f32], sxy: &[f32], sxz: &[f32],
212 syy: &[f32], syz: &[f32], szz: &[f32],
213 grid: &crate::Grid,
214) {
215 let (nx, ny, _) = grid.dims;
216 let (r1, r2, r3) = (1.0 / grid.vsx() as f32, 1.0 / grid.vsy() as f32, 1.0 / grid.vsz() as f32);
217 let nxy = nx * ny;
218 let d = |a: &[f32], l: usize, has_prev: bool, step: usize| a[l] - if has_prev { a[l - step] } else { 0.0 };
220 maybe_par_chunks_mut!(divx, nxy)
221 .zip(maybe_par_chunks_mut!(divy, nxy))
222 .zip(maybe_par_chunks_mut!(divz, nxy))
223 .enumerate()
224 .for_each(|(k, ((dxs, dys), dzs))| {
225 for j in 0..ny {
226 for i in 0..nx {
227 let local = i + j * nx;
228 let l = local + k * nxy;
229 let (bi, bj, bk) = (i > 0, j > 0, k > 0);
230 dxs[local] = (d(sxx, l, bi, 1) + d(sxy, l, bj, nx) + d(sxz, l, bk, nxy)) * r1;
231 dys[local] = (d(sxy, l, bi, 1) + d(syy, l, bj, nx) + d(syz, l, bk, nxy)) * r2;
232 dzs[local] = (d(sxz, l, bi, 1) + d(syz, l, bj, nx) + d(szz, l, bk, nxy)) * r3;
233 }
234 }
235 });
236}
237
238pub const TGV_GAMMA_MHZ_PER_T: f64 = 42.5781;
241
242pub fn rad_per_ppm(te_s: f64, b0_t: f64) -> f64 {
245 2.0 * std::f64::consts::PI * TGV_GAMMA_MHZ_PER_T * b0_t * te_s
246}
247
248#[derive(Clone, Copy, Debug)]
251pub enum TgvInput<'a> {
252 FieldPpm {
256 field: &'a [f64],
258 echo_times: &'a [f64],
260 b0_t: f64,
262 },
263 WrappedPhase {
266 phase: &'a [f64],
268 te_s: f64,
270 b0_t: f64,
272 },
273}
274
275impl TgvInput<'_> {
276 fn data(&self) -> &[f64] {
277 match self {
278 TgvInput::FieldPpm { field, .. } => field,
279 TgvInput::WrappedPhase { phase, .. } => phase,
280 }
281 }
282
283 fn te_b0(&self, te: crate::EchoTime) -> Result<(f64, f64), String> {
285 match *self {
286 TgvInput::FieldPpm { echo_times, b0_t, .. } => Ok((te.resolve(echo_times)?, b0_t)),
287 TgvInput::WrappedPhase { te_s, b0_t, .. } => Ok((te_s, b0_t)),
288 }
289 }
290}
291
292#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
294#[derive(Clone, Debug)]
295pub struct TgvParams {
296 pub alpha1: f32,
298 pub alpha0: f32,
300 pub iterations: Option<usize>,
303 pub erosions: usize,
305 pub step_size: f32,
307 pub te: crate::EchoTime,
314 pub reference_divergence: bool,
320 pub reference_edge_erosion: bool,
329 pub reference_output_box: bool,
336}
337
338impl Default for TgvParams {
339 fn default() -> Self {
340 let (alpha0, alpha1) = get_default_alpha(2.0);
343 Self {
344 alpha1,
345 alpha0,
346 iterations: None,
347 erosions: 3,
348 step_size: 3.0,
349 te: crate::EchoTime::First,
350 reference_divergence: false,
351 reference_edge_erosion: false,
352 reference_output_box: false,
353 }
354 }
355}
356
357pub fn get_default_alpha(regularization: f32) -> (f32, f32) {
361 let r = regularization - 1.0;
362 ((0.001 + 0.001 * r).max(0.0), (0.001 + 0.002 * r).max(0.0))
363}
364
365pub fn get_default_iterations(res: (f32, f32, f32), step_size: f32) -> usize {
369 let prod_res = res.0 * res.1 * res.2;
370 let it = (1000.0_f32).max(3200.0 / prod_res.powf(0.42)) / step_size.powf(0.6);
371 it.round() as usize
372}
373
374#[derive(Clone, Debug)]
376struct BoundingBox {
377 i_min: usize,
378 i_max: usize,
379 j_min: usize,
380 j_max: usize,
381 k_min: usize,
382 k_max: usize,
383}
384
385impl BoundingBox {
386 fn from_mask(mask: &[u8], nx: usize, ny: usize, nz: usize, padding: usize) -> Self {
388 let mut i_min = nx;
389 let mut i_max = 0;
390 let mut j_min = ny;
391 let mut j_max = 0;
392 let mut k_min = nz;
393 let mut k_max = 0;
394
395 for k in 0..nz {
396 for j in 0..ny {
397 for i in 0..nx {
398 if mask[i + j * nx + k * nx * ny] != 0 {
399 i_min = i_min.min(i);
400 i_max = i_max.max(i);
401 j_min = j_min.min(j);
402 j_max = j_max.max(j);
403 k_min = k_min.min(k);
404 k_max = k_max.max(k);
405 }
406 }
407 }
408 }
409
410 let i_min = i_min.saturating_sub(padding);
412 let j_min = j_min.saturating_sub(padding);
413 let k_min = k_min.saturating_sub(padding);
414 let i_max = (i_max + padding + 1).min(nx);
415 let j_max = (j_max + padding + 1).min(ny);
416 let k_max = (k_max + padding + 1).min(nz);
417
418 Self { i_min, i_max, j_min, j_max, k_min, k_max }
419 }
420
421 fn dims(&self) -> (usize, usize, usize) {
422 (self.i_max - self.i_min, self.j_max - self.j_min, self.k_max - self.k_min)
423 }
424
425 fn total(&self) -> usize {
426 let (bx, by, bz) = self.dims();
427 bx * by * bz
428 }
429}
430
431fn extract_subvolume<T: Copy + Default>(
433 full: &[T],
434 bbox: &BoundingBox,
435 nx: usize, ny: usize, _nz: usize,
436) -> Vec<T> {
437 let (bx, by, bz) = bbox.dims();
438 let mut sub = vec![T::default(); bx * by * bz];
439
440 for k in 0..bz {
441 for j in 0..by {
442 for i in 0..bx {
443 let full_idx = (bbox.i_min + i) + (bbox.j_min + j) * nx + (bbox.k_min + k) * nx * ny;
444 let sub_idx = i + j * bx + k * bx * by;
445 sub[sub_idx] = full[full_idx];
446 }
447 }
448 }
449 sub
450}
451
452fn insert_subvolume<T: Copy>(
454 full: &mut [T],
455 sub: &[T],
456 bbox: &BoundingBox,
457 nx: usize, ny: usize, _nz: usize,
458) {
459 let (bx, by, bz) = bbox.dims();
460
461 for k in 0..bz {
462 for j in 0..by {
463 for i in 0..bx {
464 let full_idx = (bbox.i_min + i) + (bbox.j_min + j) * nx + (bbox.k_min + k) * nx * ny;
465 let sub_idx = i + j * bx + k * bx * by;
466 full[full_idx] = sub[sub_idx];
467 }
468 }
469 }
470}
471
472pub fn compute_dipole_stencil(
474 res: (f32, f32, f32),
475 b0_dir: (f32, f32, f32),
476) -> [[[f32; 3]; 3]; 3] {
477 let (dx, dy, dz) = res;
478 let (bx, by, bz) = b0_dir;
479
480 let b_norm = (bx * bx + by * by + bz * bz).sqrt();
482 let bx = bx / b_norm;
483 let by = by / b_norm;
484 let bz = bz / b_norm;
485
486 let mut stencil = [[[0.0f32; 3]; 3]; 3];
487
488 let hx2 = 1.0 / (dx * dx);
489 let hy2 = 1.0 / (dy * dy);
490 let hz2 = 1.0 / (dz * dz);
491 let factor = 1.0 / 3.0;
492
493 stencil[1][1][1] = -2.0 * (hx2 + hy2 + hz2) * factor
495 + 2.0 * (bx * bx * hx2 + by * by * hy2 + bz * bz * hz2);
496
497 stencil[0][1][1] = hx2 * factor - bx * bx * hx2;
499 stencil[2][1][1] = hx2 * factor - bx * bx * hx2;
500
501 stencil[1][0][1] = hy2 * factor - by * by * hy2;
503 stencil[1][2][1] = hy2 * factor - by * by * hy2;
504
505 stencil[1][1][0] = hz2 * factor - bz * bz * hz2;
507 stencil[1][1][2] = hz2 * factor - bz * bz * hz2;
508
509 let hxy = 1.0 / (dx * dy);
511 let hxz = 1.0 / (dx * dz);
512 let hyz = 1.0 / (dy * dz);
513
514 let xy_factor = -bx * by * hxy;
515 stencil[0][0][1] = xy_factor;
516 stencil[2][2][1] = xy_factor;
517 stencil[0][2][1] = -xy_factor;
518 stencil[2][0][1] = -xy_factor;
519
520 let xz_factor = -bx * bz * hxz;
521 stencil[0][1][0] = xz_factor;
522 stencil[2][1][2] = xz_factor;
523 stencil[0][1][2] = -xz_factor;
524 stencil[2][1][0] = -xz_factor;
525
526 let yz_factor = -by * bz * hyz;
527 stencil[1][0][0] = yz_factor;
528 stencil[1][2][2] = yz_factor;
529 stencil[1][0][2] = -yz_factor;
530 stencil[1][2][0] = -yz_factor;
531
532 stencil
533}
534
535pub fn compute_oblique_stencil(
548 res: (f32, f32, f32),
549 b0_dir: (f32, f32, f32),
550) -> [[[f32; 3]; 3]; 3] {
551 let n: usize = 64;
552 let singularity_cutout = 4.0_f64;
553 let mid = n / 2; let n2 = n * n;
555 let n3 = n * n * n;
556
557 let (dx, dy, dz) = (res.0 as f64, res.1 as f64, res.2 as f64);
558
559 let b_norm = ((b0_dir.0 * b0_dir.0 + b0_dir.1 * b0_dir.1 + b0_dir.2 * b0_dir.2) as f64).sqrt();
561 let bdir = (b0_dir.0 as f64 / b_norm, b0_dir.1 as f64 / b_norm, b0_dir.2 as f64 / b_norm);
562
563 let mut d = vec![f64::NAN; n3];
565 let mut d_mask = vec![false; n3];
566
567 for k in 0..n {
568 for j in 0..n {
569 for i in 0..n {
570 let x = i as f64 - mid as f64;
571 let y = j as f64 - mid as f64;
572 let z = k as f64 - mid as f64;
573 let r = (x * x + y * y + z * z).sqrt();
574
575 let idx = i + j * n + k * n2;
576 if r < singularity_cutout {
577 } else {
579 let xz = (bdir.0 * x + bdir.1 * y + bdir.2 * z) / r;
580 let kappa = (3.0 * xz * xz - 1.0) / (4.0 * std::f64::consts::PI * r * r * r);
581 d[idx] = kappa;
582 d_mask[idx] = true;
583 }
584 }
585 }
586 }
587
588 let coord2_sq: Vec<f64> = (0..n)
590 .map(|k| {
591 let v = 2.0 * (std::f64::consts::PI * (k as f64 + 1.0) / (2.0 * (n as f64 + 1.0))).sin();
592 v * v
593 })
594 .collect();
595
596 let mut coord2_grid = vec![0.0_f64; n3];
598 for k in 0..n {
599 for j in 0..n {
600 for i in 0..n {
601 coord2_grid[i + j * n + k * n2] = coord2_sq[i] + coord2_sq[j] + coord2_sq[k];
602 }
603 }
604 }
605
606 let dst = rustdct::DctPlanner::<f64>::new().plan_dst1(n);
607 let idst_scale = (2.0 / (n as f64 + 1.0)).powi(3);
610
611 let mut stencil_positions: Vec<(i32, i32, i32)> = Vec::with_capacity(26);
613 for dk in -1..=1_i32 {
614 for dj in -1..=1_i32 {
615 for di in -1..=1_i32 {
616 if di == 0 && dj == 0 && dk == 0 {
617 continue;
618 }
619 stencil_positions.push((di, dj, dk));
620 }
621 }
622 }
623
624 let valid_indices: Vec<usize> = d_mask
626 .iter()
627 .enumerate()
628 .filter(|(_, &m)| m)
629 .map(|(idx, _)| idx)
630 .collect();
631
632 let num_pairs = 13;
636 let sin_table = dst_sin_table(n);
637 let pairs: Vec<usize> = (0..num_pairs).collect();
638 let a_rows: Vec<Vec<f64>> = maybe_par_iter!(pairs).map(|&p| {
639 let (di, dj, dk) = stencil_positions[p];
640
641 let at = |o: i32| (mid as i32 + o) as usize;
644 let points = [
645 ((at(di), at(dj), at(dk)), 1.0),
646 ((at(-di), at(-dj), at(-dk)), 1.0),
647 ((mid, mid, mid), -2.0),
648 ];
649 let mut fdelta = vec![0.0_f64; n3];
650 for &((a, b, c), w) in &points {
651 for k in 0..n {
652 let sk = w * sin_table[c][k];
653 for j in 0..n {
654 let sjk = sk * sin_table[b][j];
655 let base = j * n + k * n2;
656 for (i, v) in fdelta[base..base + n].iter_mut().enumerate() {
657 *v += sjk * sin_table[a][i];
658 }
659 }
660 }
661 }
662
663 for idx in 0..n3 {
665 fdelta[idx] /= -coord2_grid[idx];
666 }
667
668 let vdelta = dst3d(&fdelta, n, dst.as_ref());
670
671 valid_indices.iter().map(|&idx| vdelta[idx] * idst_scale).collect()
673 }).collect();
674
675 let d_valid: Vec<f64> = valid_indices.iter().map(|&idx| d[idx]).collect();
677
678 let mut g = vec![vec![0.0_f64; num_pairs]; num_pairs];
681 for i in 0..num_pairs {
682 for j in 0..=i {
683 let dot: f64 = a_rows[i]
684 .iter()
685 .zip(a_rows[j].iter())
686 .map(|(&a, &b)| a * b)
687 .sum();
688 g[i][j] = dot;
689 g[j][i] = dot;
690 }
691 }
692
693 let mut h = vec![0.0_f64; num_pairs];
694 for i in 0..num_pairs {
695 h[i] = a_rows[i]
696 .iter()
697 .zip(d_valid.iter())
698 .map(|(&a, &b)| a * b)
699 .sum();
700 }
701
702 let y = solve_symmetric_pseudoinverse(&g, &h);
705
706 let mut result = [[[0.0f32; 3]; 3]; 3];
709 for p in 0..num_pairs {
710 let coeff = y[p] as f32;
711
712 let (di, dj, dk) = stencil_positions[p];
714 result[(di + 1) as usize][(dj + 1) as usize][(dk + 1) as usize] = coeff;
715
716 let (di2, dj2, dk2) = stencil_positions[25 - p];
718 result[(di2 + 1) as usize][(dj2 + 1) as usize][(dk2 + 1) as usize] = coeff;
719 }
720
721 for dk in -1..=1_i32 {
723 for dj in -1..=1_i32 {
724 for di in -1..=1_i32 {
725 if di == 0 && dj == 0 && dk == 0 {
726 continue;
727 }
728 let si = (di + 1) as usize;
729 let sj = (dj + 1) as usize;
730 let sk = (dk + 1) as usize;
731
732 let i2 = (di * di) as f64;
733 let j2 = (dj * dj) as f64;
734 let k2 = (dk * dk) as f64;
735 let weight = (i2 / (dx * dx) + j2 / (dy * dy) + k2 / (dz * dz)) / (i2 + j2 + k2);
736 result[si][sj][sk] *= weight as f32;
737 }
738 }
739 }
740
741 let mut total = 0.0f32;
743 for dk in 0..3 {
744 for dj in 0..3 {
745 for di in 0..3 {
746 if !(di == 1 && dj == 1 && dk == 1) {
747 total += result[di][dj][dk];
748 }
749 }
750 }
751 }
752 result[1][1][1] = -total;
753
754 result
755}
756
757fn dst3d(input: &[f64], n: usize, dst: &dyn rustdct::Dst1<f64>) -> Vec<f64> {
759 let n2 = n * n;
760 let mut data = input.to_vec();
761 let mut scratch = vec![0.0_f64; dst.get_scratch_len()];
762 let mut line = vec![0.0_f64; n];
763 let mut dst1 = |buf: &mut [f64]| {
766 scratch.fill(0.0);
767 dst.process_dst1_with_scratch(buf, &mut scratch);
768 };
769 for row in data.chunks_mut(n) {
770 dst1(row);
771 }
772 for (stride, outer_step) in [(n, n2), (n2, n)] {
774 for a in 0..n {
775 for i in 0..n {
776 let base = i + a * outer_step;
777 for t in 0..n {
778 line[t] = data[base + t * stride];
779 }
780 dst1(&mut line);
781 for t in 0..n {
782 data[base + t * stride] = line[t];
783 }
784 }
785 }
786 }
787 data
788}
789
790fn dst_sin_table(n: usize) -> Vec<Vec<f64>> {
793 let scale = std::f64::consts::PI / (n as f64 + 1.0);
794 let mut table = vec![vec![0.0_f64; n]; n];
795 for j in 0..n {
796 for k in 0..n {
797 table[j][k] = ((j as f64 + 1.0) * (k as f64 + 1.0) * scale).sin();
798 }
799 }
800 table
801}
802
803#[cfg(test)]
805fn dst3d_direct(input: &[f64], n: usize, sin_table: &[Vec<f64>]) -> Vec<f64> {
806 let n2 = n * n;
807 let mut data = input.to_vec();
808 let mut buf_in = vec![0.0_f64; n];
809 let mut buf_out = vec![0.0_f64; n];
810
811 for k in 0..n {
813 for j in 0..n {
814 let base = j * n + k * n2;
815 buf_in.copy_from_slice(&data[base..base + n]);
816 dst1(&buf_in, sin_table, &mut buf_out);
817 data[base..base + n].copy_from_slice(&buf_out);
818 }
819 }
820
821 for k in 0..n {
823 for i in 0..n {
824 for j in 0..n {
825 buf_in[j] = data[i + j * n + k * n2];
826 }
827 dst1(&buf_in, sin_table, &mut buf_out);
828 for j in 0..n {
829 data[i + j * n + k * n2] = buf_out[j];
830 }
831 }
832 }
833
834 for j in 0..n {
836 for i in 0..n {
837 for k in 0..n {
838 buf_in[k] = data[i + j * n + k * n2];
839 }
840 dst1(&buf_in, sin_table, &mut buf_out);
841 for k in 0..n {
842 data[i + j * n + k * n2] = buf_out[k];
843 }
844 }
845 }
846
847 data
848}
849
850#[cfg(test)]
851fn dst1(input: &[f64], sin_table: &[Vec<f64>], output: &mut [f64]) {
853 let n = input.len();
854 for k in 0..n {
855 let mut sum = 0.0_f64;
856 for j in 0..n {
857 sum += input[j] * sin_table[j][k];
858 }
859 output[k] = sum;
860 }
861}
862
863fn solve_symmetric_pseudoinverse(g: &[Vec<f64>], h: &[f64]) -> Vec<f64> {
869 let n = h.len();
870
871 let mut a: Vec<Vec<f64>> = g.to_vec();
873 let mut v = vec![vec![0.0_f64; n]; n];
874 for i in 0..n {
875 v[i][i] = 1.0;
876 }
877
878 let max_sweeps = 100;
880 let tol = 1e-15;
881
882 for _ in 0..max_sweeps {
883 let mut max_off = 0.0_f64;
885 for i in 0..n {
886 for j in (i + 1)..n {
887 max_off = max_off.max(a[i][j].abs());
888 }
889 }
890 if max_off < tol {
891 break;
892 }
893
894 for p in 0..n {
896 for q in (p + 1)..n {
897 if a[p][q].abs() < tol {
898 continue;
899 }
900
901 let app = a[p][p];
903 let aqq = a[q][q];
904 let apq = a[p][q];
905 let tau = (aqq - app) / (2.0 * apq);
906 let t = if tau >= 0.0 {
907 1.0 / (tau + (1.0 + tau * tau).sqrt())
908 } else {
909 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
910 };
911 let c = 1.0 / (1.0 + t * t).sqrt();
912 let s = t * c;
913
914 for i in 0..n {
917 if i == p || i == q {
918 continue;
919 }
920 let aip = a[i][p];
921 let aiq = a[i][q];
922 a[i][p] = c * aip - s * aiq;
923 a[p][i] = a[i][p];
924 a[i][q] = s * aip + c * aiq;
925 a[q][i] = a[i][q];
926 }
927
928 a[p][p] = c * c * app - 2.0 * c * s * apq + s * s * aqq;
930 a[q][q] = s * s * app + 2.0 * c * s * apq + c * c * aqq;
931 a[p][q] = 0.0;
932 a[q][p] = 0.0;
933
934 for i in 0..n {
936 let vip = v[i][p];
937 let viq = v[i][q];
938 v[i][p] = c * vip - s * viq;
939 v[i][q] = s * vip + c * viq;
940 }
941 }
942 }
943 }
944
945 let eigenvalues: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
947 let max_eigen = eigenvalues.iter().cloned().fold(0.0_f64, |a, b| a.max(b.abs()));
948 let threshold = 1e-10 * max_eigen;
949
950 let mut vt_h = vec![0.0_f64; n];
952 for i in 0..n {
953 for j in 0..n {
954 vt_h[i] += v[j][i] * h[j];
955 }
956 }
957
958 for i in 0..n {
959 if eigenvalues[i].abs() > threshold {
960 vt_h[i] /= eigenvalues[i];
961 } else {
962 vt_h[i] = 0.0;
963 }
964 }
965
966 let mut x = vec![0.0_f64; n];
967 for i in 0..n {
968 for j in 0..n {
969 x[i] += v[i][j] * vt_h[j];
970 }
971 }
972
973 x
974}
975
976fn apply_stencil(
982 output: &mut [f32],
983 input: &[f32],
984 stencil: &[[[f32; 3]; 3]; 3],
985 mask: Option<&[u8]>,
986 nx: usize, ny: usize, nz: usize,
987) {
988 let nxy = nx * ny;
989 maybe_par_chunks_mut!(output, nxy).enumerate().for_each(|(k, out_slab)| {
991 for j in 0..ny {
992 for i in 0..nx {
993 let local = i + j * nx;
994 let idx = local + k * nxy;
995
996 if mask.is_some_and(|m| m[idx] == 0) {
997 out_slab[local] = 0.0;
998 continue;
999 }
1000
1001 if i == 0 || j == 0 || k == 0 || i + 1 >= nx || j + 1 >= ny || k + 1 >= nz {
1004 out_slab[local] = 0.0;
1005 continue;
1006 }
1007
1008 let mut sum = 0.0f32;
1009
1010 for dk in 0..3i32 {
1011 for dj in 0..3i32 {
1012 for di in 0..3i32 {
1013 let ni = (i as i32 + di - 1) as usize;
1014 let nj = (j as i32 + dj - 1) as usize;
1015 let nk = (k as i32 + dk - 1) as usize;
1016
1017 let nidx = ni + nj * nx + nk * nxy;
1018 sum += stencil[di as usize][dj as usize][dk as usize] * input[nidx];
1019 }
1020 }
1021 }
1022
1023 out_slab[local] = sum;
1024 }
1025 }
1026 });
1027}
1028
1029pub fn compute_field_laplacian(
1035 field: &[f32],
1036 mask: &[u8],
1037 nx: usize, ny: usize, nz: usize,
1038 vsx: f32, vsy: f32, vsz: f32,
1039) -> Vec<f32> {
1040 let mut lap = compute_laplacian(field, nx, ny, nz, vsx, vsy, vsz);
1041 for (v, &m) in lap.iter_mut().zip(mask.iter()) {
1042 if m == 0 {
1043 *v = 0.0;
1044 }
1045 }
1046 lap
1047}
1048
1049pub fn compute_phase_laplacian(
1055 phase: &[f32],
1056 mask: &[u8],
1057 nx: usize, ny: usize, nz: usize,
1058 vsx: f32, vsy: f32, vsz: f32,
1059) -> Vec<f32> {
1060 let n_total = nx * ny * nz;
1061
1062 let sin_phase: Vec<f32> = phase.iter().map(|&p| p.sin()).collect();
1063 let cos_phase: Vec<f32> = phase.iter().map(|&p| p.cos()).collect();
1064
1065 let lap_sin = compute_laplacian(&sin_phase, nx, ny, nz, vsx, vsy, vsz);
1066 let lap_cos = compute_laplacian(&cos_phase, nx, ny, nz, vsx, vsy, vsz);
1067
1068 let mut laplacian = vec![0.0f32; n_total];
1069 for i in 0..n_total {
1070 if mask[i] != 0 {
1071 laplacian[i] = lap_sin[i] * cos_phase[i] - lap_cos[i] * sin_phase[i];
1072 }
1073 }
1074
1075 laplacian
1076}
1077
1078fn compute_laplacian(
1080 input: &[f32],
1081 nx: usize, ny: usize, nz: usize,
1082 vsx: f32, vsy: f32, vsz: f32,
1083) -> Vec<f32> {
1084 let n_total = nx * ny * nz;
1085 let mut output = vec![0.0f32; n_total];
1086
1087 let hx2 = 1.0 / (vsx * vsx);
1088 let hy2 = 1.0 / (vsy * vsy);
1089 let hz2 = 1.0 / (vsz * vsz);
1090 let center = -2.0 * (hx2 + hy2 + hz2);
1091
1092 for k in 0..nz {
1093 let km1 = if k == 0 { 0 } else { k - 1 };
1094 let kp1 = if k + 1 >= nz { nz - 1 } else { k + 1 };
1095
1096 for j in 0..ny {
1097 let jm1 = if j == 0 { 0 } else { j - 1 };
1098 let jp1 = if j + 1 >= ny { ny - 1 } else { j + 1 };
1099
1100 for i in 0..nx {
1101 let im1 = if i == 0 { 0 } else { i - 1 };
1102 let ip1 = if i + 1 >= nx { nx - 1 } else { i + 1 };
1103
1104 let idx = i + j * nx + k * nx * ny;
1105
1106 output[idx] = center * input[idx]
1107 + hx2 * (input[im1 + j * nx + k * nx * ny] + input[ip1 + j * nx + k * nx * ny])
1108 + hy2 * (input[i + jm1 * nx + k * nx * ny] + input[i + jp1 * nx + k * nx * ny])
1109 + hz2 * (input[i + j * nx + km1 * nx * ny] + input[i + j * nx + kp1 * nx * ny]);
1110 }
1111 }
1112 }
1113
1114 output
1115}
1116
1117fn apply_laplacian_inplace(
1120 output: &mut [f32],
1121 input: &[f32],
1122 mask: Option<&[u8]>,
1123 nx: usize, ny: usize, nz: usize,
1124 vsx: f32, vsy: f32, vsz: f32,
1125) {
1126 let hx2 = 1.0 / (vsx * vsx);
1127 let hy2 = 1.0 / (vsy * vsy);
1128 let hz2 = 1.0 / (vsz * vsz);
1129 let nxy = nx * ny;
1130
1131 maybe_par_chunks_mut!(output, nxy).enumerate().for_each(|(k, out_slab)| {
1132 let k_offset = k * nxy;
1133
1134 for j in 0..ny {
1135 let j_offset = j * nx;
1136
1137 for i in 0..nx {
1138 let local = i + j_offset;
1139 let idx = local + k_offset;
1140
1141 if mask.is_some_and(|m| m[idx] == 0) {
1142 out_slab[local] = 0.0;
1143 continue;
1144 }
1145
1146 let a0 = input[idx];
1147
1148 let a_xm = if i > 0 { input[(i - 1) + j_offset + k_offset] } else { a0 };
1150 let a_xp = if i + 1 < nx { input[(i + 1) + j_offset + k_offset] } else { a0 };
1151 let a_ym = if j > 0 { input[i + (j - 1) * nx + k_offset] } else { a0 };
1152 let a_yp = if j + 1 < ny { input[i + (j + 1) * nx + k_offset] } else { a0 };
1153 let a_zm = if k > 0 { input[i + j_offset + (k - 1) * nxy] } else { a0 };
1154 let a_zp = if k + 1 < nz { input[i + j_offset + (k + 1) * nxy] } else { a0 };
1155
1156 out_slab[local] = hx2 * (a_xm - 2.0 * a0 + a_xp)
1158 + hy2 * (a_ym - 2.0 * a0 + a_yp)
1159 + hz2 * (a_zm - 2.0 * a0 + a_zp);
1160 }
1161 }
1162 });
1163}
1164
1165
1166#[inline]
1168fn grad_norm_sq(res: (f32, f32, f32)) -> f32 {
1169 let (dx, dy, dz) = res;
1170 4.0 * (1.0 / (dx * dx) + 1.0 / (dy * dy) + 1.0 / (dz * dz))
1171}
1172
1173fn compute_operator_norm_sqr(g: f32, g2: f32, w: f32) -> f32 {
1182 let g4 = g2 * g2;
1186 let g2w = g2 * w;
1187 let g2_w2 = g2 + w * w;
1188 let g2_1 = g2 + 1.0;
1189
1190 let mut v = [1.0f32, 1.0, 1.0];
1192
1193 for _ in 0..20 {
1194 let y0 = g4 * v[0] + g2w * v[1];
1196 let y1 = g2w * v[0] + g2_w2 * v[1] + g * v[2];
1197 let y2 = g * v[1] + g2_1 * v[2];
1198
1199 let norm = (y0 * y0 + y1 * y1 + y2 * y2).sqrt();
1201 if norm < 1e-10 {
1202 break;
1203 }
1204
1205 v[0] = y0 / norm;
1207 v[1] = y1 / norm;
1208 v[2] = y2 / norm;
1209 }
1210
1211 let y0 = g4 * v[0] + g2w * v[1];
1213 let y1 = g2w * v[0] + g2_w2 * v[1] + g * v[2];
1214 let y2 = g * v[1] + g2_1 * v[2];
1215
1216 v[0] * y0 + v[1] * y1 + v[2] * y2
1217}
1218
1219#[inline]
1221fn norm3(x: f32, y: f32, z: f32) -> f32 {
1222 (x * x + y * y + z * z).sqrt()
1223}
1224
1225#[inline]
1227fn frobenius_norm(sxx: f32, sxy: f32, sxz: f32, syy: f32, syz: f32, szz: f32) -> f32 {
1228 (sxx * sxx + syy * syy + szz * szz + 2.0 * (sxy * sxy + sxz * sxz + syz * syz)).sqrt()
1229}
1230
1231#[inline]
1233fn project_linf3(px: &mut f32, py: &mut f32, pz: &mut f32, threshold: f32) {
1234 let norm = norm3(*px, *py, *pz);
1235 if norm > threshold {
1236 let scale = threshold / norm;
1237 *px *= scale;
1238 *py *= scale;
1239 *pz *= scale;
1240 }
1241}
1242
1243#[inline]
1245fn project_linf6(
1246 qxx: &mut f32, qxy: &mut f32, qxz: &mut f32,
1247 qyy: &mut f32, qyz: &mut f32, qzz: &mut f32,
1248 threshold: f32,
1249) {
1250 let norm = frobenius_norm(*qxx, *qxy, *qxz, *qyy, *qyz, *qzz);
1251 if norm > threshold {
1252 let scale = threshold / norm;
1253 *qxx *= scale;
1254 *qxy *= scale;
1255 *qxz *= scale;
1256 *qyy *= scale;
1257 *qyz *= scale;
1258 *qzz *= scale;
1259 }
1260}
1261
1262struct TgvWorkspace {
1264 chi: Vec<f32>,
1266 chi_: Vec<f32>,
1267 phi: Vec<f32>,
1268 phi_: Vec<f32>,
1269 wx: Vec<f32>,
1270 wy: Vec<f32>,
1271 wz: Vec<f32>,
1272 wx_: Vec<f32>,
1273 wy_: Vec<f32>,
1274 wz_: Vec<f32>,
1275
1276 eta: Vec<f32>,
1278 px: Vec<f32>,
1279 py: Vec<f32>,
1280 pz: Vec<f32>,
1281 qxx: Vec<f32>,
1282 qxy: Vec<f32>,
1283 qxz: Vec<f32>,
1284 qyy: Vec<f32>,
1285 qyz: Vec<f32>,
1286 qzz: Vec<f32>,
1287
1288 temp1: Vec<f32>,
1290 temp2: Vec<f32>,
1291 gx: Vec<f32>,
1292 gy: Vec<f32>,
1293 gz: Vec<f32>,
1294
1295 sxx: Vec<f32>,
1297 sxy: Vec<f32>,
1298 sxz: Vec<f32>,
1299 syy: Vec<f32>,
1300 syz: Vec<f32>,
1301 szz: Vec<f32>,
1302
1303 divqx: Vec<f32>,
1305 divqy: Vec<f32>,
1306 divqz: Vec<f32>,
1307}
1308
1309impl TgvWorkspace {
1310 fn new(n: usize) -> Self {
1311 Self {
1312 chi: vec![0.0; n],
1313 chi_: vec![0.0; n],
1314 phi: vec![0.0; n],
1315 phi_: vec![0.0; n],
1316 wx: vec![0.0; n],
1317 wy: vec![0.0; n],
1318 wz: vec![0.0; n],
1319 wx_: vec![0.0; n],
1320 wy_: vec![0.0; n],
1321 wz_: vec![0.0; n],
1322 eta: vec![0.0; n],
1323 px: vec![0.0; n],
1324 py: vec![0.0; n],
1325 pz: vec![0.0; n],
1326 qxx: vec![0.0; n],
1327 qxy: vec![0.0; n],
1328 qxz: vec![0.0; n],
1329 qyy: vec![0.0; n],
1330 qyz: vec![0.0; n],
1331 qzz: vec![0.0; n],
1332 temp1: vec![0.0; n],
1333 temp2: vec![0.0; n],
1334 gx: vec![0.0; n],
1335 gy: vec![0.0; n],
1336 gz: vec![0.0; n],
1337 sxx: vec![0.0; n],
1338 sxy: vec![0.0; n],
1339 sxz: vec![0.0; n],
1340 syy: vec![0.0; n],
1341 syz: vec![0.0; n],
1342 szz: vec![0.0; n],
1343 divqx: vec![0.0; n],
1344 divqy: vec![0.0; n],
1345 divqz: vec![0.0; n],
1346 }
1347 }
1348}
1349
1350pub fn tgv_qsm(
1370 input: TgvInput,
1371 mask: &[u8],
1372 grid: &crate::Grid,
1373 params: &TgvParams,
1374 b0_dir: (f64, f64, f64),
1375 mut progress: impl FnMut(usize, usize),
1376) -> Result<Vec<f64>, crate::error::InputError> {
1377 use crate::error::{check_len, InputError};
1378 let n_total = grid.n_total();
1379 check_len("TGV input", input.data().len(), n_total)?;
1380 check_len("TGV mask", mask.len(), n_total)?;
1381 let (te_s, b0_t) = input.te_b0(params.te).map_err(|e| InputError(format!("TGV: {e}")))?;
1382 if !(te_s.is_finite() && te_s > 0.0 && b0_t.is_finite() && b0_t > 0.0) {
1383 return Err(InputError(format!(
1384 "TGV needs te_s > 0 and b0_t > 0, got TE = {te_s} s, B0 = {b0_t} T"
1385 )));
1386 }
1387 let c = rad_per_ppm(te_s, b0_t);
1390
1391 let data_f32: Vec<f32> = match input {
1393 TgvInput::FieldPpm { field, .. } => field.iter().map(|&v| (v * c) as f32).collect(),
1394 TgvInput::WrappedPhase { phase, .. } => phase.iter().map(|&v| v as f32).collect(),
1395 };
1396 let b0_dir = (b0_dir.0 as f32, b0_dir.1 as f32, b0_dir.2 as f32);
1397
1398 let (nx, ny, nz) = grid.dims;
1399 let vsx = grid.vsx() as f32;
1400 let vsy = grid.vsy() as f32;
1401 let vsz = grid.vsz() as f32;
1402 let res = (vsx, vsy, vsz);
1403 let iterations = params.iterations.unwrap_or_else(|| get_default_iterations(res, params.step_size));
1404
1405 let mut mask_eroded = mask.to_vec();
1407 for _ in 0..params.erosions {
1408 mask_eroded = erode_mask(&mask_eroded, nx, ny, nz, params.reference_edge_erosion);
1409 }
1410 if !mask_eroded.iter().any(|&m| m != 0) {
1412 return Err(if mask.iter().any(|&m| m != 0) {
1413 InputError::param(
1414 "TGV erosions",
1415 params.erosions,
1416 "small enough to leave part of the mask (this mask erodes away)",
1417 )
1418 } else {
1419 InputError::new("TGV mask is empty")
1420 });
1421 }
1422
1423 let mask0 = erode_mask(&mask_eroded, nx, ny, nz, params.reference_edge_erosion);
1425
1426 let bbox = BoundingBox::from_mask(&mask_eroded, nx, ny, nz, 1);
1429 let (bx, by, bz) = bbox.dims();
1430 let b_total = bbox.total();
1431 let sub_grid = crate::Grid::new(bx, by, bz, vsx as f64, vsy as f64, vsz as f64);
1432
1433 let data_sub = extract_subvolume(&data_f32, &bbox, nx, ny, nz);
1435 let mask0_sub = extract_subvolume(&mask0, &bbox, nx, ny, nz);
1436 let mask_eroded_sub = extract_subvolume(&mask_eroded, &bbox, nx, ny, nz);
1437
1438 let mut laplace_phi0 = match input {
1440 TgvInput::FieldPpm { .. } => {
1441 compute_field_laplacian(&data_sub, &mask0_sub, bx, by, bz, vsx, vsy, vsz)
1442 }
1443 TgvInput::WrappedPhase { .. } => {
1444 compute_phase_laplacian(&data_sub, &mask0_sub, bx, by, bz, vsx, vsy, vsz)
1445 }
1446 };
1447
1448 let (sum, count): (f32, usize) = laplace_phi0.iter().zip(mask0_sub.iter())
1450 .filter(|(_, &m)| m != 0)
1451 .fold((0.0, 0), |(s, c), (&v, _)| (s + v, c + 1));
1452 if count > 0 {
1453 let mean = sum / count as f32;
1454 for (v, &m) in laplace_phi0.iter_mut().zip(mask0_sub.iter()) {
1455 if m != 0 {
1456 *v -= mean;
1457 }
1458 }
1459 }
1460
1461 let stencil = compute_oblique_stencil(res, b0_dir);
1463
1464 let grad_norm_squared = grad_norm_sq(res);
1466 let grad_norm = grad_norm_squared.sqrt();
1467 let wave_norm: f32 = stencil.iter().flatten().flatten().map(|x| x.abs()).sum();
1468 let norm_sqr = compute_operator_norm_sqr(grad_norm, grad_norm_squared, wave_norm);
1469
1470 let tau = 1.0 / norm_sqr.sqrt();
1471 let sigma = tau;
1472
1473 let sigma_step = sigma * params.step_size;
1477 let tau_step = tau * params.step_size;
1478
1479 let alpha = (params.alpha0, params.alpha1);
1482
1483 let mut ws = TgvWorkspace::new(b_total);
1485
1486 for iter in 0..iterations {
1488 progress(iter, iterations);
1489
1490 apply_laplacian_inplace(&mut ws.temp1, &ws.phi_, Some(&mask0_sub), bx, by, bz, vsx, vsy, vsz);
1494 apply_stencil(&mut ws.temp2, &ws.chi_, &stencil, Some(&mask0_sub), bx, by, bz);
1495
1496 for i in 0..b_total {
1497 if mask0_sub[i] != 0 {
1498 ws.eta[i] += sigma * (-ws.temp1[i] + ws.temp2[i] - laplace_phi0[i]);
1499 }
1500 }
1501
1502 crate::utils::gradient::fgrad_inplace_f32(
1506 &mut ws.gx, &mut ws.gy, &mut ws.gz, &ws.chi_, &sub_grid
1507 );
1508
1509 for i in 0..b_total {
1510 let in_mask0 = mask0_sub[i] != 0;
1511 let in_mask = mask_eroded_sub[i] != 0;
1512
1513 if in_mask0 || in_mask {
1514 let sigmaw0 = if in_mask0 { sigma_step } else { 0.0 };
1516 let sigmaw = if in_mask { sigma_step } else { 0.0 };
1517
1518 ws.px[i] += sigmaw0 * ws.gx[i] - sigmaw * ws.wx_[i];
1519 ws.py[i] += sigmaw0 * ws.gy[i] - sigmaw * ws.wy_[i];
1520 ws.pz[i] += sigmaw0 * ws.gz[i] - sigmaw * ws.wz_[i];
1521
1522 project_linf3(&mut ws.px[i], &mut ws.py[i], &mut ws.pz[i], alpha.1);
1523 }
1524 }
1525
1526 crate::utils::gradient::symgrad_inplace_f32(
1528 &mut ws.sxx, &mut ws.sxy, &mut ws.sxz, &mut ws.syy, &mut ws.syz, &mut ws.szz,
1529 &ws.wx_, &ws.wy_, &ws.wz_, &sub_grid
1530 );
1531
1532 for i in 0..b_total {
1533 if mask0_sub[i] != 0 {
1534 ws.qxx[i] += sigma_step * ws.sxx[i];
1535 ws.qxy[i] += sigma_step * ws.sxy[i];
1536 ws.qxz[i] += sigma_step * ws.sxz[i];
1537 ws.qyy[i] += sigma_step * ws.syy[i];
1538 ws.qyz[i] += sigma_step * ws.syz[i];
1539 ws.qzz[i] += sigma_step * ws.szz[i];
1540
1541 project_linf6(
1542 &mut ws.qxx[i], &mut ws.qxy[i], &mut ws.qxz[i],
1543 &mut ws.qyy[i], &mut ws.qyz[i], &mut ws.qzz[i],
1544 alpha.0
1545 );
1546 }
1547 }
1548
1549 std::mem::swap(&mut ws.phi, &mut ws.phi_);
1551 std::mem::swap(&mut ws.chi, &mut ws.chi_);
1552 std::mem::swap(&mut ws.wx, &mut ws.wx_);
1553 std::mem::swap(&mut ws.wy, &mut ws.wy_);
1554 std::mem::swap(&mut ws.wz, &mut ws.wz_);
1555
1556 for i in 0..b_total {
1560 ws.temp1[i] = if mask0_sub[i] != 0 { ws.eta[i] } else { 0.0 };
1561 }
1562 apply_laplacian_inplace(&mut ws.temp2, &ws.temp1, None, bx, by, bz, vsx, vsy, vsz);
1564
1565 for i in 0..b_total {
1566 let denom = 1.0 + if mask_eroded_sub[i] != 0 { tau } else { 0.0 };
1567 ws.phi[i] = (ws.phi_[i] + tau * ws.temp2[i]) / denom;
1568 }
1569
1570 crate::utils::gradient::bdiv_masked_inplace_f32(
1572 &mut ws.temp1, &ws.px, &ws.py, &ws.pz, &mask0_sub, &sub_grid
1573 );
1574
1575 for i in 0..b_total {
1576 ws.gx[i] = if mask0_sub[i] != 0 { ws.eta[i] } else { 0.0 };
1577 }
1578 apply_stencil(&mut ws.temp2, &ws.gx, &stencil, None, bx, by, bz);
1580
1581 for i in 0..b_total {
1582 ws.chi[i] = ws.chi_[i] + tau_step * (ws.temp1[i] - ws.temp2[i]);
1583 }
1584
1585 for i in 0..b_total {
1587 let m = if mask0_sub[i] != 0 { 1.0 } else { 0.0 };
1588 ws.sxx[i] = ws.qxx[i] * m;
1589 ws.sxy[i] = ws.qxy[i] * m;
1590 ws.sxz[i] = ws.qxz[i] * m;
1591 ws.syy[i] = ws.qyy[i] * m;
1592 ws.syz[i] = ws.qyz[i] * m;
1593 ws.szz[i] = ws.qzz[i] * m;
1594 }
1595
1596 if params.reference_divergence {
1599 symdiv_rows_f32(
1600 &mut ws.divqx, &mut ws.divqy, &mut ws.divqz,
1601 &ws.sxx, &ws.sxy, &ws.sxz, &ws.syy, &ws.syz, &ws.szz,
1602 &sub_grid
1603 );
1604 } else {
1605 crate::utils::gradient::symdiv_inplace_f32(
1606 &mut ws.divqx, &mut ws.divqy, &mut ws.divqz,
1607 &ws.sxx, &ws.sxy, &ws.sxz, &ws.syy, &ws.syz, &ws.szz,
1608 &sub_grid
1609 );
1610 }
1611
1612 for i in 0..b_total {
1614 ws.wx[i] = ws.wx_[i];
1615 ws.wy[i] = ws.wy_[i];
1616 ws.wz[i] = ws.wz_[i];
1617 if mask_eroded_sub[i] != 0 {
1618 ws.wx[i] += tau_step * (ws.px[i] + ws.divqx[i]);
1619 ws.wy[i] += tau_step * (ws.py[i] + ws.divqy[i]);
1620 ws.wz[i] += tau_step * (ws.pz[i] + ws.divqz[i]);
1621 }
1622 }
1623
1624 for i in 0..b_total {
1626 ws.phi_[i] = 2.0 * ws.phi[i] - ws.phi_[i];
1627 ws.chi_[i] = 2.0 * ws.chi[i] - ws.chi_[i];
1628 ws.wx_[i] = 2.0 * ws.wx[i] - ws.wx_[i];
1629 ws.wy_[i] = 2.0 * ws.wy[i] - ws.wy_[i];
1630 ws.wz_[i] = 2.0 * ws.wz[i] - ws.wz_[i];
1631 }
1632 }
1633
1634 progress(iterations, iterations);
1635
1636 let mut result = vec![0.0f32; n_total];
1639 if !params.reference_output_box {
1640 for (v, &m) in ws.chi.iter_mut().zip(mask_eroded_sub.iter()) {
1641 if m == 0 {
1642 *v = 0.0;
1643 }
1644 }
1645 }
1646
1647 insert_subvolume(&mut result, &ws.chi, &bbox, nx, ny, nz);
1649
1650 Ok(result.iter().map(|&v| v as f64 / c).collect())
1651}
1652
1653#[cfg(test)]
1654mod tests {
1655 use super::*;
1656
1657 #[test]
1658 fn test_dipole_stencil() {
1659 let stencil = compute_dipole_stencil((1.0, 1.0, 1.0), (0.0, 0.0, 1.0));
1660
1661 let mut sum = 0.0f32;
1662 for k in 0..3 {
1663 for j in 0..3 {
1664 for i in 0..3 {
1665 sum += stencil[i][j][k];
1666 }
1667 }
1668 }
1669 assert!(sum.abs() < 1e-6, "Stencil sum should be ~0, got {}", sum);
1670 }
1671
1672 #[test]
1673 fn test_phase_laplacian() {
1674 let nx = 4;
1675 let ny = 4;
1676 let nz = 4;
1677 let n = nx * ny * nz;
1678
1679 let phase = vec![1.0f32; n];
1680 let mask = vec![1u8; n];
1681
1682 let lap = compute_phase_laplacian(&phase, &mask, nx, ny, nz, 1.0, 1.0, 1.0);
1683
1684 let max_val = lap.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
1685 assert!(max_val < 1e-5, "Laplacian of constant should be ~0, got max {}", max_val);
1686 }
1687
1688 fn adjoint_gap(rows: bool) -> f64 {
1689 let (nx, ny, nz) = (9, 8, 7);
1692 let n = nx * ny * nz;
1693 let grid = crate::Grid::new(nx, ny, nz, 0.9, 1.0, 1.4);
1694 let val = |s: usize, l: usize| ((l * 2654435761 + s * 40503) % 1000) as f32 / 500.0 - 1.0;
1695 let w: Vec<Vec<f32>> = (0..3).map(|s| (0..n).map(|l| val(s, l)).collect()).collect();
1696 let face = |l: usize| {
1697 let (i, j, k) = (l % nx, (l / nx) % ny, l / (nx * ny));
1698 i == 0 || j == 0 || k == 0 || i + 1 == nx || j + 1 == ny || k + 1 == nz
1699 };
1700 let q: Vec<Vec<f32>> = (0..6).map(|s| (0..n).map(|l| if face(l) { 0.0 } else { val(s + 3, l) }).collect()).collect();
1701 let mut e: Vec<Vec<f32>> = vec![vec![0.0; n]; 6];
1702 let [exx, exy, exz, eyy, eyz, ezz] = &mut e[..] else { unreachable!() };
1703 crate::utils::gradient::symgrad_inplace_f32(exx, exy, exz, eyy, eyz, ezz, &w[0], &w[1], &w[2], &grid);
1704 let mut d: Vec<Vec<f32>> = vec![vec![0.0; n]; 3];
1705 let [dx, dy, dz] = &mut d[..] else { unreachable!() };
1706 if rows {
1707 symdiv_rows_f32(dx, dy, dz, &q[0], &q[1], &q[2], &q[3], &q[4], &q[5], &grid);
1708 } else {
1709 crate::utils::gradient::symdiv_inplace_f32(dx, dy, dz, &q[0], &q[1], &q[2], &q[3], &q[4], &q[5], &grid);
1710 }
1711 let weight = [1.0, 2.0, 2.0, 1.0, 2.0, 1.0];
1712 let lhs: f64 = (0..6).map(|s| (0..n).map(|l| weight[s] * e[s][l] as f64 * q[s][l] as f64).sum::<f64>()).sum();
1713 let rhs: f64 = -(0..3).map(|s| (0..n).map(|l| w[s][l] as f64 * d[s][l] as f64).sum::<f64>()).sum::<f64>();
1714 (lhs - rhs).abs() / lhs.abs().max(rhs.abs())
1715 }
1716
1717 #[test]
1718 fn default_w_update_divergence_is_the_adjoint_and_the_references_is_not() {
1719 assert!(adjoint_gap(false) < 1e-5, "default: relative gap {}", adjoint_gap(false));
1720 assert!(adjoint_gap(true) > 1e-2, "reference: relative gap {}", adjoint_gap(true));
1721 }
1722
1723 #[test]
1724 fn test_erode_mask() {
1725 let nx = 5;
1726 let ny = 5;
1727 let nz = 5;
1728
1729 let mask = vec![1u8; nx * ny * nz];
1732 let eroded = erode_mask(&mask, nx, ny, nz, false);
1733 assert_eq!(eroded.iter().filter(|&&m| m == 1).count(), 27);
1734 assert!(erode_mask(&mask, nx, ny, nz, true).iter().all(|&m| m == 1));
1735
1736 let mut mask = mask;
1738 let center = 2 + 2 * nx + 2 * nx * ny;
1739 mask[center] = 0;
1740 let eroded = erode_mask(&mask, nx, ny, nz, true);
1741 assert_eq!(eroded.iter().filter(|&&m| m == 0).count(), 7);
1742 assert_eq!(eroded[center + 1], 0);
1743 assert_eq!(eroded[center + 1 + nx], 1);
1744 }
1745
1746 #[test]
1747 fn test_default_alpha() {
1748 let close = |a: (f32, f32), b: (f32, f32)| (a.0 - b.0).abs() < 1e-9 && (a.1 - b.1).abs() < 1e-9;
1750 assert!(close(get_default_alpha(1.0), (0.001, 0.001)));
1751 assert!(close(get_default_alpha(2.0), (0.002, 0.003)));
1752 assert!(close(get_default_alpha(3.0), (0.003, 0.005)));
1753 assert!(close(get_default_alpha(4.0), (0.004, 0.007)));
1754 assert!(close(get_default_alpha(0.0), (0.0, 0.0)));
1755 let d = TgvParams::default();
1756 assert!(close((d.alpha0, d.alpha1), (0.002, 0.003)));
1757 assert_eq!((d.iterations, d.erosions, d.step_size), (None, 3, 3.0));
1758 assert!((rad_per_ppm(0.004, 7.0) - 7.4907).abs() < 1e-3);
1759 }
1760
1761 #[test]
1762 fn test_bounding_box() {
1763 let nx = 10;
1764 let ny = 10;
1765 let nz = 10;
1766 let mut mask = vec![0u8; nx * ny * nz];
1767
1768 for k in 3..7 {
1770 for j in 3..7 {
1771 for i in 3..7 {
1772 mask[i + j * nx + k * nx * ny] = 1;
1773 }
1774 }
1775 }
1776
1777 let bbox = BoundingBox::from_mask(&mask, nx, ny, nz, 1);
1778
1779 assert_eq!(bbox.i_min, 2);
1781 assert_eq!(bbox.i_max, 8);
1782 assert_eq!(bbox.j_min, 2);
1783 assert_eq!(bbox.j_max, 8);
1784 }
1785
1786 #[test]
1787 fn test_tgv_qsm_small() {
1788 let n = 12;
1789 let n_total = n * n * n;
1790 let center = 6.0_f32;
1791 let radius = 4.0_f32;
1792
1793 let mut mask = vec![0u8; n_total];
1795 for k in 0..n {
1796 for j in 0..n {
1797 for i in 0..n {
1798 let dx = i as f32 - center;
1799 let dy = j as f32 - center;
1800 let dz = k as f32 - center;
1801 if (dx * dx + dy * dy + dz * dz).sqrt() < radius {
1802 mask[i + j * n + k * n * n] = 1;
1803 }
1804 }
1805 }
1806 }
1807
1808 let mut phase = vec![0.0f64; n_total];
1810 for k in 0..n {
1811 for j in 0..n {
1812 for i in 0..n {
1813 let idx = i + j * n + k * n * n;
1814 if mask[idx] != 0 {
1815 phase[idx] = 0.1 * k as f64;
1816 }
1817 }
1818 }
1819 }
1820
1821 let params = TgvParams {
1822 iterations: Some(10),
1823 erosions: 1,
1824 ..TgvParams::default()
1825 };
1826
1827 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
1828 let input = TgvInput::WrappedPhase { phase: &phase, te_s: 0.02, b0_t: 3.0 };
1829 let result = tgv_qsm(input, &mask, &grid, ¶ms, (0.0, 0.0, 1.0), |_, _| {}).unwrap();
1830
1831 assert_eq!(result.len(), n_total);
1832
1833 for &v in &result {
1835 assert!(v.is_finite(), "Result contains non-finite value: {}", v);
1836 }
1837
1838 let has_nonzero = result.iter().any(|&v| v.abs() > 1e-20);
1840 assert!(has_nonzero, "Result is entirely zero; expected non-zero values within mask");
1841 }
1842
1843 #[test]
1844 fn test_oblique_stencil() {
1845 let stencil = compute_oblique_stencil((1.0, 1.0, 1.0), (0.0, 0.0, 1.0));
1846
1847 let mut sum = 0.0f32;
1849 for k in 0..3 {
1850 for j in 0..3 {
1851 for i in 0..3 {
1852 sum += stencil[i][j][k];
1853 }
1854 }
1855 }
1856 assert!(
1857 sum.abs() < 1e-4,
1858 "Oblique stencil sum should be ~0, got {}",
1859 sum
1860 );
1861
1862 let mut off_sum = 0.0f32;
1864 for k in 0..3 {
1865 for j in 0..3 {
1866 for i in 0..3 {
1867 if !(i == 1 && j == 1 && k == 1) {
1868 off_sum += stencil[i][j][k];
1869 }
1870 }
1871 }
1872 }
1873 assert!(
1874 (stencil[1][1][1] + off_sum).abs() < 1e-6,
1875 "Center should be -sum(others): center={}, off_sum={}",
1876 stencil[1][1][1], off_sum
1877 );
1878 }
1879
1880 #[test]
1881 fn test_oblique_stencil_aniso() {
1882 let stencil = compute_oblique_stencil((1.0, 1.0, 2.0), (0.2, 0.3, 0.9));
1883
1884 let mut sum = 0.0f32;
1886 for k in 0..3 {
1887 for j in 0..3 {
1888 for i in 0..3 {
1889 sum += stencil[i][j][k];
1890 }
1891 }
1892 }
1893 assert!(
1894 sum.abs() < 1e-4,
1895 "Anisotropic oblique stencil sum should be ~0, got {}",
1896 sum
1897 );
1898 }
1899
1900 #[test]
1901 fn test_get_default_iterations() {
1902 let it = get_default_iterations((1.0, 1.0, 1.0), 1.0);
1904 assert!(it >= 1000, "Iterations should be >= 1000 for 1mm iso, got {}", it);
1905
1906 let it_large = get_default_iterations((2.0, 2.0, 2.0), 1.0);
1908 assert!(it_large >= 1000, "Iterations should still be >= 1000 for 2mm iso");
1909
1910 let it_small = get_default_iterations((0.5, 0.5, 0.5), 1.0);
1912 assert!(it_small > it, "Smaller voxels should need more iterations: {} vs {}", it_small, it);
1913
1914 let it_fast = get_default_iterations((1.0, 1.0, 1.0), 3.0);
1916 assert!(it_fast < it, "Higher step_size should give fewer iterations: {} vs {}", it_fast, it);
1917 }
1918
1919 #[test]
1920 fn test_tgv_zero_input() {
1921 let n = 12;
1923 let n_total = n * n * n;
1924 let center = 6.0_f32;
1925 let radius = 4.0_f32;
1926
1927 let mut mask = vec![0u8; n_total];
1928 for k in 0..n {
1929 for j in 0..n {
1930 for i in 0..n {
1931 let dx = i as f32 - center;
1932 let dy = j as f32 - center;
1933 let dz = k as f32 - center;
1934 if (dx * dx + dy * dy + dz * dz).sqrt() < radius {
1935 mask[i + j * n + k * n * n] = 1;
1936 }
1937 }
1938 }
1939 }
1940
1941 let phase = vec![0.0f64; n_total];
1942
1943 let params = TgvParams {
1944 iterations: Some(100),
1945 erosions: 1,
1946 ..TgvParams::default()
1947 };
1948
1949 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
1950 let progress_iters = std::cell::RefCell::new(Vec::new());
1951 let result = tgv_qsm(
1952 TgvInput::FieldPpm { field: &phase, echo_times: &[0.02], b0_t: 3.0 }, &mask, &grid, ¶ms, (0.0, 0.0, 1.0),
1953 |iter, _total| { progress_iters.borrow_mut().push(iter); }
1954 ).unwrap();
1955
1956 assert_eq!(result.len(), n_total);
1957
1958 let max_abs = result.iter().map(|v| v.abs()).fold(0.0f64, f64::max);
1960 assert!(
1961 max_abs < 1e-3,
1962 "Zero-phase TGV result should be ~0, got max abs {}",
1963 max_abs
1964 );
1965
1966 let iters = progress_iters.borrow();
1968 assert_eq!(iters.len(), 101);
1969 assert_eq!(*iters.last().unwrap(), 100);
1970 }
1971
1972 #[test]
1975 fn output_box_adds_the_rim_and_keeps_the_eroded_mask() {
1976 let n = 20;
1977 let mask = sphere_mask(n, 8.0);
1978 let field = test_field(n, 0.01);
1979 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
1980 let run = |box_out: bool| {
1981 let p = TgvParams { iterations: Some(50), erosions: 1, reference_output_box: box_out, ..Default::default() };
1982 tgv_qsm(TgvInput::FieldPpm { field: &field, echo_times: &[0.02], b0_t: 3.0 }, &mask, &grid, &p, (0.0, 0.0, 1.0), |_, _| {})
1983 .unwrap()
1984 };
1985 let (trimmed, boxed) = (run(false), run(true));
1986 let eroded = erode_mask(&mask, n, n, n, false);
1987 let mut rim = 0;
1988 for l in 0..n * n * n {
1989 if eroded[l] != 0 {
1990 assert_eq!(trimmed[l], boxed[l]);
1991 } else {
1992 assert_eq!(trimmed[l], 0.0);
1993 rim += (boxed[l] != 0.0) as usize;
1994 }
1995 }
1996 assert!(rim > 0, "no χ outside the eroded mask with reference_output_box");
1997 }
1998
1999 fn sphere_mask(n: usize, radius: f64) -> Vec<u8> {
2000 let c = (n as f64 - 1.0) / 2.0;
2001 let mut mask = vec![0u8; n * n * n];
2002 for k in 0..n {
2003 for j in 0..n {
2004 for i in 0..n {
2005 let (dx, dy, dz) = (i as f64 - c, j as f64 - c, k as f64 - c);
2006 if (dx * dx + dy * dy + dz * dz).sqrt() < radius {
2007 mask[i + j * n + k * n * n] = 1;
2008 }
2009 }
2010 }
2011 }
2012 mask
2013 }
2014
2015 fn test_field(n: usize, scale: f64) -> Vec<f64> {
2017 let c = (n as f64 - 1.0) / 2.0;
2018 let mut f = vec![0.0; n * n * n];
2019 for k in 0..n {
2020 for j in 0..n {
2021 for i in 0..n {
2022 let (x, y, z) = (i as f64 - c, j as f64 - c, k as f64 - c);
2023 let r2 = x * x + y * y + z * z;
2024 f[i + j * n + k * n * n] = scale * ((-r2 / 8.0).exp() + 0.002 * (x * x - z * z));
2025 }
2026 }
2027 }
2028 f
2029 }
2030
2031 #[test]
2035 fn field_ppm_uses_plain_laplacian_where_del_would_be_wrong() {
2036 let n = 16;
2037 let mask = vec![1u8; n * n * n];
2038 let a = 0.05f32;
2040 let c = (n as f32 - 1.0) / 2.0;
2041 let field: Vec<f32> = (0..n * n * n)
2042 .map(|idx| { let x = (idx % n) as f32 - c; a * x * x })
2043 .collect();
2044 let rad_per = rad_per_ppm(0.020, 3.0) as f32; let phase: Vec<f32> = field.iter().map(|&v| v * rad_per).collect();
2046 let max_rad = phase.iter().cloned().fold(0.0f32, f32::max);
2047 assert!(max_rad > std::f32::consts::PI, "test needs rad-equivalent > π, max {max_rad}");
2048
2049 let plain = compute_field_laplacian(&field, &mask, n, n, n, 1.0, 1.0, 1.0);
2050 let del = compute_phase_laplacian(&phase, &mask, n, n, n, 1.0, 1.0, 1.0);
2051
2052 let mut max_err_plain = 0.0f32;
2054 let mut max_err_del = 0.0f32;
2055 for k in 1..n - 1 {
2056 for j in 1..n - 1 {
2057 for i in 1..n - 1 {
2058 let idx = i + j * n + k * n * n;
2059 max_err_plain = max_err_plain.max((plain[idx] - 2.0 * a).abs());
2060 max_err_del = max_err_del.max((del[idx] / rad_per - 2.0 * a).abs());
2061 }
2062 }
2063 }
2064 assert!(max_err_plain < 1e-4, "plain Laplacian error {max_err_plain}");
2065 assert!(max_err_del > 0.5 * 2.0 * a, "DEL error unexpectedly small: {max_err_del}");
2067 }
2068
2069 #[test]
2073 fn field_ppm_alpha_is_in_rad_units_at_te() {
2074 let n = 20;
2075 let mask = sphere_mask(n, 8.5);
2076 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
2077 let field = test_field(n, 0.1);
2078 let params = TgvParams { iterations: Some(300), erosions: 1, ..TgvParams::default() };
2079 let at = |te: f64, p: &TgvParams| {
2080 tgv_qsm(TgvInput::FieldPpm { field: &field, echo_times: &[te], b0_t: 3.0 }, &mask, &grid, p, (0.0, 0.0, 1.0), |_, _| {}).unwrap()
2081 };
2082 let chi_2te = at(0.02, ¶ms);
2083 let half = TgvParams { alpha0: params.alpha0 / 2.0, alpha1: params.alpha1 / 2.0, ..params.clone() };
2084 let chi_half = at(0.01, &half);
2085 let chi_te = at(0.01, ¶ms);
2086
2087 let norm = chi_2te.iter().map(|v| v * v).sum::<f64>().sqrt();
2088 let rel = |a: &[f64], b: &[f64]| a.iter().zip(b).map(|(x, y)| (x - y).powi(2)).sum::<f64>().sqrt() / norm;
2089 assert!(norm > 0.0);
2090 assert!(rel(&chi_2te, &chi_half) < 1e-5, "relative difference {}", rel(&chi_2te, &chi_half));
2091 assert!(rel(&chi_2te, &chi_te) > 1e-3, "TE had no effect: {}", rel(&chi_2te, &chi_te));
2093 }
2094
2095 #[test]
2098 fn wrapped_phase_matches_field_ppm() {
2099 let n = 20;
2100 let mask = sphere_mask(n, 8.5);
2101 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
2102 let field = test_field(n, 0.02);
2103 let params = TgvParams { iterations: Some(300), erosions: 1, ..TgvParams::default() };
2104 for te in [0.005, 0.020] {
2105 let chi_f = tgv_qsm(TgvInput::FieldPpm { field: &field, echo_times: &[te], b0_t: 3.0 }, &mask, &grid, ¶ms, (0.0, 0.0, 1.0), |_, _| {}).unwrap();
2106 let norm = chi_f.iter().map(|v| v * v).sum::<f64>().sqrt();
2107 let c = rad_per_ppm(te, 3.0);
2108 let wrap = |x: f64| (x + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI;
2109 let phase: Vec<f64> = field.iter().map(|v| wrap(v * c)).collect();
2110 let input = TgvInput::WrappedPhase { phase: &phase, te_s: te, b0_t: 3.0 };
2111 let chi_p = tgv_qsm(input, &mask, &grid, ¶ms, (0.0, 0.0, 1.0), |_, _| {}).unwrap();
2112 let diff = chi_f.iter().zip(&chi_p).map(|(a, b)| (a - b).powi(2)).sum::<f64>().sqrt();
2113 assert!(diff / norm < 0.02, "TE {te}: relative difference {}", diff / norm);
2114 }
2115 }
2116
2117 #[test]
2119 fn dst3d_matches_the_direct_sum() {
2120 let n = 9;
2121 let x: Vec<f64> = (0..n * n * n).map(|i| ((i * 37 % 101) as f64 / 50.0) - 1.0).collect();
2122 let fast = dst3d(&x, n, rustdct::DctPlanner::<f64>::new().plan_dst1(n).as_ref());
2123 let direct = dst3d_direct(&x, n, &dst_sin_table(n));
2124 let err = fast.iter().zip(&direct).map(|(a, b)| (a - b).abs()).fold(0.0, f64::max);
2125 assert!(err < 1e-9, "max difference {err}");
2126 }
2127
2128 fn slope(got: &[f64], truth: &[f64], keep: impl Fn(usize) -> bool) -> f64 {
2131 let idx: Vec<usize> = (0..truth.len()).filter(|&i| keep(i)).collect();
2132 let k = idx.len() as f64;
2133 let mt = idx.iter().map(|&i| truth[i]).sum::<f64>() / k;
2134 let mg = idx.iter().map(|&i| got[i]).sum::<f64>() / k;
2135 let cov: f64 = idx.iter().map(|&i| (truth[i] - mt) * (got[i] - mg)).sum();
2136 let var: f64 = idx.iter().map(|&i| (truth[i] - mt).powi(2)).sum();
2137 cov / var
2138 }
2139
2140 #[test]
2146 fn chi_comes_back_on_the_phantom_scale() {
2147 let n = 24;
2148 let c = n as f64 / 2.0;
2149 let at = |i: usize| ((i % n) as f64 + 0.5, ((i / n) % n) as f64 + 0.5, (i / (n * n)) as f64 + 0.5);
2150 let blobs = [((c - 4.0, c, c), 3.0, 0.15), ((c + 4.0, c + 2.0, c), 2.5, -0.10), ((c, c - 4.0, c + 3.0), 2.0, 0.20)];
2151 let chi: Vec<f64> = (0..n * n * n).map(|i| {
2152 let p = at(i);
2153 blobs.iter().rev()
2154 .find(|&&((x, y, z), r, _)| ((p.0 - x).powi(2) + (p.1 - y).powi(2) + (p.2 - z).powi(2)).sqrt() < r)
2155 .map_or(0.0, |b| b.2)
2156 }).collect();
2157 let r = |i: usize| { let p = at(i); ((p.0 - c).powi(2) + (p.1 - c).powi(2) + (p.2 - c).powi(2)).sqrt() };
2158 let mask: Vec<u8> = (0..n * n * n).map(|i| (r(i) < c - 1.0) as u8).collect();
2159 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
2160 let kernel = crate::kernels::dipole::dipole_kernel(&grid, (0.0, 0.0, 1.0));
2161 let field = crate::fft::apply_real_kernel(&chi, &kernel, n, n, n);
2162 let scored = |i: usize| r(i) < c - 5.0;
2164
2165 let params = TgvParams::default();
2166 let (te, b0) = (0.010, 3.0);
2167 let ppm = tgv_qsm(TgvInput::FieldPpm { field: &field, echo_times: &[te], b0_t: b0 }, &mask, &grid, ¶ms, (0.0, 0.0, 1.0), |_, _| {}).unwrap();
2168 let s_ppm = slope(&ppm, &chi, scored);
2169 assert!((0.5..1.5).contains(&s_ppm), "FieldPpm slope {s_ppm:.3} against the phantom");
2170
2171 let wrap = |x: f64| (x + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI;
2172 let phase: Vec<f64> = field.iter().map(|v| wrap(v * rad_per_ppm(te, b0))).collect();
2173 let input = TgvInput::WrappedPhase { phase: &phase, te_s: te, b0_t: b0 };
2174 let rad = tgv_qsm(input, &mask, &grid, ¶ms, (0.0, 0.0, 1.0), |_, _| {}).unwrap();
2175 let s_rad = slope(&rad, &chi, scored);
2176 eprintln!("phantom χ slope vs truth: FieldPpm {s_ppm:.3}, WrappedPhase {s_rad:.3}");
2177 assert!((0.5..1.5).contains(&s_rad), "WrappedPhase slope {s_rad:.3} against the phantom");
2178 }
2179
2180 #[test]
2181 fn rejects_zero_te() {
2182 let n = 8;
2183 let mask = vec![1u8; n * n * n];
2184 let phase = vec![0.0; n * n * n];
2185 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
2186 let input = TgvInput::WrappedPhase { phase: &phase, te_s: 0.0, b0_t: 3.0 };
2187 let err = tgv_qsm(input, &mask, &grid, &TgvParams::default(), (0.0, 0.0, 1.0), |_, _| {})
2188 .unwrap_err();
2189 assert!(err.to_string().contains("te_s > 0"), "{err}");
2190 let input = TgvInput::FieldPpm { field: &phase, echo_times: &[0.0], b0_t: 3.0 };
2191 assert!(tgv_qsm(input, &mask, &grid, &TgvParams::default(), (0.0, 0.0, 1.0), |_, _| {}).is_err());
2192 let input = TgvInput::FieldPpm { field: &phase, echo_times: &[0.02], b0_t: 3.0 };
2194 assert!(tgv_qsm(input, &mask[1..], &grid, &TgvParams::default(), (0.0, 0.0, 1.0), |_, _| {}).is_err());
2195 }
2196
2197 #[test]
2200 fn mask_eroded_away_is_an_error() {
2201 let n = 8;
2202 let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
2203 let field = vec![0.0; n * n * n];
2204 let mut mask = vec![0u8; n * n * n];
2205 mask[3 + 3 * n + 3 * n * n] = 1; let p = TgvParams { erosions: 1, iterations: Some(1), ..Default::default() };
2207 let input = TgvInput::FieldPpm { field: &field, echo_times: &[0.02], b0_t: 3.0 };
2208 let err = tgv_qsm(input, &mask, &grid, &p, (0.0, 0.0, 1.0), |_, _| {}).unwrap_err();
2209 assert!(err.0.contains("TGV erosions = 1"), "{err}");
2210 let input = TgvInput::FieldPpm { field: &field, echo_times: &[0.02], b0_t: 3.0 };
2211 let err = tgv_qsm(input, &[0u8; 512], &grid, &p, (0.0, 0.0, 1.0), |_, _| {}).unwrap_err();
2212 assert!(err.0.contains("TGV mask is empty"), "{err}");
2213 }
2214}