1use std::f64::consts::PI;
18
19use crate::region_grow::{grow_region_unwrap, grow_region_unwrap_from_visited, grow_region_unwrap_full};
20use crate::Grid;
21#[cfg(feature = "parallel")]
22use rayon::prelude::*;
23
24#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum RomeoWeightType {
29 Romeo,
32 Romeo2,
34 Romeo3,
36 Romeo4,
38 Romeo6,
40 BestPath,
42}
43
44impl RomeoWeightType {
45 fn weight_flags(&self) -> [bool; 6] {
49 match self {
50 RomeoWeightType::Romeo => [true, true, true, true, false, false],
51 RomeoWeightType::Romeo2 => [true, false, false, true, false, false],
52 RomeoWeightType::Romeo3 => [true, true, false, true, false, false],
53 RomeoWeightType::Romeo4 => [true, true, true, true, false, false],
54 RomeoWeightType::Romeo6 => [true, true, true, true, true, true],
55 RomeoWeightType::BestPath => [false; 6], }
57 }
58}
59
60#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
68#[derive(Clone, Debug)]
69pub struct RomeoParams {
70 pub phase_coherence: bool,
74 pub phase_gradient_coherence: bool,
77 pub phase_linearity: bool,
79 pub mag_coherence: bool,
81 pub mag_weight: bool,
84 pub mag_weight2: bool,
87 pub bestpath: bool,
90
91 pub template: usize,
96 pub individual: bool,
98 pub correct_global: bool,
101 pub temporal_uncertain_unwrapping: f64,
104 pub max_seeds: u8,
106 pub merge_regions: bool,
108 pub correct_regions: bool,
110 pub wrap_addition: f64,
113}
114
115impl Default for RomeoParams {
116 fn default() -> Self {
117 Self {
118 phase_coherence: true,
120 phase_gradient_coherence: true,
121 phase_linearity: true,
122 mag_coherence: true,
123 mag_weight: false,
124 mag_weight2: false,
125 bestpath: false,
126 template: 0,
127 individual: false,
128 correct_global: false,
129 temporal_uncertain_unwrapping: 0.5,
130 max_seeds: 1,
131 merge_regions: false,
132 correct_regions: false,
133 wrap_addition: 0.0,
134 }
135 }
136}
137
138impl RomeoParams {
139 pub fn from_weight_type(wt: RomeoWeightType) -> Self {
141 if wt == RomeoWeightType::BestPath {
142 return Self { bestpath: true, ..Default::default() };
143 }
144 let f = wt.weight_flags();
145 Self {
146 phase_coherence: f[0],
147 phase_gradient_coherence: f[1],
148 phase_linearity: f[2],
149 mag_coherence: f[3],
150 mag_weight: f[4],
151 mag_weight2: f[5],
152 ..Default::default()
153 }
154 }
155
156 pub fn weight_flags(&self) -> [bool; 6] {
158 [
159 self.phase_coherence,
160 self.phase_gradient_coherence,
161 self.phase_linearity,
162 self.mag_coherence,
163 self.mag_weight,
164 self.mag_weight2,
165 ]
166 }
167}
168
169const TWO_PI: f64 = 2.0 * PI;
170
171#[inline]
173fn wrap_angle(angle: f64) -> f64 {
174 let mut a = angle % TWO_PI;
175 if a > PI {
176 a -= TWO_PI;
177 } else if a < -PI {
178 a += TWO_PI;
179 }
180 a
181}
182
183#[inline(always)]
185fn idx3d(i: usize, j: usize, k: usize, nx: usize, ny: usize) -> usize {
186 i + j * nx + k * nx * ny
187}
188
189pub fn calculate_weights_romeo(
195 phase: &[f64],
196 mag: &[f64],
197 phase2: Option<&[f64]>,
198 te1: f64,
199 te2: f64,
200 mask: &[u8],
201 nx: usize, ny: usize, nz: usize,
202) -> Vec<u8> {
203 calculate_weights_romeo_with_flags(
204 phase, mag, phase2, te1, te2, mask, nx, ny, nz,
205 RomeoWeightType::Romeo.weight_flags(),
206 )
207}
208
209pub fn calculate_weights_romeo_configurable(
213 phase: &[f64],
214 mag: &[f64],
215 phase2: Option<&[f64]>,
216 te1: f64,
217 te2: f64,
218 mask: &[u8],
219 nx: usize, ny: usize, nz: usize,
220 use_phase_gradient_coherence: bool,
221 use_mag_coherence: bool,
222 use_mag_weight: bool,
223) -> Vec<u8> {
224 let flags = [
225 true, use_phase_gradient_coherence,
227 false, use_mag_coherence,
229 use_mag_weight,
230 false, ];
232 calculate_weights_romeo_with_flags(
233 phase, mag, phase2, te1, te2, mask, nx, ny, nz, flags,
234 )
235}
236
237pub fn calculate_weights_romeo_with_flags(
248 phase: &[f64],
249 mag: &[f64],
250 phase2: Option<&[f64]>,
251 te1: f64,
252 te2: f64,
253 mask: &[u8],
254 nx: usize, ny: usize, nz: usize,
255 flags: [bool; 6],
256) -> Vec<u8> {
257 let n_total = nx * ny * nz;
258 let mut weights = vec![0u8; 3 * n_total];
259
260 let has_mag = !mag.is_empty();
261 let has_phase2 = phase2.is_some();
262 let te_ratio = if te2.abs() > 1e-10 { te1 / te2 } else { 1.0 };
263
264 let f_mc = flags[3] && has_mag;
266 let f_mw = flags[4] && has_mag;
267 let f_mw2 = flags[5] && has_mag;
268 let f_pgc = flags[1] && has_phase2;
270 let f_pl = flags[2];
271
272 let max_mag = if has_mag && (f_mw || f_mw2) {
274 percentile_95(mag, mask)
275 } else {
276 1.0
277 };
278 let half_max_mag = 0.5 * max_mag + 1e-12;
279
280 for dim in 0..3_usize {
281 let block = &mut weights[dim * n_total..(dim + 1) * n_total];
284 maybe_par_iter_mut!(block).enumerate().for_each(|(idx, w)| {
285 let i = idx % nx;
286 let j = (idx / nx) % ny;
287 let k = idx / (nx * ny);
288 let (ni, nj, nk) = match dim {
289 0 => (i + 1, j, k),
290 1 => (i, j + 1, k),
291 _ => (i, j, k + 1),
292 };
293
294 if ni >= nx || nj >= ny || nk >= nz {
295 return;
296 }
297
298 let idx_n = idx3d(ni, nj, nk, nx, ny);
299
300 if mask[idx] == 0 || mask[idx_n] == 0 {
301 return;
302 }
303
304 let mut weight = 1.0_f64;
305
306 if flags[0] {
308 let pc = 1.0 - wrap_angle(phase[idx_n] - phase[idx]).abs() / PI;
309 weight *= 0.1 + 0.9 * pc;
310 }
311
312 if f_pgc {
314 let phase2_data = phase2.unwrap();
315 let wrapped_p1 = wrap_angle(phase[idx_n] - phase[idx]);
316 let wrapped_p2 = wrap_angle(phase2_data[idx_n] - phase2_data[idx]);
317 let pgc = (1.0 - (wrapped_p1 - wrapped_p2 * te_ratio).abs()).max(0.0);
318 weight *= 0.1 + 0.9 * pgc;
319 }
320
321 if f_pl {
323 let pl = phase_linearity_edge(phase, i, j, k, ni, nj, nk, dim, nx, ny, nz);
324 weight *= 0.1 + 0.9 * pl;
325 }
326
327 if has_mag {
328 let m1 = mag[idx];
329 let m2 = mag[idx_n];
330 let small = m1.min(m2);
331 let big = m1.max(m2);
332
333 if f_mc {
335 let mc = if big > 1e-12 { (small / big).powi(2) } else { 0.0 };
336 weight *= 0.1 + 0.9 * mc;
337 }
338
339 if f_mw {
341 let mw = 0.5 + 0.5 * (small / half_max_mag).min(1.0);
342 weight *= 0.1 + 0.9 * mw;
343 }
344
345 if f_mw2 {
347 let mw2 = 0.5 + 0.5 * (half_max_mag / big.max(1e-12)).min(1.0);
348 weight *= 0.1 + 0.9 * mw2;
349 }
350 }
351
352 *w = rescale_weight(weight);
354 });
355 }
356
357 weights
358}
359
360fn phase_linearity_edge(
365 phase: &[f64],
366 i: usize, j: usize, k: usize,
367 ni: usize, nj: usize, nk: usize,
368 _dim: usize,
369 nx: usize, ny: usize, nz: usize,
370) -> f64 {
371 let idx = idx3d(i, j, k, nx, ny);
372 let idx_n = idx3d(ni, nj, nk, nx, ny);
373
374 let (hi, hj, hk) = (2 * i as i32 - ni as i32, 2 * j as i32 - nj as i32, 2 * k as i32 - nk as i32);
376 let pl1 = if hi >= 0 && hi < nx as i32 && hj >= 0 && hj < ny as i32 && hk >= 0 && hk < nz as i32 {
377 let h_idx = idx3d(hi as usize, hj as usize, hk as usize, nx, ny);
378 phase_linearity_triplet(phase[h_idx], phase[idx], phase[idx_n])
379 } else {
380 0.9
381 };
382
383 let (ki, kj, kk) = (2 * ni as i32 - i as i32, 2 * nj as i32 - j as i32, 2 * nk as i32 - k as i32);
385 let pl2 = if ki >= 0 && ki < nx as i32 && kj >= 0 && kj < ny as i32 && kk >= 0 && kk < nz as i32 {
386 let k_idx = idx3d(ki as usize, kj as usize, kk as usize, nx, ny);
387 phase_linearity_triplet(phase[idx], phase[idx_n], phase[k_idx])
388 } else {
389 0.9
390 };
391
392 pl1 * pl2
393}
394
395#[inline]
400fn phase_linearity_triplet(a: f64, b: f64, c: f64) -> f64 {
401 let second_deriv = wrap_angle(a - 2.0 * b + c);
402 let pl = (1.0 - (second_deriv / 2.0).abs()).max(0.0);
403 if pl.is_nan() { 0.5 } else { pl }
404}
405
406#[inline]
410fn rescale_weight(w: f64) -> u8 {
411 if w > 0.0 && w <= 1.0 {
412 (w * 254.0).round() as u8 + 1 } else if w > 1.0 {
414 255
415 } else {
416 0
417 }
418}
419
420fn percentile_95(mag: &[f64], mask: &[u8]) -> f64 {
422 let mut values: Vec<f64> = mag.iter().enumerate()
423 .filter(|(i, v)| mask[*i] > 0 && v.is_finite())
424 .map(|(_, &v)| v)
425 .collect();
426 if values.is_empty() {
427 return 1.0;
428 }
429 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
430 let idx = ((values.len() as f64 * 0.95) as usize).min(values.len() - 1);
431 values[idx]
432}
433
434pub fn calculate_weights_bestpath(
440 phase: &[f64],
441 mask: &[u8],
442 nx: usize, ny: usize, nz: usize,
443) -> Vec<u8> {
444 let n_total = nx * ny * nz;
445
446 let strides = [1_i64, nx as i64, (nx * ny) as i64];
449 let mut neighbor_offsets: Vec<i64> = Vec::new();
450 for dx in -1i64..=1 {
451 for dy in -1i64..=1 {
452 for dz in -1i64..=1 {
453 if dx == 0 && dy == 0 && dz == 0 { continue; }
454 let offset = (dx * strides[0] + dy * strides[1] + dz * strides[2]).abs();
455 if !neighbor_offsets.contains(&offset) {
456 neighbor_offsets.push(offset);
457 }
458 }
459 }
460 }
461
462 let mut d2 = vec![0.0_f64; n_total];
463 for &n in &neighbor_offsets {
464 let nu = n as usize;
465 for i in nu..(n_total - nu) {
466 let v1 = wrap_angle(phase[i - nu] - phase[i]);
467 let v2 = wrap_angle(phase[i] - phase[i + nu]);
468 let diff = v1 - v2;
469 d2[i] += diff * diff;
470 }
471 }
472
473 let r: Vec<f64> = d2.iter().map(|&v| {
475 let d = v.sqrt();
476 if d > 1e-12 { 1.0 / d } else { 0.0 }
477 }).collect();
478
479 let mut weights = vec![0u8; 3 * n_total];
481 for dim in 0..3_usize {
482 for i in 0..nx {
483 for j in 0..ny {
484 for k in 0..nz {
485 let (ni, nj, nk) = match dim {
486 0 => (i + 1, j, k),
487 1 => (i, j + 1, k),
488 _ => (i, j, k + 1),
489 };
490 if ni >= nx || nj >= ny || nk >= nz { continue; }
491
492 let idx = idx3d(i, j, k, nx, ny);
493 let idx_n = idx3d(ni, nj, nk, nx, ny);
494
495 if mask[idx] == 0 || mask[idx_n] == 0 { continue; }
496
497 let w = r[idx] + r[idx_n];
498 let scaled = ((1.0 - w / 10.0) * 255.0).round().clamp(1.0, 255.0) as u8;
501 let edge_idx = dim * n_total + idx3d(i, j, k, nx, ny);
502 weights[edge_idx] = scaled;
503 }
504 }
505 }
506 }
507
508 weights
509}
510
511pub fn calculate_weights_single_echo(
513 phase: &[f64],
514 mag: &[f64],
515 mask: &[u8],
516 nx: usize, ny: usize, nz: usize,
517) -> Vec<u8> {
518 calculate_weights_romeo(phase, mag, None, 1.0, 1.0, mask, nx, ny, nz)
519}
520
521#[allow(clippy::erasing_op)]
541pub fn voxel_quality_romeo(
542 phase: &[f64],
543 mag: &[f64],
544 phase2: Option<&[f64]>,
545 te1: f64,
546 te2: f64,
547 mask: &[u8],
548 grid: &Grid,
549) -> Vec<f64> {
550 let (nx, ny, nz) = grid.dims;
551 let n_total = nx * ny * nz;
552 let weights = calculate_weights_romeo(phase, mag, phase2, te1, te2, mask, nx, ny, nz);
553
554 let mut quality = vec![0.0_f64; n_total];
555
556 maybe_par_iter_mut!(quality).enumerate().for_each(|(idx, q)| {
559 if mask[idx] == 0 {
560 return;
561 }
562 let i = idx % nx;
563 let j = (idx / nx) % ny;
564 let k = idx / (nx * ny);
565
566 let mut sum = 0.0_f64;
567 let mut count = 0u32;
568
569 if i + 1 < nx && mask[idx3d(i + 1, j, k, nx, ny)] != 0 {
571 sum += weights[idx] as f64;
572 count += 1;
573 }
574 if i > 0 && mask[idx3d(i - 1, j, k, nx, ny)] != 0 {
576 sum += weights[idx3d(i - 1, j, k, nx, ny)] as f64;
577 count += 1;
578 }
579 if j + 1 < ny && mask[idx3d(i, j + 1, k, nx, ny)] != 0 {
581 sum += weights[n_total + idx] as f64;
582 count += 1;
583 }
584 if j > 0 && mask[idx3d(i, j - 1, k, nx, ny)] != 0 {
586 sum += weights[n_total + idx3d(i, j - 1, k, nx, ny)] as f64;
587 count += 1;
588 }
589 if k + 1 < nz && mask[idx3d(i, j, k + 1, nx, ny)] != 0 {
591 sum += weights[2 * n_total + idx] as f64;
592 count += 1;
593 }
594 if k > 0 && mask[idx3d(i, j, k - 1, nx, ny)] != 0 {
596 sum += weights[2 * n_total + idx3d(i, j, k - 1, nx, ny)] as f64;
597 count += 1;
598 }
599
600 if count > 0 {
601 *q = (sum / count as f64) / 255.0 * 100.0;
603 }
604 });
605
606 quality
607}
608
609pub fn find_seed_point(mask: &[u8], nx: usize, ny: usize, nz: usize) -> (usize, usize, usize) {
615 let mut sum_i = 0usize;
616 let mut sum_j = 0usize;
617 let mut sum_k = 0usize;
618 let mut count = 0usize;
619
620 for k in 0..nz {
621 for j in 0..ny {
622 for i in 0..nx {
623 let idx = idx3d(i, j, k, nx, ny);
624 if mask[idx] > 0 {
625 sum_i += i;
626 sum_j += j;
627 sum_k += k;
628 count += 1;
629 }
630 }
631 }
632 }
633
634 if count == 0 {
635 return (nx / 2, ny / 2, nz / 2);
636 }
637
638 (sum_i / count, sum_j / count, sum_k / count)
639}
640
641fn compute_weights_from_params(
643 params: &RomeoParams,
644 phase: &[f64],
645 mag: &[f64],
646 phase2: Option<&[f64]>,
647 te1: f64, te2: f64,
648 mask: &[u8],
649 nx: usize, ny: usize, nz: usize,
650) -> Vec<u8> {
651 if params.bestpath {
652 calculate_weights_bestpath(phase, mask, nx, ny, nz)
653 } else {
654 calculate_weights_romeo_with_flags(
655 phase, mag, phase2, te1, te2, mask, nx, ny, nz,
656 params.weight_flags(),
657 )
658 }
659}
660
661pub fn unwrap_romeo(
677 phase: &[f64],
678 mag: &[f64],
679 phase2: Option<&[f64]>,
680 te1: f64,
681 te2: f64,
682 mask: &[u8],
683 params: &RomeoParams,
684 grid: &Grid,
685) -> Vec<f64> {
686 let (nx, ny, nz) = grid.dims;
687 let weights = compute_weights_from_params(
688 params, phase, mag, phase2, te1, te2,
689 mask, nx, ny, nz,
690 );
691
692 let mut unwrapped = phase.to_vec();
693 let n_total = nx * ny * nz;
694
695 if params.max_seeds > 1 || params.wrap_addition > 0.0 || params.merge_regions || params.correct_regions {
696 let mut visited = vec![0u8; n_total];
698 let num_regions = grow_region_unwrap_full(
699 &mut unwrapped, &weights, mask, &mut visited,
700 nx, ny, nz, params.wrap_addition, params.max_seeds,
701 phase2, if phase2.is_some() { Some((te1, te2)) } else { None },
702 );
703
704 if params.merge_regions && num_regions > 1 {
705 let remaining = merge_regions_post(
706 &mut unwrapped, &mut visited, num_regions, &weights, nx, ny, nz,
707 );
708 if params.correct_regions {
709 for &r in &remaining {
711 correct_regions(&mut unwrapped, &visited, r);
712 }
713 }
714 } else if params.correct_regions && num_regions > 0 {
715 correct_regions(&mut unwrapped, &visited, num_regions);
716 }
717 } else {
718 let (seed_i, seed_j, seed_k) = find_seed_point(mask, nx, ny, nz);
720 let mut work_mask = mask.to_vec();
721 grow_region_unwrap(
722 &mut unwrapped, &weights, &mut work_mask,
723 nx, ny, nz, seed_i, seed_j, seed_k,
724 );
725 }
726
727 if params.correct_global {
729 correct_global_offset(&mut unwrapped, mask);
730 }
731
732 unwrapped
733}
734
735pub fn unwrap_romeo_multi_echo<P: AsRef<[f64]> + Sync, M: AsRef<[f64]> + Sync>(
758 phases: &[P],
759 mags: &[M],
760 tes: &[f64],
761 mask: &[u8],
762 params: &RomeoParams,
763 grid: &Grid,
764) -> Vec<Vec<f64>> {
765 let (nx, ny, nz) = grid.dims;
766 let n_echoes = phases.len();
767 assert!(n_echoes > 0, "phases must have at least one echo");
768 assert_eq!(n_echoes, tes.len(), "phases and tes must have same length");
769
770 if n_echoes == 1 {
771 let mag = if mags.is_empty() { &[] as &[f64] } else { mags[0].as_ref() };
772 return vec![unwrap_romeo(
773 phases[0].as_ref(), mag, None, 0.0, 0.0,
774 mask, params, grid,
775 )];
776 }
777
778 if params.individual {
779 unwrap_individual(phases, mags, tes, mask, params, nx, ny, nz)
780 } else {
781 unwrap_template(phases, mags, tes, mask, params, nx, ny, nz)
782 }
783}
784
785fn unwrap_template<P: AsRef<[f64]>, M: AsRef<[f64]>>(
795 phases: &[P],
796 mags: &[M],
797 tes: &[f64],
798 mask: &[u8],
799 params: &RomeoParams,
800 nx: usize, ny: usize, nz: usize,
801) -> Vec<Vec<f64>> {
802 let n_echoes = phases.len();
803 let n_total = nx * ny * nz;
804 let template = params.template.min(n_echoes - 1);
805
806 let p2ref = if template == 0 { 1 } else { template - 1 };
808
809 let template_mag = if mags.is_empty() { &[] as &[f64] } else { mags[template].as_ref() };
811 let weights = compute_weights_from_params(
812 params,
813 phases[template].as_ref(),
814 template_mag,
815 Some(phases[p2ref].as_ref()),
816 tes[template], tes[p2ref],
817 mask, nx, ny, nz,
818 );
819
820 let mut result: Vec<Vec<f64>> = phases.iter().map(|p| p.as_ref().to_vec()).collect();
822 if params.wrap_addition > 0.0 {
823 let mut visited = vec![0u8; n_total];
824 grow_region_unwrap_full(
825 &mut result[template], &weights, mask, &mut visited,
826 nx, ny, nz, params.wrap_addition, 1,
827 Some(phases[p2ref].as_ref()), Some((tes[template], tes[p2ref])),
828 );
829 } else {
830 let (seed_i, seed_j, seed_k) = find_seed_point(mask, nx, ny, nz);
831 let mut work_mask = mask.to_vec();
832 grow_region_unwrap(
833 &mut result[template], &weights, &mut work_mask,
834 nx, ny, nz, seed_i, seed_j, seed_k,
835 );
836 }
837
838 let echo_order: Vec<usize> = (0..template).rev()
842 .chain((template + 1)..n_echoes)
843 .collect();
844
845 for ieco in echo_order {
846 let iref = if ieco < template { ieco + 1 } else { ieco - 1 };
847 let te_ratio = tes[ieco] / tes[iref];
848
849 for i in 0..n_total {
851 if mask[i] > 0 {
852 let ref_value = result[iref][i] * te_ratio;
853 result[ieco][i] = unwrap_voxel(result[ieco][i], ref_value);
854 }
855 }
856
857 if params.temporal_uncertain_unwrapping > 0.0 {
859 let ref_scaled: Vec<f64> = (0..n_total).map(|i| {
861 if mask[i] > 0 { result[iref][i] * te_ratio } else { 0.0 }
862 }).collect();
863
864 temporal_uncertain_rewrap(
865 &mut result[ieco],
866 &ref_scaled,
867 &weights,
868 mask,
869 params.temporal_uncertain_unwrapping,
870 nx, ny, nz,
871 );
872 }
873 }
874
875 result
876}
877
878fn unwrap_individual<P: AsRef<[f64]> + Sync, M: AsRef<[f64]> + Sync>(
888 phases: &[P],
889 mags: &[M],
890 tes: &[f64],
891 mask: &[u8],
892 params: &RomeoParams,
893 nx: usize, ny: usize, nz: usize,
894) -> Vec<Vec<f64>> {
895 let n_echoes = phases.len();
896 let (seed_i, seed_j, seed_k) = find_seed_point(mask, nx, ny, nz);
897
898 let echo_indices: Vec<usize> = (0..n_echoes).collect();
902 let mut result: Vec<Vec<f64>> = maybe_par_iter!(echo_indices)
903 .map(|&i| {
904 let e2 = if i == 0 { 1 } else { i - 1 };
906
907 let mag = if mags.is_empty() { &[] as &[f64] } else { mags[i].as_ref() };
908 let weights = compute_weights_from_params(
909 params,
910 phases[i].as_ref(),
911 mag,
912 Some(phases[e2].as_ref()),
913 tes[i], tes[e2],
914 mask, nx, ny, nz,
915 );
916
917 let mut unwrapped = phases[i].as_ref().to_vec();
918 let mut work_mask = mask.to_vec();
919 grow_region_unwrap(
920 &mut unwrapped, &weights, &mut work_mask,
921 nx, ny, nz, seed_i, seed_j, seed_k,
922 );
923 unwrapped
924 })
925 .collect();
926
927 if params.correct_global {
928 correct_multi_echo_wraps(&mut result, tes, mask);
929 }
930
931 result
932}
933
934#[inline]
942fn unwrap_voxel(new: f64, old: f64) -> f64 {
943 new - TWO_PI * ((new - old) / TWO_PI).round()
944}
945
946pub fn correct_multi_echo_wraps(
953 phases: &mut [Vec<f64>],
954 tes: &[f64],
955 mask: &[u8],
956) {
957 let n_total = phases[0].len();
958
959 for ieco in 1..phases.len() {
960 let iref = ieco - 1;
961 let te_ratio = tes[ieco] / tes[iref];
962
963 let mut wrap_counts: Vec<f64> = Vec::new();
965 for i in 0..n_total {
966 if mask[i] > 0 {
967 let expected = phases[iref][i] * te_ratio;
968 let nwraps = ((expected - phases[ieco][i]) / TWO_PI).round();
969 if nwraps.is_finite() {
970 wrap_counts.push(nwraps);
971 }
972 }
973 }
974
975 if wrap_counts.is_empty() {
976 continue;
977 }
978
979 wrap_counts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
981 let median_wraps = wrap_counts[wrap_counts.len() / 2];
982
983 if median_wraps.abs() > 0.1 {
984 let correction = TWO_PI * median_wraps;
985 for i in 0..n_total {
986 if mask[i] > 0 {
987 phases[ieco][i] += correction;
988 }
989 }
990 }
991 }
992}
993
994fn correct_global_offset(phase: &mut [f64], mask: &[u8]) {
999 let mut wraps: Vec<f64> = phase.iter().enumerate()
1000 .filter(|(i, v)| mask[*i] > 0 && v.is_finite())
1001 .map(|(_, &v)| (v / TWO_PI).round())
1002 .collect();
1003
1004 if wraps.is_empty() {
1005 return;
1006 }
1007
1008 wraps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1009 let median_wraps = wraps[wraps.len() / 2];
1010
1011 if median_wraps.abs() > 0.1 {
1012 let correction = TWO_PI * median_wraps;
1013 for i in 0..phase.len() {
1014 if mask[i] > 0 {
1015 phase[i] -= correction;
1016 }
1017 }
1018 }
1019}
1020
1021fn correct_regions(phase: &mut [f64], visited: &[u8], num_regions: u8) {
1025 for region in 1..=num_regions {
1026 let mut wraps: Vec<f64> = Vec::new();
1027 for i in 0..phase.len() {
1028 if visited[i] == region && phase[i].is_finite() {
1029 wraps.push((phase[i] / TWO_PI).round());
1030 }
1031 }
1032 if wraps.is_empty() { continue; }
1033 wraps.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1034 let median_wraps = wraps[wraps.len() / 2];
1035 if median_wraps.abs() > 0.1 {
1036 let correction = TWO_PI * median_wraps;
1037 for i in 0..phase.len() {
1038 if visited[i] == region {
1039 phase[i] -= correction;
1040 }
1041 }
1042 }
1043 }
1044}
1045
1046fn merge_regions_post(
1052 phase: &mut [f64],
1053 visited: &mut [u8],
1054 num_regions: u8,
1055 weights: &[u8],
1056 nx: usize, ny: usize, nz: usize,
1057) -> Vec<u8> {
1058 let n_total = nx * ny * nz;
1059 let nr = num_regions as usize;
1060 if nr <= 1 { return vec![1]; }
1061
1062 let mut region_size = vec![0usize; nr + 1]; for &v in visited.iter() {
1065 if v > 0 && (v as usize) <= nr {
1066 region_size[v as usize] += 1;
1067 }
1068 }
1069
1070 let mut offsets = vec![0.0_f64; (nr + 1) * (nr + 1)];
1072 let mut offset_counts = vec![0i64; (nr + 1) * (nr + 1)];
1073 let flat = |r1: usize, r2: usize| r1 * (nr + 1) + r2;
1074
1075 for dim in 0..3_usize {
1076 for i in 0..nx {
1077 for j in 0..ny {
1078 for k in 0..nz {
1079 let (ni, nj, nk) = match dim {
1080 0 => (i + 1, j, k),
1081 1 => (i, j + 1, k),
1082 _ => (i, j, k + 1),
1083 };
1084 if ni >= nx || nj >= ny || nk >= nz { continue; }
1085 let idx = idx3d(i, j, k, nx, ny);
1086 let idx_n = idx3d(ni, nj, nk, nx, ny);
1087 let ri = visited[idx] as usize;
1088 let rj = visited[idx_n] as usize;
1089 if ri == 0 || rj == 0 || ri == rj { continue; }
1090
1091 let edge_idx = dim * n_total + idx3d(i, j, k, nx, ny);
1092 let w = 255u16.saturating_sub(weights[edge_idx] as u16);
1093 let w = if w == 255 { 0 } else { w as i64 };
1094
1095 offsets[flat(ri, rj)] += (phase[idx] - phase[idx_n]) * w as f64;
1096 offset_counts[flat(ri, rj)] += w;
1097 }
1098 }
1099 }
1100 }
1101
1102 for i in 1..=nr {
1104 for j in i..=nr {
1105 offset_counts[flat(i, j)] += offset_counts[flat(j, i)];
1106 offset_counts[flat(j, i)] = offset_counts[flat(i, j)];
1107 offsets[flat(i, j)] -= offsets[flat(j, i)];
1108 offsets[flat(j, i)] = -offsets[flat(i, j)];
1109 }
1110 }
1111
1112 let mut corrected = vec![false; nr + 1];
1114 let mut remaining = Vec::new();
1115
1116 while corrected[1..=nr].iter().any(|&c| !c) {
1117 let mut best_region = 0;
1119 let mut best_size = 0;
1120 for r in 1..=nr {
1121 if !corrected[r] && region_size[r] > best_size {
1122 best_size = region_size[r];
1123 best_region = r;
1124 }
1125 }
1126 if best_region == 0 { break; }
1127 corrected[best_region] = true;
1128 remaining.push(best_region as u8);
1129
1130 let mut merge_pairs: Vec<(usize, usize, i64)> = Vec::new();
1133 for i in 1..=nr {
1134 for j in 1..=nr {
1135 if corrected[i] && !corrected[j] && offset_counts[flat(i, j)] > 0 {
1136 merge_pairs.push((i, j, offset_counts[flat(i, j)]));
1137 }
1138 }
1139 }
1140 merge_pairs.sort_by(|a, b| b.2.cmp(&a.2));
1141
1142 for &(ri, rj, count) in &merge_pairs {
1143 if corrected[rj] { continue; }
1144 let offset = (offsets[flat(ri, rj)] / count as f64 / TWO_PI).round();
1145 if offset != 0.0 {
1146 let correction = offset * TWO_PI;
1147 for v in 0..n_total {
1148 if visited[v] == rj as u8 {
1149 phase[v] += correction;
1150 visited[v] = ri as u8;
1151 }
1152 }
1153 }
1154 corrected[rj] = true;
1155 offset_counts[flat(ri, rj)] = -1;
1156 offset_counts[flat(rj, ri)] = -1;
1157 }
1158 }
1159
1160 remaining
1161}
1162
1163fn temporal_uncertain_rewrap(
1171 phase: &mut [f64],
1172 ref_scaled: &[f64],
1173 weights: &[u8],
1174 mask: &[u8],
1175 threshold: f64,
1176 nx: usize, ny: usize, nz: usize,
1177) {
1178 let n_total = nx * ny * nz;
1179
1180 let half_phase: Vec<f64> = phase.iter().map(|&v| v * 0.5).collect();
1184 let half_ref: Vec<f64> = ref_scaled.iter().map(|&v| v * 0.5).collect();
1185
1186 let grid = crate::Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1187 let quality = voxel_quality_romeo(
1188 &half_phase, &[], Some(&half_ref), 1.0, 1.0,
1189 mask, &grid,
1190 );
1191
1192 let threshold_scaled = threshold * 100.0; let mut visited = vec![0u8; n_total];
1198 let mut any_uncertain = false;
1199 let mut any_certain = false;
1200
1201 for i in 0..n_total {
1202 if mask[i] == 0 {
1203 visited[i] = 0;
1204 } else if quality[i] > threshold_scaled {
1205 visited[i] = 2;
1206 any_certain = true;
1207 } else {
1208 visited[i] = 1;
1209 any_uncertain = true;
1210 }
1211 }
1212
1213 if !any_uncertain || !any_certain {
1214 return;
1215 }
1216
1217 grow_region_unwrap_from_visited(phase, weights, &mut visited, nx, ny, nz);
1219}
1220
1221#[cfg(test)]
1222mod tests {
1223 use super::*;
1224 use crate::Grid;
1225
1226 fn grid(n: usize) -> Grid { Grid::new(n, n, n, 1.0, 1.0, 1.0) }
1227
1228 #[test]
1229 fn test_wrap_angle() {
1230 assert!((wrap_angle(0.0) - 0.0).abs() < 1e-10);
1231 assert!((wrap_angle(PI) - PI).abs() < 1e-10);
1232 assert!((wrap_angle(-PI) - (-PI)).abs() < 1e-10);
1233 assert!((wrap_angle(TWO_PI) - 0.0).abs() < 1e-10);
1234 assert!((wrap_angle(3.0 * PI) - PI).abs() < 1e-10);
1235 assert!((wrap_angle(-3.0 * PI) - (-PI)).abs() < 1e-10);
1236 }
1237
1238 #[test]
1239 fn test_weights_constant_phase() {
1240 let n = 4;
1242 let phase = vec![0.0; n * n * n];
1243 let mag = vec![1.0; n * n * n];
1244 let mask = vec![1u8; n * n * n];
1245
1246 let weights = calculate_weights_single_echo(&phase, &mag, &mask, n, n, n);
1247
1248 let mut high_weight_count = 0;
1250 for &w in weights.iter() {
1251 if w == 255 {
1252 high_weight_count += 1;
1253 }
1254 }
1255 assert!(high_weight_count > 0, "Should have some high weights for constant phase");
1256 }
1257
1258 #[test]
1259 fn test_weights_wrapped_jump() {
1260 let n = 4;
1262 let mut phase = vec![0.0; n * n * n];
1263
1264 for i in 2..n {
1266 for j in 0..n {
1267 for k in 0..n {
1268 phase[idx3d(i, j, k, n, n)] = TWO_PI;
1269 }
1270 }
1271 }
1272
1273 let mask = vec![1u8; n * n * n];
1274 let _weights = calculate_weights_single_echo(&phase, &[], &mask, n, n, n);
1275
1276 }
1280
1281 #[test]
1282 fn test_weights_mask() {
1283 let n = 4;
1285 let phase = vec![0.5; n * n * n];
1286 let mut mask = vec![1u8; n * n * n];
1287
1288 mask[0] = 0;
1290 mask[1] = 0;
1291
1292 let weights = calculate_weights_single_echo(&phase, &[], &mask, n, n, n);
1293
1294 assert_eq!(weights[0], 0); }
1298
1299 #[test]
1300 fn test_voxel_quality_constant_phase() {
1301 let n = 4;
1303 let phase = vec![0.0; n * n * n];
1304 let mag = vec![1.0; n * n * n];
1305 let mask = vec![1u8; n * n * n];
1306
1307 let quality = voxel_quality_romeo(&phase, &mag, None, 1.0, 1.0, &mask, &grid(n));
1308
1309 assert_eq!(quality.len(), n * n * n);
1310
1311 let interior_q = quality[idx3d(1, 1, 1, n, n)];
1314 assert!(interior_q > 90.0,
1315 "Interior voxel quality should be >90, got {}", interior_q);
1316 }
1317
1318 #[test]
1319 fn test_voxel_quality_masked() {
1320 let n = 4;
1322 let phase = vec![0.0; n * n * n];
1323 let mut mask = vec![1u8; n * n * n];
1324 mask[idx3d(1, 1, 1, n, n)] = 0;
1325
1326 let quality = voxel_quality_romeo(&phase, &[], None, 1.0, 1.0, &mask, &grid(n));
1327
1328 assert_eq!(quality[idx3d(1, 1, 1, n, n)], 0.0,
1329 "Masked-out voxel should have quality 0");
1330 }
1331
1332 #[test]
1333 fn test_voxel_quality_range() {
1334 let n = 6;
1336 let phase: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.7).collect();
1337 let mag: Vec<f64> = (0..n * n * n).map(|i| (i as f64) / (n * n * n) as f64).collect();
1338 let mask = vec![1u8; n * n * n];
1339
1340 let quality = voxel_quality_romeo(&phase, &mag, None, 1.0, 1.0, &mask, &grid(n));
1341
1342 for &q in quality.iter() {
1343 assert!(q >= 0.0 && q <= 100.0,
1344 "Quality should be in [0, 100], got {}", q);
1345 }
1346 }
1347
1348 #[test]
1353 fn test_find_seed_point_full_mask() {
1354 let (nx, ny, nz) = (8, 8, 8);
1355 let mask = vec![1u8; nx * ny * nz];
1356 let (si, sj, sk) = find_seed_point(&mask, nx, ny, nz);
1357 assert_eq!(si, 3);
1358 assert_eq!(sj, 3);
1359 assert_eq!(sk, 3);
1360 }
1361
1362 #[test]
1363 fn test_find_seed_point_empty_mask() {
1364 let (nx, ny, nz) = (8, 8, 8);
1365 let mask = vec![0u8; nx * ny * nz];
1366 let (si, sj, sk) = find_seed_point(&mask, nx, ny, nz);
1367 assert_eq!(si, 4);
1368 assert_eq!(sj, 4);
1369 assert_eq!(sk, 4);
1370 }
1371
1372 #[test]
1377 fn test_unwrap_romeo_3d_smooth() {
1378 let n = 8;
1379 let total = n * n * n;
1380 let phase: Vec<f64> = (0..total).map(|i| {
1381 (i % n) as f64 / n as f64 * 0.5
1382 }).collect();
1383 let mag = vec![1.0; total];
1384 let mask = vec![1u8; total];
1385
1386 let unwrapped = unwrap_romeo(
1387 &phase, &mag, None, 0.0, 0.0,
1388 &mask, &RomeoParams::default(), &grid(n),
1389 );
1390
1391 assert_eq!(unwrapped.len(), total);
1392 for &v in &unwrapped {
1393 assert!(v.is_finite());
1394 }
1395 }
1396
1397 #[test]
1398 fn test_unwrap_romeo_3d_with_wrap() {
1399 let n = 8;
1401 let total = n * n * n;
1402 let mut phase = vec![0.0; total];
1403
1404 for i in 0..n {
1406 for j in 0..n {
1407 for k in 0..n {
1408 let idx = idx3d(i, j, k, n, n);
1409 let val = 0.1 * i as f64;
1410 phase[idx] = if i >= 4 { val + TWO_PI } else { val };
1411 }
1412 }
1413 }
1414
1415 let mask = vec![1u8; total];
1416 let unwrapped = unwrap_romeo(
1417 &phase, &[], None, 0.0, 0.0,
1418 &mask, &RomeoParams::default(), &grid(n),
1419 );
1420
1421 for j in 0..n {
1424 for k in 0..n {
1425 for i in 1..n {
1426 let curr = unwrapped[idx3d(i, j, k, n, n)];
1427 let prev = unwrapped[idx3d(i - 1, j, k, n, n)];
1428 let diff = (curr - prev).abs();
1429 assert!(diff < PI + 0.5,
1430 "Adjacent voxels differ by {:.2} at x={}", diff, i);
1431 }
1432 }
1433 }
1434 }
1435
1436 #[test]
1441 fn test_multi_echo_template_output_sizes() {
1442 let n = 8;
1443 let total = n * n * n;
1444 let tes = [5.0, 10.0, 15.0];
1445 let slope = 0.05;
1446
1447 let phases: Vec<Vec<f64>> = tes.iter().map(|&te| {
1448 (0..total).map(|i| {
1449 let x = (i % n) as f64;
1450 wrap_angle(slope * x * te)
1451 }).collect()
1452 }).collect();
1453 let mags: Vec<Vec<f64>> = tes.iter().map(|_| vec![1.0; total]).collect();
1454 let mask = vec![1u8; total];
1455
1456 let result = unwrap_romeo_multi_echo(
1457 &phases, &mags, &tes, &mask,
1458 &RomeoParams::default(), &grid(n),
1459 );
1460
1461 assert_eq!(result.len(), 3);
1462 for echo in &result {
1463 assert_eq!(echo.len(), total);
1464 for &v in echo {
1465 assert!(v.is_finite());
1466 }
1467 }
1468 }
1469
1470 #[test]
1471 fn test_multi_echo_template_temporal_consistency() {
1472 let n = 8;
1475 let total = n * n * n;
1476 let tes = [5.0, 10.0, 15.0];
1477 let slope = 0.03; let phases: Vec<Vec<f64>> = tes.iter().map(|&te| {
1480 (0..total).map(|i| {
1481 let x = (i % n) as f64;
1482 slope * x * te }).collect()
1484 }).collect();
1485 let mags: Vec<Vec<f64>> = tes.iter().map(|_| vec![1.0; total]).collect();
1486 let mask = vec![1u8; total];
1487
1488 let result = unwrap_romeo_multi_echo(
1489 &phases, &mags, &tes, &mask,
1490 &RomeoParams::default(), &grid(n),
1491 );
1492
1493 for i in 0..total {
1495 if mask[i] > 0 && result[0][i].abs() > 1e-10 {
1496 let ratio_12 = result[1][i] / result[0][i];
1497 let expected_ratio = tes[1] / tes[0];
1498 assert!((ratio_12 - expected_ratio).abs() < 0.5,
1499 "Echo 1/0 ratio {:.3} vs expected {:.3} at voxel {}",
1500 ratio_12, expected_ratio, i);
1501 }
1502 }
1503 }
1504
1505 #[test]
1506 fn test_multi_echo_single_echo_fallback() {
1507 let n = 4;
1508 let total = n * n * n;
1509 let phase = vec![0.1; total];
1510 let mask = vec![1u8; total];
1511
1512 let result = unwrap_romeo_multi_echo(
1513 &[&phase[..]], &[] as &[&[f64]], &[5.0], &mask,
1514 &RomeoParams::default(), &grid(n),
1515 );
1516
1517 assert_eq!(result.len(), 1);
1518 assert_eq!(result[0].len(), total);
1519 }
1520
1521 #[test]
1526 fn test_multi_echo_individual_mode() {
1527 let n = 8;
1528 let total = n * n * n;
1529 let tes = [5.0, 10.0, 15.0];
1530 let slope = 0.03;
1531
1532 let phases: Vec<Vec<f64>> = tes.iter().map(|&te| {
1533 (0..total).map(|i| {
1534 let x = (i % n) as f64;
1535 slope * x * te
1536 }).collect()
1537 }).collect();
1538 let mags: Vec<Vec<f64>> = tes.iter().map(|_| vec![1.0; total]).collect();
1539 let mask = vec![1u8; total];
1540
1541 let params = RomeoParams {
1542 individual: true,
1543 correct_global: true,
1544 ..Default::default()
1545 };
1546
1547 let result = unwrap_romeo_multi_echo(
1548 &phases, &mags, &tes, &mask,
1549 ¶ms, &grid(n),
1550 );
1551
1552 assert_eq!(result.len(), 3);
1553 for echo in &result {
1554 assert_eq!(echo.len(), total);
1555 for &v in echo {
1556 assert!(v.is_finite());
1557 }
1558 }
1559 }
1560
1561 #[test]
1566 fn test_correct_multi_echo_wraps_no_correction_needed() {
1567 let n = 64;
1568 let tes = [5.0, 10.0, 15.0];
1569 let mask = vec![1u8; n];
1570
1571 let mut phases: Vec<Vec<f64>> = tes.iter().map(|&te| {
1573 vec![0.1 * te; n]
1574 }).collect();
1575
1576 let original: Vec<Vec<f64>> = phases.iter().map(|p| p.clone()).collect();
1577 correct_multi_echo_wraps(&mut phases, &tes, &mask);
1578
1579 for e in 0..3 {
1580 for i in 0..n {
1581 assert!((phases[e][i] - original[e][i]).abs() < 1e-10,
1582 "No correction should be applied");
1583 }
1584 }
1585 }
1586
1587 #[test]
1588 fn test_correct_multi_echo_wraps_with_offset() {
1589 let n = 64;
1590 let tes = [5.0, 10.0];
1591 let mask = vec![1u8; n];
1592
1593 let mut phases = vec![
1596 vec![0.5; n],
1597 vec![1.0 + TWO_PI; n],
1598 ];
1599
1600 correct_multi_echo_wraps(&mut phases, &tes, &mask);
1601
1602 for i in 0..n {
1604 assert!((phases[1][i] - 1.0).abs() < 0.1,
1605 "Expected ~1.0, got {}", phases[1][i]);
1606 }
1607 }
1608
1609 #[test]
1614 fn test_weight_type_flags() {
1615 let f = RomeoWeightType::Romeo.weight_flags();
1617 assert_eq!(f, [true, true, true, true, false, false]);
1618
1619 let f = RomeoWeightType::Romeo2.weight_flags();
1621 assert_eq!(f, [true, false, false, true, false, false]);
1622
1623 let f = RomeoWeightType::Romeo6.weight_flags();
1625 assert_eq!(f, [true, true, true, true, true, true]);
1626 }
1627
1628 #[test]
1629 fn test_romeo4_is_romeo_alias() {
1630 assert_eq!(
1631 RomeoWeightType::Romeo.weight_flags(),
1632 RomeoWeightType::Romeo4.weight_flags(),
1633 );
1634 }
1635
1636 #[test]
1637 fn test_params_weight_flags_roundtrip() {
1638 let default_params = RomeoParams::default();
1640 assert_eq!(default_params.weight_flags(), RomeoWeightType::Romeo.weight_flags());
1641 assert!(!default_params.bestpath);
1642
1643 for wt in [RomeoWeightType::Romeo, RomeoWeightType::Romeo2,
1645 RomeoWeightType::Romeo3, RomeoWeightType::Romeo6] {
1646 let params = RomeoParams::from_weight_type(wt);
1647 assert_eq!(params.weight_flags(), wt.weight_flags());
1648 assert!(!params.bestpath);
1649 }
1650
1651 let bp = RomeoParams::from_weight_type(RomeoWeightType::BestPath);
1653 assert!(bp.bestpath);
1654 }
1655
1656 #[test]
1657 fn test_params_custom_weight_combination() {
1658 let params = RomeoParams {
1660 phase_coherence: true,
1661 phase_gradient_coherence: false,
1662 phase_linearity: false,
1663 mag_coherence: false,
1664 mag_weight: true,
1665 mag_weight2: false,
1666 ..Default::default()
1667 };
1668 assert_eq!(params.weight_flags(), [true, false, false, false, true, false]);
1669 }
1670
1671 #[test]
1676 fn test_multi_echo_template_selection() {
1677 let n = 8;
1678 let total = n * n * n;
1679 let tes = [5.0, 10.0, 15.0];
1680 let phases: Vec<Vec<f64>> = tes.iter().map(|&te| {
1681 (0..total).map(|i| 0.02 * (i % n) as f64 * te).collect()
1682 }).collect();
1683 let mags: Vec<Vec<f64>> = tes.iter().map(|_| vec![1.0; total]).collect();
1684 let mask = vec![1u8; total];
1685
1686 let params = RomeoParams {
1688 template: 1,
1689 ..Default::default()
1690 };
1691
1692 let result = unwrap_romeo_multi_echo(
1693 &phases, &mags, &tes, &mask,
1694 ¶ms, &grid(n),
1695 );
1696
1697 assert_eq!(result.len(), 3);
1698 for echo in &result {
1699 for &v in echo {
1700 assert!(v.is_finite());
1701 }
1702 }
1703 }
1704
1705 #[test]
1710 fn test_phase_linearity_triplet_linear() {
1711 assert!((phase_linearity_triplet(0.1, 0.2, 0.3) - 1.0).abs() < 1e-6);
1713 assert!((phase_linearity_triplet(-0.5, 0.0, 0.5) - 1.0).abs() < 1e-6);
1714 }
1715
1716 #[test]
1717 fn test_phase_linearity_triplet_jump() {
1718 let pl = phase_linearity_triplet(0.0, 0.0, PI);
1720 assert!(pl < 0.5, "Jump should give low linearity, got {}", pl);
1721 }
1722
1723 #[test]
1728 fn test_bestpath_weights_constant_phase() {
1729 let n = 6;
1730 let total = n * n * n;
1731 let phase = vec![0.0; total];
1732 let mask = vec![1u8; total];
1733
1734 let weights = calculate_weights_bestpath(&phase, &mask, n, n, n);
1735 assert_eq!(weights.len(), 3 * total);
1736
1737 }
1740
1741 #[test]
1742 fn test_bestpath_weights_smooth_gradient() {
1743 let n = 8;
1744 let total = n * n * n;
1745 let phase: Vec<f64> = (0..total).map(|i| 0.1 * (i % n) as f64).collect();
1747 let mask = vec![1u8; total];
1748
1749 let weights = calculate_weights_bestpath(&phase, &mask, n, n, n);
1750
1751 for &w in &weights {
1753 assert!(w <= 255);
1754 }
1755 }
1756
1757 #[test]
1762 fn test_unwrap_with_wrap_addition() {
1763 let n = 8;
1764 let total = n * n * n;
1765 let phase: Vec<f64> = (0..total).map(|i| {
1766 let x = (i % n) as f64;
1767 wrap_angle(0.1 * x)
1768 }).collect();
1769 let mask = vec![1u8; total];
1770
1771 let params = RomeoParams {
1772 wrap_addition: 1.0,
1773 ..Default::default()
1774 };
1775
1776 let unwrapped = unwrap_romeo(
1777 &phase, &[], None, 0.0, 0.0,
1778 &mask, ¶ms, &grid(n),
1779 );
1780
1781 for &v in &unwrapped {
1782 assert!(v.is_finite());
1783 }
1784 }
1785
1786 #[test]
1791 fn test_unwrap_multi_seed() {
1792 let n = 8;
1793 let total = n * n * n;
1794 let phase: Vec<f64> = (0..total).map(|i| {
1795 wrap_angle(0.05 * (i % n) as f64)
1796 }).collect();
1797 let mask = vec![1u8; total];
1798
1799 let params = RomeoParams {
1800 max_seeds: 4,
1801 ..Default::default()
1802 };
1803
1804 let unwrapped = unwrap_romeo(
1805 &phase, &[], None, 0.0, 0.0,
1806 &mask, ¶ms, &grid(n),
1807 );
1808
1809 for &v in &unwrapped {
1810 assert!(v.is_finite());
1811 }
1812 }
1813
1814 #[test]
1815 fn test_unwrap_multi_seed_with_merge() {
1816 let n = 8;
1817 let total = n * n * n;
1818 let phase: Vec<f64> = (0..total).map(|i| {
1819 wrap_angle(0.05 * (i % n) as f64)
1820 }).collect();
1821 let mask = vec![1u8; total];
1822
1823 let params = RomeoParams {
1824 max_seeds: 4,
1825 merge_regions: true,
1826 correct_regions: true,
1827 ..Default::default()
1828 };
1829
1830 let unwrapped = unwrap_romeo(
1831 &phase, &[], None, 0.0, 0.0,
1832 &mask, ¶ms, &grid(n),
1833 );
1834
1835 for &v in &unwrapped {
1836 assert!(v.is_finite());
1837 }
1838 }
1839
1840 #[test]
1845 fn test_correct_global_offset() {
1846 let n = 64;
1847 let mask = vec![1u8; n];
1848 let mut phase: Vec<f64> = vec![TWO_PI + 0.1; n];
1850
1851 correct_global_offset(&mut phase, &mask);
1852
1853 for &v in &phase {
1855 assert!((v - 0.1).abs() < 0.2, "Expected ~0.1, got {}", v);
1856 }
1857 }
1858
1859 #[test]
1860 fn test_unwrap_romeo_with_correctglobal() {
1861 let n = 4;
1862 let total = n * n * n;
1863 let phase = vec![0.1; total];
1864 let mask = vec![1u8; total];
1865
1866 let params = RomeoParams {
1867 correct_global: true,
1868 ..Default::default()
1869 };
1870
1871 let unwrapped = unwrap_romeo(
1872 &phase, &[], None, 0.0, 0.0,
1873 &mask, ¶ms, &grid(n),
1874 );
1875
1876 for &v in &unwrapped {
1877 assert!(v.is_finite());
1878 }
1879 }
1880
1881 #[test]
1886 fn test_unwrap_romeo_bestpath() {
1887 let n = 8;
1888 let total = n * n * n;
1889 let phase: Vec<f64> = (0..total).map(|i| {
1890 wrap_angle(0.1 * (i % n) as f64)
1891 }).collect();
1892 let mask = vec![1u8; total];
1893
1894 let params = RomeoParams {
1895 bestpath: true,
1896 ..Default::default()
1897 };
1898
1899 let unwrapped = unwrap_romeo(
1900 &phase, &[], None, 0.0, 0.0,
1901 &mask, ¶ms, &grid(n),
1902 );
1903
1904 for &v in &unwrapped {
1905 assert!(v.is_finite());
1906 }
1907 }
1908
1909 #[test]
1914 fn test_rescale_weight() {
1915 assert_eq!(rescale_weight(1.0), 255); assert_eq!(rescale_weight(0.5), 128); assert!(rescale_weight(0.001) >= 1); assert_eq!(rescale_weight(0.0), 0); assert_eq!(rescale_weight(-0.1), 0); }
1921}