1#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
14#[derive(Clone, Debug)]
15pub struct PhaseOffsetParams {
16 pub sigma: [f64; 3],
18}
19
20impl Default for PhaseOffsetParams {
21 fn default() -> Self {
22 Self {
23 sigma: [4.0, 4.0, 4.0],
24 }
25 }
26}
27
28pub type Mcpc3dsParams = PhaseOffsetParams;
30
31#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
33#[derive(Clone, Debug)]
34pub struct LinearFitParams {
35 pub estimate_offset: bool,
37 pub reliability_threshold_percentile: f64,
39}
40
41impl Default for LinearFitParams {
42 fn default() -> Self {
43 Self {
44 estimate_offset: true,
45 reliability_threshold_percentile: 90.0,
46 }
47 }
48}
49
50use std::f64::consts::PI;
51use crate::Grid;
52use crate::unwrap::romeo::{unwrap_romeo, RomeoParams};
53use crate::unwrap::laplacian::laplacian_unwrap;
54use crate::unwrap::UnwrapMethod;
55
56const TWO_PI: f64 = 2.0 * PI;
57
58#[inline]
60fn wrap_to_pi(angle: f64) -> f64 {
61 let mut a = angle % TWO_PI;
62 if a > PI {
63 a -= TWO_PI;
64 } else if a < -PI {
65 a += TWO_PI;
66 }
67 a
68}
69
70#[inline(always)]
72fn idx3d(i: usize, j: usize, k: usize, nx: usize, ny: usize) -> usize {
73 i + j * nx + k * nx * ny
74}
75
76#[derive(Clone, Copy, Debug, PartialEq)]
78pub enum B0WeightType {
79 PhaseSNR,
81 PhaseVar,
83 Average,
85 TEs,
87 Mag,
89}
90
91impl B0WeightType {
92 pub fn from_str(s: &str) -> Self {
93 match s.to_lowercase().as_str() {
94 "phase_snr" | "phasesnr" => B0WeightType::PhaseSNR,
95 "phase_var" | "phasevar" => B0WeightType::PhaseVar,
96 "average" | "uniform" => B0WeightType::Average,
97 "tes" | "te" => B0WeightType::TEs,
98 "mag" | "magnitude" => B0WeightType::Mag,
99 _ => B0WeightType::PhaseSNR, }
101 }
102}
103
104pub fn gaussian_smooth_3d_phase(
118 phase: &[f64],
119 sigma: [f64; 3],
120 mask: &[u8],
121 grid: &Grid,
122) -> Vec<f64> {
123 let (nx, ny, nz) = grid.dims;
124 let n_total = nx * ny * nz;
125
126 let mut real = vec![0.0; n_total];
129 let mut imag = vec![0.0; n_total];
130
131 for i in 0..n_total {
133 if mask[i] > 0 {
134 real[i] = phase[i].cos();
135 imag[i] = phase[i].sin();
136 }
137 }
138
139 let real_smoothed = gaussian_smooth_3d_separable(&real, sigma, mask, nx, ny, nz);
141 let imag_smoothed = gaussian_smooth_3d_separable(&imag, sigma, mask, nx, ny, nz);
142
143 let mut result = vec![0.0; n_total];
145 for i in 0..n_total {
146 if mask[i] > 0 {
147 result[i] = imag_smoothed[i].atan2(real_smoothed[i]);
148 }
149 }
150
151 result
152}
153
154fn gaussian_smooth_3d_separable(
156 data: &[f64],
157 sigma: [f64; 3],
158 mask: &[u8],
159 nx: usize, ny: usize, nz: usize,
160) -> Vec<f64> {
161 let n_total = nx * ny * nz;
162 let mut result = data.to_vec();
163 let mut temp = vec![0.0; n_total];
164
165 if sigma[0] > 0.0 {
167 let kernel = make_gaussian_kernel(sigma[0]);
168 let half = kernel.len() / 2;
169
170 for k in 0..nz {
171 for j in 0..ny {
172 for i in 0..nx {
173 let idx = idx3d(i, j, k, nx, ny);
174 if mask[idx] == 0 {
175 temp[idx] = 0.0;
176 continue;
177 }
178
179 let mut sum = 0.0;
180 let mut weight_sum = 0.0;
181
182 for (ki, &kv) in kernel.iter().enumerate() {
183 let ii = i as isize + ki as isize - half as isize;
184 if ii >= 0 && ii < nx as isize {
185 let nidx = idx3d(ii as usize, j, k, nx, ny);
186 if mask[nidx] > 0 {
187 sum += result[nidx] * kv;
188 weight_sum += kv;
189 }
190 }
191 }
192
193 temp[idx] = if weight_sum > 0.0 { sum / weight_sum } else { 0.0 };
194 }
195 }
196 }
197 std::mem::swap(&mut result, &mut temp);
198 }
199
200 if sigma[1] > 0.0 {
202 let kernel = make_gaussian_kernel(sigma[1]);
203 let half = kernel.len() / 2;
204
205 for k in 0..nz {
206 for j in 0..ny {
207 for i in 0..nx {
208 let idx = idx3d(i, j, k, nx, ny);
209 if mask[idx] == 0 {
210 temp[idx] = 0.0;
211 continue;
212 }
213
214 let mut sum = 0.0;
215 let mut weight_sum = 0.0;
216
217 for (ki, &kv) in kernel.iter().enumerate() {
218 let jj = j as isize + ki as isize - half as isize;
219 if jj >= 0 && jj < ny as isize {
220 let nidx = idx3d(i, jj as usize, k, nx, ny);
221 if mask[nidx] > 0 {
222 sum += result[nidx] * kv;
223 weight_sum += kv;
224 }
225 }
226 }
227
228 temp[idx] = if weight_sum > 0.0 { sum / weight_sum } else { 0.0 };
229 }
230 }
231 }
232 std::mem::swap(&mut result, &mut temp);
233 }
234
235 if sigma[2] > 0.0 {
237 let kernel = make_gaussian_kernel(sigma[2]);
238 let half = kernel.len() / 2;
239
240 for k in 0..nz {
241 for j in 0..ny {
242 for i in 0..nx {
243 let idx = idx3d(i, j, k, nx, ny);
244 if mask[idx] == 0 {
245 temp[idx] = 0.0;
246 continue;
247 }
248
249 let mut sum = 0.0;
250 let mut weight_sum = 0.0;
251
252 for (ki, &kv) in kernel.iter().enumerate() {
253 let kk = k as isize + ki as isize - half as isize;
254 if kk >= 0 && kk < nz as isize {
255 let nidx = idx3d(i, j, kk as usize, nx, ny);
256 if mask[nidx] > 0 {
257 sum += result[nidx] * kv;
258 weight_sum += kv;
259 }
260 }
261 }
262
263 temp[idx] = if weight_sum > 0.0 { sum / weight_sum } else { 0.0 };
264 }
265 }
266 }
267 std::mem::swap(&mut result, &mut temp);
268 }
269
270 result
271}
272
273fn make_gaussian_kernel(sigma: f64) -> Vec<f64> {
275 let radius = (3.0 * sigma).ceil() as usize;
276 let size = 2 * radius + 1;
277 let mut kernel = vec![0.0; size];
278
279 let two_sigma_sq = 2.0 * sigma * sigma;
280 let mut sum = 0.0;
281
282 for i in 0..size {
283 let x = i as f64 - radius as f64;
284 kernel[i] = (-x * x / two_sigma_sq).exp();
285 sum += kernel[i];
286 }
287
288 for k in kernel.iter_mut() {
290 *k /= sum;
291 }
292
293 kernel
294}
295
296pub fn hermitian_inner_product(
304 phase1: &[f64], mag1: &[f64],
305 phase2: &[f64], mag2: &[f64],
306 mask: &[u8],
307 n: usize,
308) -> (Vec<f64>, Vec<f64>) {
309 let mut hip_phase = vec![0.0; n];
310 let mut hip_mag = vec![0.0; n];
311
312 for i in 0..n {
313 if mask[i] > 0 {
314 hip_phase[i] = wrap_to_pi(phase2[i] - phase1[i]);
315 hip_mag[i] = mag1[i] * mag2[i];
316 }
317 }
318
319 (hip_phase, hip_mag)
320}
321
322pub fn phase_offset_removal(
358 phases: &[impl AsRef<[f64]>],
359 mags: &[impl AsRef<[f64]>],
360 tes: &[f64],
361 mask: &[u8],
362 sigma: [f64; 3],
363 echoes: [usize; 2],
364 unwrap_method: UnwrapMethod,
365 grid: &Grid,
366) -> (Vec<Vec<f64>>, Vec<f64>) {
367 let (nx, ny, nz) = grid.dims;
368 let n_echoes = phases.len();
369 let n_total = nx * ny * nz;
370
371 let e1 = echoes[0];
372 let e2 = echoes[1];
373
374 let delta_te = tes[e2] - tes[e1];
376
377 let (hip_phase, hip_mag) = hermitian_inner_product(
380 phases[e1].as_ref(), mags[e1].as_ref(),
381 phases[e2].as_ref(), mags[e2].as_ref(),
382 mask, n_total
383 );
384
385 let unwrapped_hip = match unwrap_method {
387 UnwrapMethod::Romeo => {
388 let weight: Vec<f64> = hip_mag.iter().map(|&x| x.sqrt()).collect();
389 unwrap_romeo(&hip_phase, &weight, None, 0.0, 0.0, mask, &RomeoParams::default(), grid)
390 }
391 UnwrapMethod::Laplacian => {
392 laplacian_unwrap(&hip_phase, mask, grid)
395 }
396 };
397 drop(hip_phase);
398 drop(hip_mag);
399
400 let scale = tes[e1] / delta_te;
403 let mut phase_offset = vec![0.0; n_total];
404 for i in 0..n_total {
405 if mask[i] > 0 {
406 phase_offset[i] = phases[e1].as_ref()[i] - scale * unwrapped_hip[i];
410 }
411 }
412 drop(unwrapped_hip); let phase_offset_smoothed = gaussian_smooth_3d_phase(&phase_offset, sigma, mask, grid);
417 drop(phase_offset); let mut corrected_phases = Vec::with_capacity(n_echoes);
423 for e in 0..n_echoes {
424 let mut corrected = vec![0.0; n_total];
425 for i in 0..n_total {
426 if mask[i] > 0 {
427 corrected[i] = wrap_to_pi(phases[e].as_ref()[i] - phase_offset_smoothed[i]);
428 }
429 }
430 corrected_phases.push(corrected);
431 }
432
433 (corrected_phases, phase_offset_smoothed)
434}
435
436pub fn gaussian_box_sizes(sigma: f64, n: usize) -> Vec<usize> {
440 fn round_half_even(x: f64) -> f64 {
441 let r = x.round();
442 if (x - x.trunc()).abs() == 0.5 && r % 2.0 != 0.0 { r - x.signum() } else { r }
443 }
444 if !(sigma > 0.0) || n == 0 {
445 return vec![1; n];
446 }
447 let nf = n as f64;
448 let w_ideal = (12.0 * sigma * sigma / nf + 1.0).sqrt();
449 let wl = round_half_even(w_ideal - (w_ideal + 1.0) % 2.0); let wu = wl + 2.0;
451 let m_ideal = (12.0 * sigma * sigma - nf * wl * wl - 4.0 * nf * wl - 3.0 * nf) / (-4.0 * wl - 4.0);
452 let m = round_half_even(m_ideal);
453 (1..=n).map(|i| if (i as f64) <= m { wl as usize } else { wu as usize }).collect()
454}
455
456pub fn nan_box_filter_line(line: &mut [f64], boxsize: usize, orig: &mut Vec<f64>) {
462 #[derive(PartialEq, Clone, Copy)]
463 enum Mode { Nan, Normal, Fill }
464 let n = line.len();
465 let r = boxsize / 2;
466 if n == 0 || boxsize < 3 {
467 return;
468 }
469 let maxfills = r;
470 orig.clear();
471 orig.resize(n + boxsize - 1, f64::NAN);
472 orig[r..r + n].copy_from_slice(line);
473
474 let mut lsum: f64 = orig[r..2 * r].iter().sum();
475 if lsum.is_nan() {
476 lsum = 0.0;
477 }
478 let (mut nfills, mut nvalids) = (0usize, 0usize);
479 let mut mode = Mode::Nan;
480 let bs = boxsize as f64;
481
482 for i in 1..=n {
484 if lsum.is_nan() {
485 break;
486 }
487 let lead = orig[i - 1 + 2 * r];
488 match mode {
489 Mode::Normal => {
490 if lead.is_nan() {
491 mode = Mode::Fill;
492 }
493 }
494 Mode::Nan => {
495 if lead.is_nan() { nvalids = 0; } else { nvalids += 1; }
496 if nvalids == boxsize {
497 mode = Mode::Normal;
498 lsum = orig[i - 1..i + 2 * r].iter().sum();
499 line[i - 1] = lsum / bs;
500 continue;
501 }
502 }
503 Mode::Fill => {
504 if lead.is_nan() {
505 nfills += 1;
506 if nfills > maxfills {
507 mode = Mode::Nan;
508 nfills = 0;
509 lsum = 0.0;
510 nvalids = 0;
511 }
512 } else {
513 mode = Mode::Normal;
514 nfills = 0;
515 }
516 }
517 }
518 match mode {
519 Mode::Normal => {
520 let trailing = if i >= 2 { orig[i - 2] } else { 0.0 };
521 lsum += orig[i - 1 + 2 * r] - trailing;
522 line[i - 1] = lsum / bs;
523 }
524 Mode::Fill => {
525 let trailing = if i >= 2 { orig[i - 2] } else { 0.0 };
526 lsum -= trailing;
527 line[i - 1] = (lsum - orig[i - 1]) / (bs - 2.0);
528 let prev = if i - 1 >= r { line[i - 1 - r] } else { line[i - 1] };
529 let extrapolated = 2.0 * line[i - 1] - prev;
530 orig[i - 1 + 2 * r] = extrapolated;
531 if i + r < n {
532 line[i - 1 + r] = extrapolated;
533 }
534 lsum += extrapolated;
535 }
536 Mode::Nan => {}
537 }
538 }
539}
540
541pub fn nan_box_smooth_3d(image: &mut [f64], sigma: [f64; 3], mask: &[u8], grid: &Grid) {
547 let (nx, ny, nz) = grid.dims;
548 let dims = [nx, ny, nz];
549 let n_total = nx * ny * nz;
550 assert_eq!(image.len(), n_total);
551 assert_eq!(mask.len(), n_total);
552 const NBOX: usize = 4;
553 for i in 0..n_total {
554 if mask[i] == 0 {
555 image[i] = f64::NAN;
556 }
557 }
558 let mut boxsizes: Vec<Vec<usize>> = sigma.iter().map(|&s| gaussian_box_sizes(s, NBOX)).collect();
559 for d in 0..3 {
561 for b in boxsizes[d].iter_mut() {
562 if *b % 2 == 0 { *b += 1; }
563 if *b as f64 > dims[d] as f64 / 2.0 {
564 let mut v = dims[d] / 2;
565 if v % 2 == 0 { v += 1; }
566 *b = v;
567 }
568 }
569 }
570 let mut line: Vec<f64> = Vec::new();
571 let mut scratch: Vec<f64> = Vec::new();
572 for ibox in 0..NBOX {
573 for d in 0..3 {
574 let bsize = boxsizes[d][ibox];
575 let len = dims[d];
576 if len == 1 || bsize < 3 {
577 continue;
578 }
579 let reverse = ibox % 2 == 1;
580 let (stride, n_lines_a, stride_a, n_lines_b, stride_b) = match d {
581 0 => (1, ny, nx, nz, nx * ny),
582 1 => (nx, nx, 1, nz, nx * ny),
583 _ => (nx * ny, nx, 1, ny, nx),
584 };
585 line.resize(len, 0.0);
586 for b in 0..n_lines_b {
587 for a in 0..n_lines_a {
588 let base = a * stride_a + b * stride_b;
589 for k in 0..len {
590 let kk = if reverse { len - 1 - k } else { k };
591 line[k] = image[base + kk * stride];
592 }
593 nan_box_filter_line(&mut line, bsize, &mut scratch);
594 for k in 0..len {
595 let kk = if reverse { len - 1 - k } else { k };
596 image[base + kk * stride] = line[k];
597 }
598 }
599 }
600 }
601 }
602}
603
604pub fn nan_box_smooth_3d_phase(phase: &[f64], sigma: [f64; 3], mask: &[u8], grid: &Grid) -> Vec<f64> {
608 let n = phase.len();
609 let mut re: Vec<f64> = phase.iter().map(|p| p.cos()).collect();
610 let mut im: Vec<f64> = phase.iter().map(|p| p.sin()).collect();
611 nan_box_smooth_3d(&mut re, sigma, mask, grid);
612 nan_box_smooth_3d(&mut im, sigma, mask, grid);
613 (0..n).map(|i| im[i].atan2(re[i])).collect()
614}
615
616#[derive(Debug, Clone)]
618pub struct CoilCombinationResult {
619 pub phases: Vec<Vec<f64>>,
621 pub magnitudes: Vec<Vec<f64>>,
623 pub mask: Vec<u8>,
625}
626
627pub fn mcpc3ds_combine<P: AsRef<[f64]>, M: AsRef<[f64]>>(
658 phases: &[Vec<P>],
659 mags: &[Vec<M>],
660 tes: &[f64],
661 sigma: [f64; 3],
662 echoes: [usize; 2],
663 unwrap_method: UnwrapMethod,
664 grid: &Grid,
665) -> CoilCombinationResult {
666 let (nx, ny, nz) = grid.dims;
667 let n = nx * ny * nz;
668 let n_coils = phases.len();
669 assert!(n_coils > 0, "mcpc3ds_combine: no coils");
670 assert_eq!(mags.len(), n_coils, "mcpc3ds_combine: magnitude/phase coil count differ");
671 let n_echoes = phases[0].len();
672 assert!(n_echoes >= 2, "mcpc3ds_combine: at least two echoes are required");
673 assert_eq!(tes.len(), n_echoes, "mcpc3ds_combine: echo time count must match echoes");
674 let [e1, e2] = echoes;
675 assert!(e1 < n_echoes && e2 < n_echoes && e1 != e2, "mcpc3ds_combine: HIP echoes out of range");
676 for c in 0..n_coils {
677 assert_eq!(phases[c].len(), n_echoes, "mcpc3ds_combine: coil {} has a different echo count", c);
678 assert_eq!(mags[c].len(), n_echoes, "mcpc3ds_combine: coil {} magnitude echo count differs", c);
679 for e in 0..n_echoes {
680 assert_eq!(phases[c][e].as_ref().len(), n, "mcpc3ds_combine: coil {} echo {} phase size", c, e);
681 assert_eq!(mags[c][e].as_ref().len(), n, "mcpc3ds_combine: coil {} echo {} magnitude size", c, e);
682 }
683 }
684
685 let mut hip_re = vec![0.0f64; n];
687 let mut hip_im = vec![0.0f64; n];
688 for c in 0..n_coils {
689 let (p1, p2) = (phases[c][e1].as_ref(), phases[c][e2].as_ref());
690 let (m1, m2) = (mags[c][e1].as_ref(), mags[c][e2].as_ref());
691 for i in 0..n {
692 let a = m1[i] * m2[i];
693 let d = p2[i] - p1[i];
694 hip_re[i] += a * d.cos();
695 hip_im[i] += a * d.sin();
696 }
697 }
698 let hip_phase: Vec<f64> = (0..n).map(|i| hip_im[i].atan2(hip_re[i])).collect();
699 let weight: Vec<f64> = (0..n).map(|i| (hip_re[i] * hip_re[i] + hip_im[i] * hip_im[i]).sqrt().sqrt()).collect();
701 drop(hip_re);
702 drop(hip_im);
703
704 let mask = crate::utils::bias_correction::robust_mask(&weight, grid);
706 let unwrapped_hip = match unwrap_method {
707 UnwrapMethod::Romeo => unwrap_romeo(&hip_phase, &weight, None, 0.0, 0.0, &mask, &RomeoParams::default(), grid),
708 UnwrapMethod::Laplacian => laplacian_unwrap(&hip_phase, &mask, grid),
709 };
710 drop(hip_phase);
711 drop(weight);
712
713 let scale = tes[e1] / (tes[e2] - tes[e1]);
715 let mut acc_re: Vec<Vec<f64>> = (0..n_echoes).map(|_| vec![0.0f64; n]).collect();
716 let mut acc_im: Vec<Vec<f64>> = (0..n_echoes).map(|_| vec![0.0f64; n]).collect();
717 let mut po = vec![0.0f64; n];
718 for c in 0..n_coils {
719 let p1 = phases[c][e1].as_ref();
720 for i in 0..n {
721 po[i] = if mask[i] > 0 { p1[i] - scale * unwrapped_hip[i] } else { 0.0 };
722 }
723 let mut po_s = nan_box_smooth_3d_phase(&po, sigma, &mask, grid);
725 for v in po_s.iter_mut() {
726 if !v.is_finite() { *v = 0.0; }
727 }
728 for e in 0..n_echoes {
729 let (p, m) = (phases[c][e].as_ref(), mags[c][e].as_ref());
730 let (re, im) = (&mut acc_re[e], &mut acc_im[e]);
731 for i in 0..n {
732 let w = m[i] * m[i];
733 let ang = p[i] - po_s[i];
734 re[i] += w * ang.cos();
735 im[i] += w * ang.sin();
736 }
737 }
738 }
739 drop(po);
740 drop(unwrapped_hip);
741
742 let mut phases_out = Vec::with_capacity(n_echoes);
743 let mut mags_out = Vec::with_capacity(n_echoes);
744 for e in 0..n_echoes {
745 let (re, im) = (&acc_re[e], &acc_im[e]);
746 phases_out.push((0..n).map(|i| im[i].atan2(re[i])).collect());
747 mags_out.push((0..n).map(|i| (re[i] * re[i] + im[i] * im[i]).sqrt().sqrt()).collect());
748 }
749
750 CoilCombinationResult { phases: phases_out, magnitudes: mags_out, mask }
751}
752
753pub fn calculate_b0_weighted(
770 unwrapped_phases: &[impl AsRef<[f64]>],
771 mags: &[impl AsRef<[f64]>],
772 tes: &[f64],
773 mask: &[u8],
774 weight_type: B0WeightType,
775 grid: &Grid,
776) -> Vec<f64> {
777 let n_total = grid.n_total();
778 let n_echoes = unwrapped_phases.len();
779 let mut b0 = vec![0.0; n_total];
780
781 let scale = 1.0 / TWO_PI;
785
786 for i in 0..n_total {
787 if mask[i] == 0 {
788 continue;
789 }
790
791 let mut weighted_sum = 0.0;
792 let mut weight_sum = 0.0;
793
794 for e in 0..n_echoes {
795 let te = tes[e];
796 let mag_val = mags[e].as_ref()[i];
797 let phase_over_te = unwrapped_phases[e].as_ref()[i] / te;
798
799 let w = match weight_type {
800 B0WeightType::PhaseSNR => mag_val * te,
801 B0WeightType::PhaseVar => mag_val * mag_val * te * te,
802 B0WeightType::Average => 1.0,
803 B0WeightType::TEs => te,
804 B0WeightType::Mag => mag_val,
805 };
806
807 weighted_sum += phase_over_te * w;
808 weight_sum += w;
809 }
810
811 if weight_sum > 1e-10 {
812 b0[i] = scale * weighted_sum / weight_sum;
813 }
814 }
815
816 b0
817}
818
819pub fn bipolar_correction<P: AsMut<[f64]> + AsRef<[f64]>>(
839 phases: &mut [P],
840 mags: &[impl AsRef<[f64]>],
841 tes: &[f64],
842 mask: &[u8],
843 sigma: [f64; 3],
844 grid: &Grid,
845) {
846 let (nx, ny, nz) = grid.dims;
847 let n_echoes = phases.len();
848 if n_echoes < 3 {
849 return; }
851
852 let n_total = nx * ny * nz;
853 let delta_te = tes[1] - tes[0];
854 let m = tes[0] / delta_te;
855 let k = (tes[0] + tes[2]) / tes[1];
856
857 let phi2 = if (k - k.round()).abs() < 0.01 {
860 let mag2 = if mags.is_empty() { &[] as &[f64] } else { mags[1].as_ref() };
862 unwrap_romeo(
863 phases[1].as_ref(), mag2, None, 0.0, 0.0,
864 mask, &RomeoParams::default(), grid,
865 )
866 } else {
867 phases[1].as_ref().to_vec()
868 };
869
870 let mut artefact = vec![0.0; n_total];
871 for i in 0..n_total {
872 if mask[i] > 0 {
873 artefact[i] = wrap_to_pi(
874 phases[0].as_ref()[i] + phases[2].as_ref()[i] - k * phi2[i]
875 );
876 }
877 }
878
879 artefact = gaussian_smooth_3d_phase(&artefact, sigma, mask, grid);
881
882 let mag1 = if mags.is_empty() { &[] as &[f64] } else { mags[0].as_ref() };
884 let romeo_params = RomeoParams {
885 correct_global: true,
886 ..Default::default()
887 };
888 artefact = unwrap_romeo(
889 &artefact, mag1, None, 0.0, 0.0,
890 mask, &romeo_params, grid,
891 );
892
893 let f = (2.0 - k) * m - k;
898 if f.abs() < 1e-10 {
899 return; }
901
902 for ieco in 0..n_echoes {
903 let t = if (ieco + 1) % 2 == 0 { (m + 1.0) / f } else { m / f };
906 for i in 0..n_total {
907 if mask[i] > 0 {
908 phases[ieco].as_mut()[i] = wrap_to_pi(
909 phases[ieco].as_ref()[i] - t * artefact[i]
910 );
911 }
912 }
913 }
914}
915
916
917pub struct LinearFitResult {
923 pub field: Vec<f64>,
925 pub phase_offset: Vec<f64>,
927 pub fit_residual: Vec<f64>,
929 pub reliability_mask: Vec<u8>,
931}
932
933pub fn multi_echo_linear_fit(
951 unwrapped_phases: &[impl AsRef<[f64]>],
952 mags: &[impl AsRef<[f64]>],
953 tes: &[f64],
954 mask: &[u8],
955 estimate_offset: bool,
956 reliability_threshold_percentile: f64,
957) -> LinearFitResult {
958 let n_echoes = unwrapped_phases.len();
959 let n_total = unwrapped_phases[0].as_ref().len();
960
961 let mut field = vec![0.0; n_total];
962 let mut phase_offset = vec![0.0; n_total];
963 let mut fit_residual = vec![0.0; n_total];
964
965 if estimate_offset {
966 for v in 0..n_total {
975 if mask[v] == 0 {
976 continue;
977 }
978
979 let mut sum_w = 0.0;
981 let mut sum_w_te = 0.0;
982 let mut sum_w_phase = 0.0;
983
984 for e in 0..n_echoes {
985 let w = mags[e].as_ref()[v];
986 sum_w += w;
987 sum_w_te += w * tes[e];
988 sum_w_phase += w * unwrapped_phases[e].as_ref()[v];
989 }
990
991 if sum_w < 1e-10 {
992 continue;
993 }
994
995 let te_mean = sum_w_te / sum_w;
996 let phase_mean = sum_w_phase / sum_w;
997
998 let mut sum_w_te_centered_sq = 0.0;
1000 let mut sum_w_te_centered_phase_centered = 0.0;
1001
1002 for e in 0..n_echoes {
1003 let w = mags[e].as_ref()[v];
1004 let te_centered = tes[e] - te_mean;
1005 let phase_centered = unwrapped_phases[e].as_ref()[v] - phase_mean;
1006 sum_w_te_centered_sq += w * te_centered * te_centered;
1007 sum_w_te_centered_phase_centered += w * te_centered * phase_centered;
1008 }
1009
1010 if sum_w_te_centered_sq > 1e-10 {
1011 let slope = sum_w_te_centered_phase_centered / sum_w_te_centered_sq;
1012 let intercept = phase_mean - slope * te_mean;
1013 field[v] = slope;
1014 phase_offset[v] = intercept;
1015
1016 let mut sum_w_resid_sq = 0.0;
1018 for e in 0..n_echoes {
1019 let w = mags[e].as_ref()[v];
1020 let predicted = intercept + slope * tes[e];
1021 let diff = unwrapped_phases[e].as_ref()[v] - predicted;
1022 sum_w_resid_sq += w * diff * diff;
1023 }
1024 fit_residual[v] = sum_w_resid_sq / sum_w * n_echoes as f64;
1026 }
1027 }
1028 } else {
1029 for v in 0..n_total {
1034 if mask[v] == 0 {
1035 continue;
1036 }
1037
1038 let mut sum_w_te_phase = 0.0;
1039 let mut sum_w_te_sq = 0.0;
1040 let mut sum_w = 0.0;
1041
1042 for e in 0..n_echoes {
1043 let w = mags[e].as_ref()[v];
1044 let te = tes[e];
1045 let phase = unwrapped_phases[e].as_ref()[v];
1046 sum_w_te_phase += w * te * phase;
1047 sum_w_te_sq += w * te * te;
1048 sum_w += w;
1049 }
1050
1051 if sum_w_te_sq > 1e-10 {
1052 let slope = sum_w_te_phase / sum_w_te_sq;
1053 field[v] = slope;
1054
1055 let mut sum_w_resid_sq = 0.0;
1057 for e in 0..n_echoes {
1058 let w = mags[e].as_ref()[v];
1059 let predicted = slope * tes[e];
1060 let diff = unwrapped_phases[e].as_ref()[v] - predicted;
1061 sum_w_resid_sq += w * diff * diff;
1062 }
1063 if sum_w > 1e-10 {
1065 fit_residual[v] = sum_w_resid_sq / sum_w * n_echoes as f64;
1066 }
1067 }
1068 }
1069 }
1070
1071 let reliability_mask = if reliability_threshold_percentile > 0.0 {
1073 compute_reliability_mask(&fit_residual, mask, reliability_threshold_percentile)
1074 } else {
1075 mask.to_vec()
1077 };
1078
1079 LinearFitResult {
1080 field,
1081 phase_offset,
1082 fit_residual,
1083 reliability_mask,
1084 }
1085}
1086
1087fn compute_reliability_mask(
1091 fit_residual: &[f64],
1092 mask: &[u8],
1093 threshold_percentile: f64,
1094) -> Vec<u8> {
1095 let n_total = fit_residual.len();
1096
1097 let mut residuals: Vec<f64> = fit_residual.iter()
1099 .enumerate()
1100 .filter(|(i, &r)| mask[*i] > 0 && r > 0.0 && r.is_finite())
1101 .map(|(_, &r)| r)
1102 .collect();
1103
1104 if residuals.is_empty() {
1105 return mask.to_vec();
1106 }
1107
1108 residuals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1110 let percentile_idx = ((threshold_percentile / 100.0) * residuals.len() as f64) as usize;
1111 let threshold = residuals[percentile_idx.min(residuals.len() - 1)];
1112
1113 let mut reliability = vec![0u8; n_total];
1115 for i in 0..n_total {
1116 if mask[i] > 0 && fit_residual[i] < threshold {
1117 reliability[i] = 1;
1118 }
1119 }
1120
1121 reliability
1122}
1123
1124#[inline]
1126pub fn field_to_hz(field: &[f64]) -> Vec<f64> {
1127 field.iter().map(|&f| f / TWO_PI).collect()
1128}
1129
1130#[cfg(test)]
1131mod tests {
1132 use super::*;
1133
1134 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
1135 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
1136 }
1137
1138 #[test]
1139 fn test_wrap_to_pi() {
1140 assert!((wrap_to_pi(0.0) - 0.0).abs() < 1e-10);
1141 assert!((wrap_to_pi(PI) - PI).abs() < 1e-10);
1142 assert!((wrap_to_pi(-PI) - (-PI)).abs() < 1e-10);
1143 assert!((wrap_to_pi(3.0 * PI) - PI).abs() < 1e-10);
1144 assert!((wrap_to_pi(-3.0 * PI) - (-PI)).abs() < 1e-10);
1145 }
1146
1147 #[test]
1148 fn test_gaussian_kernel() {
1149 let kernel = make_gaussian_kernel(1.0);
1150 let sum: f64 = kernel.iter().sum();
1151 assert!((sum - 1.0).abs() < 1e-10);
1152 }
1153
1154 #[test]
1155 fn test_hip() {
1156 let n = 8;
1157 let phase1 = vec![0.1; n];
1158 let phase2 = vec![0.3; n];
1159 let mag1 = vec![1.0; n];
1160 let mag2 = vec![1.0; n];
1161 let mask = vec![1u8; n];
1162
1163 let (hip_phase, hip_mag) = hermitian_inner_product(&phase1, &mag1, &phase2, &mag2, &mask, n);
1164
1165 for i in 0..n {
1166 assert!((hip_phase[i] - 0.2).abs() < 1e-10);
1167 assert!((hip_mag[i] - 1.0).abs() < 1e-10);
1168 }
1169 }
1170
1171 fn make_synthetic_multi_echo(
1181 nx: usize, ny: usize, nz: usize,
1182 tes: &[f64],
1183 ) -> (Vec<Vec<f64>>, Vec<Vec<f64>>, Vec<u8>) {
1184 let n = nx * ny * nz;
1185 let n_echoes = tes.len();
1186
1187 let phase_offset_val = 0.3;
1189 let slope_scale = 50.0;
1193
1194 let mask = vec![1u8; n];
1195 let mut phases: Vec<Vec<f64>> = Vec::with_capacity(n_echoes);
1196 let mut mags: Vec<Vec<f64>> = Vec::with_capacity(n_echoes);
1197
1198 for e in 0..n_echoes {
1199 let mut p = vec![0.0; n];
1200 let m = vec![1.0; n]; for k in 0..nz {
1202 for j in 0..ny {
1203 for i in 0..nx {
1204 let idx = idx3d(i, j, k, nx, ny);
1205 let slope = slope_scale * i as f64;
1206 p[idx] = wrap_to_pi(phase_offset_val + slope * tes[e]);
1207 }
1208 }
1209 }
1210 phases.push(p);
1211 mags.push(m);
1212 }
1213
1214 (phases, mags, mask)
1215 }
1216
1217 #[test]
1222 fn test_idx3d_basic() {
1223 assert_eq!(idx3d(0, 0, 0, 4, 4), 0);
1224 assert_eq!(idx3d(1, 0, 0, 4, 4), 1);
1225 assert_eq!(idx3d(0, 1, 0, 4, 4), 4);
1226 assert_eq!(idx3d(0, 0, 1, 4, 4), 16);
1227 assert_eq!(idx3d(3, 3, 3, 4, 4), 63);
1228 }
1229
1230 #[test]
1235 fn test_b0_weight_type_from_str() {
1236 assert_eq!(B0WeightType::from_str("phase_snr"), B0WeightType::PhaseSNR);
1237 assert_eq!(B0WeightType::from_str("phasesnr"), B0WeightType::PhaseSNR);
1238 assert_eq!(B0WeightType::from_str("PhaseSNR"), B0WeightType::PhaseSNR);
1239 assert_eq!(B0WeightType::from_str("phase_var"), B0WeightType::PhaseVar);
1240 assert_eq!(B0WeightType::from_str("phasevar"), B0WeightType::PhaseVar);
1241 assert_eq!(B0WeightType::from_str("average"), B0WeightType::Average);
1242 assert_eq!(B0WeightType::from_str("uniform"), B0WeightType::Average);
1243 assert_eq!(B0WeightType::from_str("tes"), B0WeightType::TEs);
1244 assert_eq!(B0WeightType::from_str("te"), B0WeightType::TEs);
1245 assert_eq!(B0WeightType::from_str("mag"), B0WeightType::Mag);
1246 assert_eq!(B0WeightType::from_str("magnitude"), B0WeightType::Mag);
1247 assert_eq!(B0WeightType::from_str("unknown"), B0WeightType::PhaseSNR);
1249 }
1250
1251 #[test]
1256 fn test_gaussian_smooth_3d_phase_uniform_input() {
1257 let (nx, ny, nz) = (8, 8, 8);
1258 let n = nx * ny * nz;
1259 let phase = vec![1.0; n];
1261 let mask = vec![1u8; n];
1262 let sigma = [1.0, 1.0, 1.0];
1263
1264 let smoothed = gaussian_smooth_3d_phase(&phase, sigma, &mask, &grid(nx, ny, nz));
1265
1266 assert_eq!(smoothed.len(), n);
1267 for v in &smoothed {
1268 assert!(v.is_finite(), "smoothed value must be finite");
1269 assert!((v - 1.0).abs() < 0.05, "uniform phase should remain ~1.0, got {}", v);
1270 }
1271 }
1272
1273 #[test]
1274 fn test_gaussian_smooth_3d_phase_zero_sigma() {
1275 let (nx, ny, nz) = (4, 4, 4);
1276 let n = nx * ny * nz;
1277 let phase: Vec<f64> = (0..n).map(|i| wrap_to_pi(i as f64 * 0.1)).collect();
1278 let mask = vec![1u8; n];
1279 let sigma = [0.0, 0.0, 0.0];
1280
1281 let smoothed = gaussian_smooth_3d_phase(&phase, sigma, &mask, &grid(nx, ny, nz));
1282
1283 assert_eq!(smoothed.len(), n);
1285 for i in 0..n {
1286 assert!((smoothed[i] - phase[i]).abs() < 1e-10,
1287 "zero-sigma smoothing should be identity, voxel {}: got {} expected {}",
1288 i, smoothed[i], phase[i]);
1289 }
1290 }
1291
1292 #[test]
1293 fn test_gaussian_smooth_3d_phase_masked_zeros() {
1294 let (nx, ny, nz) = (8, 8, 8);
1295 let n = nx * ny * nz;
1296 let phase = vec![0.5; n];
1297 let mut mask = vec![1u8; n];
1298 for i in 0..n / 2 {
1300 mask[i] = 0;
1301 }
1302
1303 let sigma = [1.0, 1.0, 1.0];
1304 let smoothed = gaussian_smooth_3d_phase(&phase, sigma, &mask, &grid(nx, ny, nz));
1305
1306 assert_eq!(smoothed.len(), n);
1307 for i in 0..n / 2 {
1309 assert_eq!(smoothed[i], 0.0, "masked-out voxel {} should be 0", i);
1310 }
1311 for i in n / 2..n {
1313 assert!(smoothed[i].is_finite());
1314 }
1315 }
1316
1317 #[test]
1323 fn test_gaussian_smooth_anisotropic_sigma() {
1324 let (nx, ny, nz) = (8, 8, 8);
1325 let n = nx * ny * nz;
1326 let phase: Vec<f64> = (0..n).map(|i| wrap_to_pi(0.3 * (i as f64))).collect();
1327 let mask = vec![1u8; n];
1328 let sigma = [2.0, 0.5, 1.0]; let smoothed = gaussian_smooth_3d_phase(&phase, sigma, &mask, &grid(nx, ny, nz));
1331 assert_eq!(smoothed.len(), n);
1332 for v in &smoothed {
1333 assert!(v.is_finite());
1334 assert!(*v >= -PI && *v <= PI, "smoothed phase should be in [-pi, pi], got {}", v);
1335 }
1336 }
1337
1338 #[test]
1343 fn test_gaussian_kernel_symmetry() {
1344 let kernel = make_gaussian_kernel(2.0);
1345 let len = kernel.len();
1346 for i in 0..len / 2 {
1347 assert!((kernel[i] - kernel[len - 1 - i]).abs() < 1e-12,
1348 "kernel should be symmetric");
1349 }
1350 }
1351
1352 #[test]
1353 fn test_gaussian_kernel_peak_at_center() {
1354 let kernel = make_gaussian_kernel(1.5);
1355 let center = kernel.len() / 2;
1356 for (i, &v) in kernel.iter().enumerate() {
1357 if i != center {
1358 assert!(v <= kernel[center], "center should be peak");
1359 }
1360 }
1361 }
1362
1363 #[test]
1368 fn test_hip_with_mask() {
1369 let n = 4;
1370 let phase1 = vec![0.5; n];
1371 let phase2 = vec![1.0; n];
1372 let mag1 = vec![2.0; n];
1373 let mag2 = vec![3.0; n];
1374 let mask = vec![1, 0, 1, 0];
1375
1376 let (hip_phase, hip_mag) = hermitian_inner_product(
1377 &phase1, &mag1, &phase2, &mag2, &mask, n
1378 );
1379
1380 assert!((hip_phase[0] - 0.5).abs() < 1e-10);
1382 assert!((hip_mag[0] - 6.0).abs() < 1e-10);
1383 assert!((hip_phase[2] - 0.5).abs() < 1e-10);
1384 assert!((hip_mag[2] - 6.0).abs() < 1e-10);
1385
1386 assert_eq!(hip_phase[1], 0.0);
1388 assert_eq!(hip_mag[1], 0.0);
1389 assert_eq!(hip_phase[3], 0.0);
1390 assert_eq!(hip_mag[3], 0.0);
1391 }
1392
1393 #[test]
1394 fn test_hip_wrapping() {
1395 let n = 1;
1397 let phase1 = vec![PI - 0.1];
1398 let phase2 = vec![-PI + 0.1];
1399 let mag1 = vec![1.0];
1400 let mag2 = vec![1.0];
1401 let mask = vec![1u8];
1402
1403 let (hip_phase, _) = hermitian_inner_product(
1404 &phase1, &mag1, &phase2, &mag2, &mask, n
1405 );
1406
1407 assert!((hip_phase[0] - 0.2).abs() < 1e-10,
1409 "HIP should wrap phase difference, got {}", hip_phase[0]);
1410 }
1411
1412 #[test]
1417 fn test_find_seed_point_full_mask() {
1418 use crate::unwrap::romeo::find_seed_point;
1419 let (nx, ny, nz) = (8, 8, 8);
1420 let mask = vec![1u8; nx * ny * nz];
1421 let (si, sj, sk) = find_seed_point(&mask, nx, ny, nz);
1422 assert_eq!(si, 3); assert_eq!(sj, 3);
1425 assert_eq!(sk, 3);
1426 }
1427
1428 #[test]
1429 fn test_find_seed_point_empty_mask() {
1430 use crate::unwrap::romeo::find_seed_point;
1431 let (nx, ny, nz) = (8, 8, 8);
1432 let mask = vec![0u8; nx * ny * nz];
1433 let (si, sj, sk) = find_seed_point(&mask, nx, ny, nz);
1434 assert_eq!(si, 4);
1436 assert_eq!(sj, 4);
1437 assert_eq!(sk, 4);
1438 }
1439
1440 #[test]
1441 fn test_find_seed_point_corner_mask() {
1442 use crate::unwrap::romeo::find_seed_point;
1443 let (nx, ny, nz) = (8, 8, 8);
1444 let mut mask = vec![0u8; nx * ny * nz];
1445 mask[idx3d(0, 0, 0, nx, ny)] = 1;
1447 let (si, sj, sk) = find_seed_point(&mask, nx, ny, nz);
1448 assert_eq!(si, 0);
1449 assert_eq!(sj, 0);
1450 assert_eq!(sk, 0);
1451 }
1452
1453 #[test]
1458 fn test_field_to_hz() {
1459 let field = vec![TWO_PI, -TWO_PI, 0.0, PI];
1460 let hz = field_to_hz(&field);
1461 assert!((hz[0] - 1.0).abs() < 1e-10);
1462 assert!((hz[1] - (-1.0)).abs() < 1e-10);
1463 assert!((hz[2] - 0.0).abs() < 1e-10);
1464 assert!((hz[3] - 0.5).abs() < 1e-10);
1465 }
1466
1467 #[test]
1472 fn test_calculate_b0_weighted_phase_snr() {
1473 let n = 64;
1478 let tes = [0.005, 0.010, 0.015]; let slope = 200.0; let mask = vec![1u8; n];
1481
1482 let phases: Vec<Vec<f64>> = tes.iter()
1483 .map(|&te| vec![slope * te; n])
1484 .collect();
1485 let mags: Vec<Vec<f64>> = tes.iter()
1486 .map(|_| vec![1.0; n])
1487 .collect();
1488
1489 let b0 = calculate_b0_weighted(&phases, &mags, &tes, &mask, B0WeightType::PhaseSNR, &grid(n, 1, 1));
1490
1491 let expected_hz = 1.0 / TWO_PI * slope;
1492 assert_eq!(b0.len(), n);
1493 for v in &b0 {
1494 assert!(v.is_finite());
1495 assert!((v - expected_hz).abs() < 1e-8,
1496 "expected {} Hz, got {}", expected_hz, v);
1497 }
1498 }
1499
1500 #[test]
1501 fn test_calculate_b0_weighted_all_weight_types() {
1502 let n = 16;
1503 let tes = [0.005, 0.010, 0.015]; let slope = 100.0; let mask = vec![1u8; n];
1506
1507 let phases: Vec<Vec<f64>> = tes.iter()
1508 .map(|&te| vec![slope * te; n])
1509 .collect();
1510 let mags: Vec<Vec<f64>> = tes.iter()
1511 .map(|_| vec![2.0; n])
1512 .collect();
1513
1514 let expected_hz = 1.0 / TWO_PI * slope;
1515
1516 for wt in &[
1517 B0WeightType::PhaseSNR,
1518 B0WeightType::PhaseVar,
1519 B0WeightType::Average,
1520 B0WeightType::TEs,
1521 B0WeightType::Mag,
1522 ] {
1523 let b0 = calculate_b0_weighted(&phases, &mags, &tes, &mask, *wt, &grid(n, 1, 1));
1524 assert_eq!(b0.len(), n);
1525 for v in &b0 {
1526 assert!(v.is_finite(), "weight type {:?} produced non-finite", wt);
1527 assert!((v - expected_hz).abs() < 1e-8,
1528 "weight type {:?}: expected {} Hz, got {}", wt, expected_hz, v);
1529 }
1530 }
1531 }
1532
1533 #[test]
1534 fn test_calculate_b0_weighted_masked_out() {
1535 let n = 8;
1536 let tes = [0.005, 0.010]; let mask = vec![0u8; n]; let phases: Vec<Vec<f64>> = tes.iter()
1540 .map(|&te| vec![500.0 * te; n])
1541 .collect();
1542 let mags: Vec<Vec<f64>> = tes.iter()
1543 .map(|_| vec![1.0; n])
1544 .collect();
1545
1546 let b0 = calculate_b0_weighted(&phases, &mags, &tes, &mask, B0WeightType::PhaseSNR, &grid(n, 1, 1));
1547
1548 for v in &b0 {
1549 assert_eq!(*v, 0.0, "masked-out voxels should have B0=0");
1550 }
1551 }
1552
1553 #[test]
1554 fn test_calculate_b0_weighted_zero_magnitude() {
1555 let n = 4;
1557 let tes = [0.005, 0.010, 0.015];
1558 let mask = vec![1u8; n];
1559
1560 let phases: Vec<Vec<f64>> = tes.iter()
1561 .map(|&te| vec![0.2 * te; n])
1562 .collect();
1563 let mags: Vec<Vec<f64>> = tes.iter()
1564 .map(|_| vec![0.0; n]) .collect();
1566
1567 let b0 = calculate_b0_weighted(&phases, &mags, &tes, &mask, B0WeightType::PhaseSNR, &grid(n, 1, 1));
1569 for v in &b0 {
1570 assert_eq!(*v, 0.0, "zero-magnitude voxels should yield B0=0");
1571 }
1572
1573 let b0_avg = calculate_b0_weighted(&phases, &mags, &tes, &mask, B0WeightType::Average, &grid(n, 1, 1));
1575 let expected = 1.0 / TWO_PI * 0.2;
1576 for v in &b0_avg {
1577 assert!((v - expected).abs() < 1e-8);
1578 }
1579 }
1580
1581 #[test]
1586 fn test_multi_echo_linear_fit_no_offset() {
1587 let n = 32;
1590 let tes = [0.005, 0.010, 0.015]; let slope = 100.0; let mask = vec![1u8; n];
1593
1594 let phases: Vec<Vec<f64>> = tes.iter()
1595 .map(|&te| vec![slope * te; n])
1596 .collect();
1597 let mags: Vec<Vec<f64>> = tes.iter()
1598 .map(|_| vec![1.0; n])
1599 .collect();
1600
1601 let result = multi_echo_linear_fit(
1602 &phases, &mags, &tes, &mask,
1603 false, 0.0, );
1606
1607 assert_eq!(result.field.len(), n);
1608 assert_eq!(result.phase_offset.len(), n);
1609 assert_eq!(result.fit_residual.len(), n);
1610 assert_eq!(result.reliability_mask.len(), n);
1611
1612 for i in 0..n {
1613 assert!((result.field[i] - slope).abs() < 1e-6,
1614 "slope: expected {}, got {}", slope, result.field[i]);
1615 assert_eq!(result.phase_offset[i], 0.0,
1616 "offset should be 0 when estimate_offset=false");
1617 assert!(result.fit_residual[i] < 1e-10,
1618 "residual should be ~0 for perfect linear data");
1619 assert_eq!(result.reliability_mask[i], 1,
1620 "reliability should match mask when threshold=0");
1621 }
1622 }
1623
1624 #[test]
1625 fn test_multi_echo_linear_fit_with_offset() {
1626 let n = 16;
1628 let tes = [0.005, 0.010, 0.015, 0.020];
1629 let slope = 200.0; let intercept = 0.5; let mask = vec![1u8; n];
1632
1633 let phases: Vec<Vec<f64>> = tes.iter()
1634 .map(|&te| vec![intercept + slope * te; n])
1635 .collect();
1636 let mags: Vec<Vec<f64>> = tes.iter()
1637 .map(|_| vec![1.0; n])
1638 .collect();
1639
1640 let result = multi_echo_linear_fit(
1641 &phases, &mags, &tes, &mask,
1642 true, 0.0,
1644 );
1645
1646 for i in 0..n {
1647 assert!((result.field[i] - slope).abs() < 1e-4,
1648 "slope: expected {}, got {}", slope, result.field[i]);
1649 assert!((result.phase_offset[i] - intercept).abs() < 1e-4,
1650 "intercept: expected {}, got {}", intercept, result.phase_offset[i]);
1651 assert!(result.fit_residual[i] < 1e-8,
1652 "residual should be ~0 for perfect linear data, got {}", result.fit_residual[i]);
1653 }
1654 }
1655
1656 #[test]
1657 fn test_multi_echo_linear_fit_masked_out() {
1658 let n = 8;
1659 let tes = [0.005, 0.010, 0.015];
1660 let mask = vec![0u8; n];
1661
1662 let phases: Vec<Vec<f64>> = tes.iter()
1663 .map(|&te| vec![100.0 * te; n])
1664 .collect();
1665 let mags: Vec<Vec<f64>> = tes.iter()
1666 .map(|_| vec![1.0; n])
1667 .collect();
1668
1669 let result = multi_echo_linear_fit(&phases, &mags, &tes, &mask, true, 0.0);
1670
1671 for i in 0..n {
1672 assert_eq!(result.field[i], 0.0);
1673 assert_eq!(result.phase_offset[i], 0.0);
1674 assert_eq!(result.fit_residual[i], 0.0);
1675 }
1676 }
1677
1678 #[test]
1679 fn test_multi_echo_linear_fit_varying_slope() {
1680 let n = 8;
1682 let tes = [0.005, 0.010, 0.015];
1683 let mask = vec![1u8; n];
1684
1685 let slopes: Vec<f64> = (0..n).map(|i| 50.0 * (i as f64 + 1.0)).collect();
1686
1687 let phases: Vec<Vec<f64>> = tes.iter()
1688 .map(|&te| {
1689 slopes.iter().map(|&s| s * te).collect()
1690 })
1691 .collect();
1692 let mags: Vec<Vec<f64>> = tes.iter()
1693 .map(|_| vec![1.0; n])
1694 .collect();
1695
1696 let result = multi_echo_linear_fit(&phases, &mags, &tes, &mask, false, 0.0);
1697
1698 for i in 0..n {
1699 assert!((result.field[i] - slopes[i]).abs() < 1e-6,
1700 "voxel {}: expected slope {}, got {}", i, slopes[i], result.field[i]);
1701 }
1702 }
1703
1704 #[test]
1705 fn test_multi_echo_linear_fit_with_reliability_threshold() {
1706 let n = 100;
1708 let tes = [0.005, 0.010, 0.015];
1709 let mask = vec![1u8; n];
1710 let slope = 100.0;
1711
1712 let mut phases: Vec<Vec<f64>> = tes.iter()
1713 .map(|&te| vec![slope * te; n])
1714 .collect();
1715 let mags: Vec<Vec<f64>> = tes.iter()
1716 .map(|_| vec![1.0; n])
1717 .collect();
1718
1719 for e in 0..tes.len() {
1721 for i in 90..100 {
1722 phases[e][i] += if e % 2 == 0 { 2.0 } else { -2.0 };
1723 }
1724 }
1725
1726 let result = multi_echo_linear_fit(&phases, &mags, &tes, &mask, false, 80.0);
1728
1729 assert_eq!(result.reliability_mask.len(), n);
1730
1731 let max_clean_resid = result.fit_residual[0..90].iter()
1738 .cloned().fold(0.0f64, f64::max);
1739 let min_noisy_resid = result.fit_residual[90..100].iter()
1740 .cloned().fold(f64::INFINITY, f64::min);
1741 assert!(max_clean_resid < min_noisy_resid,
1742 "clean residuals ({}) should be less than noisy residuals ({})",
1743 max_clean_resid, min_noisy_resid);
1744
1745 for &v in &result.reliability_mask {
1747 assert!(v == 0 || v == 1);
1748 }
1749 }
1750
1751 #[test]
1752 fn test_multi_echo_linear_fit_zero_magnitude() {
1753 let n = 4;
1754 let tes = [0.005, 0.010, 0.015];
1755 let mask = vec![1u8; n];
1756
1757 let phases: Vec<Vec<f64>> = tes.iter()
1758 .map(|&te| vec![100.0 * te; n])
1759 .collect();
1760 let mags: Vec<Vec<f64>> = tes.iter()
1761 .map(|_| vec![0.0; n]) .collect();
1763
1764 let result = multi_echo_linear_fit(&phases, &mags, &tes, &mask, false, 0.0);
1766 for v in &result.field {
1767 assert!(v.is_finite());
1768 }
1769
1770 let result2 = multi_echo_linear_fit(&phases, &mags, &tes, &mask, true, 0.0);
1771 for v in &result2.field {
1772 assert!(v.is_finite());
1773 }
1774 }
1775
1776 #[test]
1781 fn test_compute_reliability_mask_basic() {
1782 let n = 10;
1783 let mask = vec![1u8; n];
1784 let fit_residual: Vec<f64> = (1..=n).map(|i| i as f64 * 0.1).collect();
1786
1787 let reliability = compute_reliability_mask(&fit_residual, &mask, 50.0);
1789 assert_eq!(reliability.len(), n);
1790
1791 let reliable_count: usize = reliability.iter().map(|&v| v as usize).sum();
1793 assert!(reliable_count > 0 && reliable_count < n,
1794 "some but not all should be reliable, got {}/{}", reliable_count, n);
1795 }
1796
1797 #[test]
1798 fn test_compute_reliability_mask_all_zero_residual() {
1799 let n = 5;
1800 let mask = vec![1u8; n];
1801 let fit_residual = vec![0.0; n];
1802
1803 let reliability = compute_reliability_mask(&fit_residual, &mask, 50.0);
1806 assert_eq!(reliability, mask);
1807 }
1808
1809 #[test]
1814 fn test_phase_offset_removal_output_sizes() {
1815 let (nx, ny, nz) = (8, 8, 8);
1816 let n = nx * ny * nz;
1817 let tes = [0.005, 0.010, 0.015];
1818 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
1819
1820 let sigma = [1.0, 1.0, 1.0];
1821 let (corrected, offset) = phase_offset_removal(
1822 &phases, &mags, &tes, &mask, sigma, [0, 1], UnwrapMethod::Romeo, &grid(nx, ny, nz),
1823 );
1824
1825 assert_eq!(corrected.len(), tes.len(), "should have one corrected phase per echo");
1827 for (e, cp) in corrected.iter().enumerate() {
1828 assert_eq!(cp.len(), n, "echo {} corrected phase should have {} voxels", e, n);
1829 }
1830 assert_eq!(offset.len(), n, "phase offset should have {} voxels", n);
1831 }
1832
1833 #[test]
1834 fn test_phase_offset_removal_finite_output() {
1835 let (nx, ny, nz) = (8, 8, 8);
1836 let tes = [0.005, 0.010, 0.015];
1837 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
1838
1839 let sigma = [1.0, 1.0, 1.0];
1840 let (corrected, offset) = phase_offset_removal(
1841 &phases, &mags, &tes, &mask, sigma, [0, 1], UnwrapMethod::Romeo, &grid(nx, ny, nz),
1842 );
1843
1844 for v in &offset {
1845 assert!(v.is_finite(), "phase offset should be finite");
1846 }
1847 for cp in &corrected {
1848 for v in cp {
1849 assert!(v.is_finite(), "corrected phase should be finite");
1850 }
1851 }
1852 }
1853
1854 #[test]
1855 fn test_phase_offset_removal_corrected_in_range() {
1856 let (nx, ny, nz) = (8, 8, 8);
1857 let tes = [0.005, 0.010, 0.015];
1858 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
1859
1860 let sigma = [1.0, 1.0, 1.0];
1861 let (corrected, _) = phase_offset_removal(
1862 &phases, &mags, &tes, &mask, sigma, [0, 1], UnwrapMethod::Romeo, &grid(nx, ny, nz),
1863 );
1864
1865 for cp in &corrected {
1867 for &v in cp {
1868 assert!(v >= -PI - 1e-10 && v <= PI + 1e-10,
1869 "corrected phase should be in [-pi, pi], got {}", v);
1870 }
1871 }
1872 }
1873
1874 #[test]
1875 fn test_phase_offset_removal_uniform_phase() {
1876 let (nx, ny, nz) = (8, 8, 8);
1878 let n = nx * ny * nz;
1879 let tes = [0.005, 0.010, 0.015];
1880
1881 let phases: Vec<Vec<f64>> = (0..3).map(|_| vec![0.5; n]).collect();
1883 let mags: Vec<Vec<f64>> = (0..3).map(|_| vec![1.0; n]).collect();
1884 let mask = vec![1u8; n];
1885
1886 let sigma = [1.0, 1.0, 1.0];
1887 let (corrected, _offset) = phase_offset_removal(
1888 &phases, &mags, &tes, &mask, sigma, [0, 1], UnwrapMethod::Romeo, &grid(nx, ny, nz),
1889 );
1890
1891 for cp in &corrected {
1893 for &v in cp {
1894 assert!(v.abs() < 1.0,
1895 "after offset removal of uniform phase, corrected should be ~0, got {}", v);
1896 }
1897 }
1898 }
1899
1900 #[test]
1905 fn test_field_mapping_composed() {
1906 use crate::unwrap::romeo::{unwrap_romeo_multi_echo, RomeoParams};
1907 let (nx, ny, nz) = (8, 8, 8);
1908 let n = nx * ny * nz;
1909 let tes = [0.005, 0.010, 0.015];
1910 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
1911 let sigma = [1.0, 1.0, 1.0];
1912
1913 let (corrected, offset) = phase_offset_removal(
1915 &phases, &mags, &tes, &mask, sigma, [0, 1],
1916 UnwrapMethod::Romeo, &grid(nx, ny, nz),
1917 );
1918 assert_eq!(corrected.len(), tes.len());
1919 assert_eq!(offset.len(), n);
1920
1921 let mag_refs: Vec<&[f64]> = mags.iter().map(|m| m.as_slice()).collect();
1923 let unwrapped = unwrap_romeo_multi_echo(
1924 &corrected, &mag_refs, &tes, &mask,
1925 &RomeoParams::default(), &grid(nx, ny, nz),
1926 );
1927 assert_eq!(unwrapped.len(), tes.len());
1928
1929 let b0 = calculate_b0_weighted(
1931 &unwrapped, &mags, &tes, &mask, B0WeightType::PhaseSNR, &grid(nx, ny, nz),
1932 );
1933 assert_eq!(b0.len(), n);
1934 for v in &b0 {
1935 assert!(v.is_finite(), "B0 should be finite");
1936 }
1937 }
1938
1939 #[test]
1940 fn test_field_mapping_different_weight_types() {
1941 use crate::unwrap::romeo::{unwrap_romeo_multi_echo, RomeoParams};
1942 let (nx, ny, nz) = (8, 8, 8);
1943 let n = nx * ny * nz;
1944 let tes = [0.005, 0.010, 0.015];
1945 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
1946 let sigma = [1.0, 1.0, 1.0];
1947
1948 let (corrected, _) = phase_offset_removal(
1949 &phases, &mags, &tes, &mask, sigma, [0, 1],
1950 UnwrapMethod::Romeo, &grid(nx, ny, nz),
1951 );
1952 let mag_refs: Vec<&[f64]> = mags.iter().map(|m| m.as_slice()).collect();
1953 let unwrapped = unwrap_romeo_multi_echo(
1954 &corrected, &mag_refs, &tes, &mask,
1955 &RomeoParams::default(), &grid(nx, ny, nz),
1956 );
1957
1958 for wt in &[
1959 B0WeightType::PhaseSNR,
1960 B0WeightType::Average,
1961 B0WeightType::TEs,
1962 B0WeightType::Mag,
1963 B0WeightType::PhaseVar,
1964 ] {
1965 let b0 = calculate_b0_weighted(&unwrapped, &mags, &tes, &mask, *wt, &grid(n, 1, 1));
1966 for v in &b0 {
1967 assert!(v.is_finite(), "B0 with {:?} should be finite", wt);
1968 }
1969 }
1970 }
1971
1972 #[test]
1977 fn test_wrap_to_pi_near_boundaries() {
1978 let v1 = wrap_to_pi(PI + 0.001);
1980 assert!(v1 < PI && v1 > -PI, "should wrap back into range");
1981
1982 let v2 = wrap_to_pi(-PI - 0.001);
1983 assert!(v2 > -PI && v2 < PI, "should wrap back into range");
1984
1985 let v3 = wrap_to_pi(100.0 * PI);
1987 assert!(v3 >= -PI && v3 <= PI, "should be in [-pi, pi], got {}", v3);
1988
1989 let v4 = wrap_to_pi(-100.0 * PI);
1990 assert!(v4 >= -PI && v4 <= PI, "should be in [-pi, pi], got {}", v4);
1991 }
1992
1993 #[test]
1999 fn test_unwrap_romeo_smooth_data() {
2000 use crate::unwrap::romeo::{unwrap_romeo, RomeoParams};
2001 let (nx, ny, nz) = (8, 8, 8);
2002 let n = nx * ny * nz;
2003 let phase: Vec<f64> = (0..n).map(|i| {
2005 let x = (i % nx) as f64 / nx as f64;
2006 0.5 * x }).collect();
2008 let mag = vec![1.0; n];
2009 let mask = vec![1u8; n];
2010
2011 let unwrapped = unwrap_romeo(
2012 &phase, &mag, None, 0.0, 0.0,
2013 &mask, &RomeoParams::default(), &grid(nx, ny, nz),
2014 );
2015
2016 assert_eq!(unwrapped.len(), n);
2017 for (i, &v) in unwrapped.iter().enumerate() {
2018 assert!(v.is_finite(), "unwrapped voxel {} should be finite", i);
2019 }
2020 }
2021
2022 #[test]
2027 fn test_linear_fit_on_mcpc3ds_output() {
2028 let (nx, ny, nz) = (8, 8, 8);
2029 let n = nx * ny * nz;
2030 let tes = [0.005, 0.010, 0.015];
2031 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
2032
2033 let sigma = [1.0, 1.0, 1.0];
2034 let (corrected, _offset) = phase_offset_removal(
2035 &phases, &mags, &tes, &mask, sigma, [0, 1], UnwrapMethod::Romeo, &grid(nx, ny, nz),
2036 );
2037
2038 let tes_s: Vec<f64> = tes.iter().map(|&t| t / 1000.0).collect();
2040 let result = multi_echo_linear_fit(
2041 &corrected, &mags, &tes_s, &mask, true, 0.0,
2042 );
2043
2044 assert_eq!(result.field.len(), n);
2045 assert_eq!(result.phase_offset.len(), n);
2046 assert_eq!(result.fit_residual.len(), n);
2047 assert_eq!(result.reliability_mask.len(), n);
2048
2049 for v in &result.field {
2050 assert!(v.is_finite(), "field should be finite");
2051 }
2052 for v in &result.phase_offset {
2053 assert!(v.is_finite(), "phase_offset should be finite");
2054 }
2055 }
2056
2057 #[test]
2062 fn test_bipolar_correction_3_echoes() {
2063 let (nx, ny, nz) = (8, 8, 8);
2064 let n = nx * ny * nz;
2065 let tes = [0.005, 0.010, 0.015];
2066 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
2067
2068 let mut phases_mut = phases;
2069 let sigma = [1.0, 1.0, 1.0];
2070 bipolar_correction(
2071 &mut phases_mut, &mags, &tes, &mask,
2072 sigma, &grid(nx, ny, nz),
2073 );
2074
2075 for echo in &phases_mut {
2077 for &v in echo {
2078 assert!(v.is_finite(), "bipolar-corrected phase should be finite");
2079 }
2080 }
2081 }
2082
2083 #[test]
2084 fn test_bipolar_correction_2_echoes_noop() {
2085 let (nx, ny, nz) = (8, 8, 8);
2087 let n = nx * ny * nz;
2088 let tes = [0.005, 0.010];
2089 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
2090
2091 let original: Vec<Vec<f64>> = phases.iter().map(|p| p.clone()).collect();
2092 let mut phases_mut = phases;
2093 bipolar_correction(
2094 &mut phases_mut, &mags, &tes, &mask,
2095 [1.0, 1.0, 1.0], &grid(nx, ny, nz),
2096 );
2097
2098 for (e, echo) in phases_mut.iter().enumerate() {
2100 for (i, &v) in echo.iter().enumerate() {
2101 assert_eq!(v, original[e][i],
2102 "2-echo bipolar correction should be no-op");
2103 }
2104 }
2105 }
2106
2107 #[test]
2108 fn test_field_mapping_with_bipolar() {
2109 use crate::unwrap::romeo::{unwrap_romeo_multi_echo, RomeoParams};
2110 let (nx, ny, nz) = (8, 8, 8);
2111 let n = nx * ny * nz;
2112 let tes = [0.005, 0.010, 0.015];
2113 let (phases, mags, mask) = make_synthetic_multi_echo(nx, ny, nz, &tes);
2114 let sigma = [1.0, 1.0, 1.0];
2115
2116 let (mut corrected, _) = phase_offset_removal(
2117 &phases, &mags, &tes, &mask, sigma, [0, 1],
2118 UnwrapMethod::Romeo, &grid(nx, ny, nz),
2119 );
2120 let mag_refs: Vec<&[f64]> = mags.iter().map(|m| m.as_slice()).collect();
2121 bipolar_correction(&mut corrected, &mag_refs, &tes, &mask, sigma, &grid(nx, ny, nz));
2122 let unwrapped = unwrap_romeo_multi_echo(
2123 &corrected, &mag_refs, &tes, &mask,
2124 &RomeoParams::default(), &grid(nx, ny, nz),
2125 );
2126 let b0 = calculate_b0_weighted(&unwrapped, &mags, &tes, &mask, B0WeightType::PhaseSNR, &grid(n, 1, 1));
2127
2128 assert_eq!(b0.len(), n);
2129 for v in &b0 {
2130 assert!(v.is_finite(), "B0 with bipolar correction should be finite");
2131 }
2132 }
2133
2134 fn synthetic_coils(nx: usize, ny: usize, nz: usize, tes: &[f64]) -> (Vec<Vec<Vec<f64>>>, Vec<Vec<Vec<f64>>>, Vec<f64>) {
2139 let n = nx * ny * nz;
2140 let offsets: Vec<Box<dyn Fn(usize, usize, usize) -> f64>> = vec![
2141 Box::new(|x, _, _| 0.3 + 0.01 * x as f64),
2142 Box::new(|_, y, _| -1.0 + 0.02 * y as f64),
2143 ];
2144 let mut field = vec![0.0; n]; let mut phases = Vec::new();
2146 let mut mags = Vec::new();
2147 for (c, po) in offsets.iter().enumerate() {
2148 let mut cp = Vec::new();
2149 let mut cm = Vec::new();
2150 for &te in tes {
2151 let mut p = vec![0.0; n];
2152 let mut m = vec![0.0; n];
2153 for z in 0..nz { for y in 0..ny { for x in 0..nx {
2154 let i = x + y * nx + z * nx * ny;
2155 let f = 2.0 + 1.5 * (x as f64 / nx as f64) - 1.0 * (z as f64 / nz as f64);
2156 field[i] = f;
2157 p[i] = wrap_to_pi(2.0 * PI * f * te + po(x, y, z));
2158 m[i] = 100.0 * (1.0 + 0.3 * (if c == 0 { x as f64 / nx as f64 } else { y as f64 / ny as f64 }));
2159 }}}
2160 cp.push(p);
2161 cm.push(m);
2162 }
2163 phases.push(cp);
2164 mags.push(cm);
2165 }
2166 (phases, mags, field)
2167 }
2168
2169 #[test]
2170 fn test_mcpc3ds_combine_removes_coil_offsets() {
2171 let (nx, ny, nz) = (20, 20, 10);
2172 let tes = [0.005, 0.010];
2173 let g = grid(nx, ny, nz);
2174 let (phases, mags, field) = synthetic_coils(nx, ny, nz, &tes);
2175 let res = mcpc3ds_combine(&phases, &mags, &tes, [2.0, 2.0, 2.0], [0, 1], UnwrapMethod::Romeo, &g);
2176 assert_eq!(res.phases.len(), 2);
2177 assert_eq!(res.magnitudes.len(), 2);
2178 let n_mask = res.mask.iter().filter(|&&m| m > 0).count();
2179 assert!(n_mask > n_mask_min(nx, ny, nz), "robust mask should cover the object: {}", n_mask);
2180 let mut checked = 0;
2182 for z in 3..nz - 3 { for y in 4..ny - 4 { for x in 4..nx - 4 {
2183 let i = x + y * nx + z * nx * ny;
2184 if res.mask[i] == 0 { continue; }
2185 for (e, &te) in tes.iter().enumerate() {
2186 let expected = wrap_to_pi(2.0 * PI * field[i] * te);
2187 let err = wrap_to_pi(res.phases[e][i] - expected).abs();
2188 assert!(err < 0.05, "echo {} voxel ({},{},{}): got {:.4}, expected {:.4}", e, x, y, z, res.phases[e][i], expected);
2189 }
2190 checked += 1;
2191 }}}
2192 assert!(checked > 100, "too few interior voxels checked: {}", checked);
2193 for i in 0..nx * ny * nz {
2195 let m0 = mags[0][0][i];
2196 let m1 = mags[1][0][i];
2197 let expected = (m0 * m0 + m1 * m1).sqrt();
2198 assert!((res.magnitudes[0][i] - expected).abs() / expected < 0.02, "magnitude at {}: {} vs {}", i, res.magnitudes[0][i], expected);
2199 }
2200 }
2201
2202 fn n_mask_min(nx: usize, ny: usize, nz: usize) -> usize { nx * ny * nz / 2 }
2203
2204 #[test]
2205 fn test_mcpc3ds_combine_single_coil_removes_offset_keeps_magnitude() {
2206 let (nx, ny, nz) = (24, 24, 12);
2207 let tes = [0.005, 0.010];
2208 let g = grid(nx, ny, nz);
2209 let (mut phases, mut mags, field) = synthetic_coils(nx, ny, nz, &tes);
2210 phases.truncate(1);
2211 mags.truncate(1);
2212 let res = mcpc3ds_combine(&phases, &mags, &tes, [2.0, 2.0, 2.0], [0, 1], UnwrapMethod::Romeo, &g);
2213 let n = nx * ny * nz;
2214 for e in 0..2 {
2215 for i in 0..n {
2216 assert!((res.magnitudes[e][i] - mags[0][e][i]).abs() < 1e-9);
2219 }
2220 }
2221 for i in 0..n {
2222 let d_in = wrap_to_pi(phases[0][1][i] - phases[0][0][i]);
2223 let d_out = wrap_to_pi(res.phases[1][i] - res.phases[0][i]);
2224 assert!(wrap_to_pi(d_out - d_in).abs() < 1e-9, "voxel {}: {} vs {}", i, d_out, d_in);
2225 }
2226 let mut checked = 0;
2228 for z in 3..nz - 3 { for y in 5..ny - 5 { for x in 5..nx - 5 {
2229 let i = x + y * nx + z * nx * ny;
2230 if res.mask[i] == 0 { continue; }
2231 let err = wrap_to_pi(res.phases[0][i] - 2.0 * PI * field[i] * tes[0]).abs();
2232 assert!(err < 0.05, "voxel ({},{},{}): {}", x, y, z, err);
2233 checked += 1;
2234 }}}
2235 assert!(checked > 100);
2236 }
2237
2238 #[test]
2239 fn test_mcpc3ds_combine_identical_coils_scale_magnitude() {
2240 let (nx, ny, nz) = (12, 12, 6);
2241 let tes = [0.004, 0.009];
2242 let g = grid(nx, ny, nz);
2243 let (mut phases, mut mags, _) = synthetic_coils(nx, ny, nz, &tes);
2244 phases.truncate(1);
2245 mags.truncate(1);
2246 let (p, m) = (phases[0].clone(), mags[0].clone());
2247 phases.push(p);
2248 mags.push(m);
2249 let res = mcpc3ds_combine(&phases, &mags, &tes, [2.0, 2.0, 2.0], [0, 1], UnwrapMethod::Laplacian, &g);
2250 for i in 0..nx * ny * nz {
2251 let expected = (2.0f64).sqrt() * mags[0][1][i];
2252 assert!((res.magnitudes[1][i] - expected).abs() < 1e-6);
2253 }
2254 }
2255
2256 #[test]
2259 fn test_gaussian_box_sizes_match_reference() {
2260 assert_eq!(gaussian_box_sizes(10.0, 4), vec![17, 17, 17, 19]);
2262 assert_eq!(gaussian_box_sizes(5.0, 4), vec![7, 9, 9, 9]);
2263 assert_eq!(gaussian_box_sizes(4.0, 4), vec![7, 7, 7, 7]);
2264 assert_eq!(gaussian_box_sizes(0.0, 4), vec![1, 1, 1, 1]);
2265 assert_eq!(gaussian_box_sizes(5.0, 3), vec![9, 9, 11]); assert_eq!(gaussian_box_sizes(2.0, 4), vec![3, 3, 3, 5]);
2267 }
2268
2269 #[test]
2270 fn test_nan_box_filter_line_constant_with_gap() {
2271 let mut line = vec![3.0; 30];
2273 for v in line.iter_mut().take(20).skip(12) { *v = f64::NAN; }
2274 let mut scratch = Vec::new();
2275 nan_box_filter_line(&mut line, 5, &mut scratch);
2276 for (i, v) in line.iter().enumerate() {
2277 if v.is_finite() {
2278 assert!((v - 3.0).abs() < 1e-12, "position {} = {}", i, v);
2279 }
2280 }
2281 assert!(line[12].is_finite() && line[13].is_finite(), "{:?}", &line[10..16]);
2283 assert!(line[16].is_nan());
2284 }
2285
2286 #[test]
2287 fn test_nan_box_filter_line_linear_ramp_interior() {
2288 let n = 40;
2289 let mut line: Vec<f64> = (0..n).map(|i| 0.5 * i as f64).collect();
2290 let mut scratch = Vec::new();
2291 nan_box_filter_line(&mut line, 7, &mut scratch);
2292 for i in 7..n - 3 {
2294 assert!((line[i] - 0.5 * i as f64).abs() < 1e-9, "position {} = {}", i, line[i]);
2295 }
2296 }
2297
2298 #[test]
2299 fn test_nan_box_smooth_3d_phase_constant_inside_mask() {
2300 let (nx, ny, nz) = (48, 48, 24);
2301 let g = grid(nx, ny, nz);
2302 let n = nx * ny * nz;
2303 let mut mask = vec![0u8; n];
2304 for z in 8..nz - 8 { for y in 16..ny - 16 { for x in 16..nx - 16 {
2305 mask[x + y * nx + z * nx * ny] = 1;
2306 }}}
2307 let phase = vec![1.2f64; n];
2308 let out = nan_box_smooth_3d_phase(&phase, [3.0, 3.0, 2.0], &mask, &g);
2309 let mut checked = 0;
2310 for i in 0..n {
2311 if mask[i] > 0 && out[i].is_finite() {
2312 assert!((out[i] - 1.2).abs() < 1e-9, "voxel {}: {}", i, out[i]);
2313 checked += 1;
2314 }
2315 }
2316 let n_mask = mask.iter().filter(|&&m| m > 0).count();
2317 assert_eq!(checked, n_mask, "every mask voxel must be defined and unchanged");
2318 assert!(out[0].is_nan());
2320 let outside_defined = (0..n).filter(|&i| mask[i] == 0 && out[i].is_finite()).count();
2321 let outside = (0..n).filter(|&i| mask[i] == 0).count();
2322 assert!(outside_defined < outside / 2, "{} of {}", outside_defined, outside);
2323 }
2324}