1#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
51#[derive(Clone, Debug)]
52pub struct HomogeneityParams {
53 pub sigma_mm: f64,
55 pub nbox: usize,
64}
65
66impl Default for HomogeneityParams {
67 fn default() -> Self {
68 Self {
69 sigma_mm: 7.0,
70 nbox: 15,
71 }
72 }
73}
74
75use std::collections::VecDeque;
76use crate::error::InputError;
77use crate::Grid;
78use crate::utils::morphology::fill_holes;
79
80#[inline(always)]
82pub(crate) fn idx3d(i: usize, j: usize, k: usize, nx: usize, ny: usize) -> usize {
83 i + j * nx + k * nx * ny
84}
85
86#[inline]
93pub(crate) fn check_volume(name: &str, len: usize, grid: &Grid) -> usize {
94 let (nx, ny, nz) = grid.dims;
95 let n_total = nx * ny * nz;
96 assert_eq!(
97 len, n_total,
98 "{name} has {len} voxels but the grid is {nx}x{ny}x{nz} ({n_total} voxels)"
99 );
100 n_total
101}
102
103fn get_box_sizes(sigma: f64, n: usize) -> Vec<usize> {
112 if sigma <= 0.0 || n == 0 {
113 return vec![0; n];
114 }
115
116 let wideal = ((12.0 * sigma * sigma / n as f64) + 1.0).sqrt();
118
119 let wl_float = wideal - (wideal + 1.0) % 2.0;
121 let wl = wl_float.round_ties_even() as usize;
123 let wl = if wl.is_multiple_of(2) { wl + 1 } else { wl }; let wu = wl + 2;
125
126 let wl_f = wl as f64;
128 let n_f = n as f64;
129 let mideal = (12.0 * sigma * sigma - n_f * wl_f * wl_f - 4.0 * n_f * wl_f - 3.0 * n_f)
130 / (-4.0 * wl_f - 4.0);
131 let m = mideal.round_ties_even() as usize;
132
133 (0..n).map(|i| if i < m { wl } else { wu }).collect()
134}
135
136fn check_box_sizes(boxsizes: &mut [Vec<usize>], dims: &[usize]) {
138 for (dim, bs) in boxsizes.iter_mut().enumerate() {
139 if dim >= dims.len() {
140 continue;
141 }
142 for b in bs.iter_mut() {
143 if *b % 2 == 0 {
145 *b += 1;
146 }
147 let max_size = dims[dim] / 2;
149 if *b > max_size {
150 *b = if max_size.is_multiple_of(2) { max_size + 1 } else { max_size };
151 }
152 }
153 }
154}
155
156fn box_filter_line(line: &mut [f64], boxsize: usize) {
160 if boxsize < 3 || line.len() < boxsize {
161 return;
162 }
163
164 let n = line.len();
165 let r = boxsize / 2;
166
167 let mut queue: VecDeque<f64> = VecDeque::with_capacity(boxsize);
169
170 let mut lsum: f64 = line[..r].iter().sum();
172 for i in 0..r {
173 queue.push_back(line[i]);
174 }
175
176 for i in 0..=r {
178 lsum += line[i + r];
179 queue.push_back(line[i + r]);
180 line[i] = lsum / (r + i + 1) as f64;
181 }
182
183 for i in (r + 1)..(n - r) {
185 let old = queue.pop_front().unwrap();
186 lsum += line[i + r] - old;
187 queue.push_back(line[i + r]);
188 line[i] = lsum / boxsize as f64;
189 }
190
191 for i in (n - r)..n {
193 let old = queue.pop_front().unwrap();
194 lsum -= old;
195 line[i] = lsum / (r + n - i) as f64;
196 }
197}
198
199fn box_filter_line_weighted(line: &mut [f64], weight: &mut [f32], boxsize: usize) {
204 if boxsize < 3 || line.len() < boxsize {
205 return;
206 }
207
208 let n = line.len();
209 let r = boxsize / 2;
210
211 let mut lq: VecDeque<f64> = VecDeque::with_capacity(boxsize);
212 let mut wq: VecDeque<f32> = VecDeque::with_capacity(boxsize);
213
214 let mut sum = f64::EPSILON; let mut wsum = f64::EPSILON;
217 let mut wsmooth = f64::EPSILON;
218
219 for i in 0..boxsize {
220 sum += line[i] * weight[i] as f64;
221 wsum += weight[i] as f64;
222 wsmooth += (weight[i] * weight[i]) as f64;
223 lq.push_back(line[i]);
224 wq.push_back(weight[i]);
225 }
226
227 for i in (r + 1)..(n - r) {
229 let w = weight[i + r];
230 let l = line[i + r];
231 let wold = wq.pop_front().unwrap();
232 let lold = lq.pop_front().unwrap();
233 wq.push_back(w);
234 lq.push_back(l);
235
236 sum += l * w as f64 - lold * wold as f64;
237 wsum += (w - wold) as f64;
238 line[i] = sum / wsum;
239 wsmooth += (w * w - wold * wold) as f64;
240 weight[i] = (wsmooth / wsum) as f32;
241 }
242}
243
244pub fn nan_box_filter_line(line: &mut [f64], boxsize: usize, orig: &mut Vec<f64>) {
256 #[derive(PartialEq, Clone, Copy)]
257 enum Mode { Nan, Normal, Fill }
258 let n = line.len();
259 let r = boxsize / 2;
260 if n == 0 || boxsize < 3 {
261 return;
262 }
263 let maxfills = r;
264 orig.clear();
265 orig.resize(n + boxsize - 1, f64::NAN);
266 orig[r..r + n].copy_from_slice(line);
267
268 let mut lsum: f64 = orig[r..2 * r].iter().sum();
269 if lsum.is_nan() {
270 lsum = 0.0;
271 }
272 let (mut nfills, mut nvalids) = (0usize, 0usize);
273 let mut mode = Mode::Nan;
274 let bs = boxsize as f64;
275
276 for i in 1..=n {
278 if lsum.is_nan() {
279 break;
280 }
281 let lead = orig[i - 1 + 2 * r];
282 match mode {
283 Mode::Normal => {
284 if lead.is_nan() {
285 mode = Mode::Fill;
286 }
287 }
288 Mode::Nan => {
289 if lead.is_nan() { nvalids = 0; } else { nvalids += 1; }
290 if nvalids == boxsize {
291 mode = Mode::Normal;
292 lsum = orig[i - 1..i + 2 * r].iter().sum();
293 line[i - 1] = lsum / bs;
294 continue;
295 }
296 }
297 Mode::Fill => {
298 if lead.is_nan() {
299 nfills += 1;
300 if nfills > maxfills {
301 mode = Mode::Nan;
302 nfills = 0;
303 lsum = 0.0;
304 nvalids = 0;
305 }
306 } else {
307 mode = Mode::Normal;
308 nfills = 0;
309 }
310 }
311 }
312 match mode {
313 Mode::Normal => {
314 let trailing = if i >= 2 { orig[i - 2] } else { 0.0 };
315 lsum += orig[i - 1 + 2 * r] - trailing;
316 line[i - 1] = lsum / bs;
317 }
318 Mode::Fill => {
319 let trailing = if i >= 2 { orig[i - 2] } else { 0.0 };
320 lsum -= trailing;
321 line[i - 1] = (lsum - orig[i - 1]) / (bs - 2.0);
322 let prev = if i > r { line[i - 1 - r] } else { line[i - 1] };
323 let extrapolated = 2.0 * line[i - 1] - prev;
324 orig[i - 1 + 2 * r] = extrapolated;
325 if i + r < n {
326 line[i - 1 + r] = extrapolated;
327 }
328 lsum += extrapolated;
329 }
330 Mode::Nan => {}
331 }
332 }
333}
334
335pub fn gaussian_smooth_3d(
349 data: &[f64],
350 sigma: [f64; 3],
351 mask: Option<&[u8]>,
352 weight: Option<&[f64]>,
353 nbox: usize,
354 grid: &Grid,
355) -> Vec<f64> {
356 let (nx, ny, nz) = grid.dims;
357 let n_total = check_volume("gaussian_smooth_3d: data", data.len(), grid);
358 if let Some(m) = mask {
359 check_volume("gaussian_smooth_3d: mask", m.len(), grid);
360 }
361 if let Some(w) = weight {
362 check_volume("gaussian_smooth_3d: weight", w.len(), grid);
363 }
364 let mut weight: Option<Vec<f32>> = weight.map(|w| {
366 let mut w: Vec<f32> = w.iter().map(|&v| v as f32).collect();
367 let min_nz = w.iter().copied().filter(|&v| v != 0.0).fold(f32::INFINITY, f32::min);
368 for v in w.iter_mut().filter(|v| **v == 0.0) {
369 *v = min_nz;
370 }
371 w
372 });
373 let mut result: Vec<f64> = data.to_vec();
374
375 let mut boxsizes: Vec<Vec<usize>> = sigma.iter()
377 .map(|&s| get_box_sizes(s, nbox))
378 .collect();
379
380 check_box_sizes(&mut boxsizes, &[nx, ny, nz]);
381
382 if let Some(m) = mask {
384 for i in 0..n_total {
385 if m[i] == 0 {
386 result[i] = f64::NAN;
387 }
388 }
389 }
390
391 let mut scratch: Vec<f64> = Vec::new();
393 for ibox in 0..nbox {
394 let bsize_x = boxsizes[0][ibox];
396 if nx > 1 && bsize_x >= 3 {
397 let reverse = mask.is_some() && ibox % 2 == 1;
399
400 for k in 0..nz {
401 for j in 0..ny {
402 let mut line: Vec<f64> = (0..nx).map(|i| {
403 let idx = if reverse { nx - 1 - i } else { i };
404 result[idx3d(idx, j, k, nx, ny)]
405 }).collect();
406
407 if mask.is_some() {
408 nan_box_filter_line(&mut line, bsize_x, &mut scratch);
409 } else if let Some(ref mut w) = weight.as_deref_mut() {
410 let mut wline: Vec<f32> = (0..nx).map(|i| {
411 let idx = if reverse { nx - 1 - i } else { i };
412 w[idx3d(idx, j, k, nx, ny)]
413 }).collect();
414 box_filter_line_weighted(&mut line, &mut wline, bsize_x);
415 for i in 0..nx {
416 let idx = if reverse { nx - 1 - i } else { i };
417 w[idx3d(idx, j, k, nx, ny)] = wline[i];
418 }
419 } else {
420 box_filter_line(&mut line, bsize_x);
421 }
422
423 for i in 0..nx {
424 let idx = if reverse { nx - 1 - i } else { i };
425 result[idx3d(idx, j, k, nx, ny)] = line[i];
426 }
427 }
428 }
429 }
430
431 let bsize_y = boxsizes[1][ibox];
433 if ny > 1 && bsize_y >= 3 {
434 let reverse = mask.is_some() && ibox % 2 == 1;
435
436 for k in 0..nz {
437 for i in 0..nx {
438 let mut line: Vec<f64> = (0..ny).map(|j| {
439 let idx = if reverse { ny - 1 - j } else { j };
440 result[idx3d(i, idx, k, nx, ny)]
441 }).collect();
442
443 if mask.is_some() {
444 nan_box_filter_line(&mut line, bsize_y, &mut scratch);
445 } else if let Some(ref mut w) = weight.as_deref_mut() {
446 let mut wline: Vec<f32> = (0..ny).map(|j| {
447 let idx = if reverse { ny - 1 - j } else { j };
448 w[idx3d(i, idx, k, nx, ny)]
449 }).collect();
450 box_filter_line_weighted(&mut line, &mut wline, bsize_y);
451 for j in 0..ny {
452 let idx = if reverse { ny - 1 - j } else { j };
453 w[idx3d(i, idx, k, nx, ny)] = wline[j];
454 }
455 } else {
456 box_filter_line(&mut line, bsize_y);
457 }
458
459 for j in 0..ny {
460 let idx = if reverse { ny - 1 - j } else { j };
461 result[idx3d(i, idx, k, nx, ny)] = line[j];
462 }
463 }
464 }
465 }
466
467 let bsize_z = boxsizes[2][ibox];
469 if nz > 1 && bsize_z >= 3 {
470 let reverse = mask.is_some() && ibox % 2 == 1;
471
472 for j in 0..ny {
473 for i in 0..nx {
474 let mut line: Vec<f64> = (0..nz).map(|k| {
475 let idx = if reverse { nz - 1 - k } else { k };
476 result[idx3d(i, j, idx, nx, ny)]
477 }).collect();
478
479 if mask.is_some() {
480 nan_box_filter_line(&mut line, bsize_z, &mut scratch);
481 } else if let Some(ref mut w) = weight.as_deref_mut() {
482 let mut wline: Vec<f32> = (0..nz).map(|k| {
483 let idx = if reverse { nz - 1 - k } else { k };
484 w[idx3d(i, j, idx, nx, ny)]
485 }).collect();
486 box_filter_line_weighted(&mut line, &mut wline, bsize_z);
487 for k in 0..nz {
488 let idx = if reverse { nz - 1 - k } else { k };
489 w[idx3d(i, j, idx, nx, ny)] = wline[k];
490 }
491 } else {
492 box_filter_line(&mut line, bsize_z);
493 }
494
495 for k in 0..nz {
496 let idx = if reverse { nz - 1 - k } else { k };
497 result[idx3d(i, j, idx, nx, ny)] = line[k];
498 }
499 }
500 }
501 }
502 }
503
504 result
505}
506
507pub fn gaussian_smooth_3d_boxsizes(
509 data: &[f64],
510 boxsizes: &[Vec<usize>],
511 nbox: usize,
512 grid: &Grid,
513) -> Vec<f64> {
514 let (nx, ny, nz) = grid.dims;
515 check_volume("gaussian_smooth_3d_boxsizes: data", data.len(), grid);
516 assert_eq!(
517 boxsizes.len(), 3,
518 "gaussian_smooth_3d_boxsizes: boxsizes needs one entry per axis, got {}",
519 boxsizes.len()
520 );
521 let mut result = data.to_vec();
522
523 for ibox in 0..nbox {
525 if nx > 1 && ibox < boxsizes[0].len() {
527 let bsize = boxsizes[0][ibox];
528 if bsize >= 3 {
529 for k in 0..nz {
530 for j in 0..ny {
531 let mut line: Vec<f64> = (0..nx).map(|i| result[idx3d(i, j, k, nx, ny)]).collect();
532 box_filter_line(&mut line, bsize);
533 for i in 0..nx {
534 result[idx3d(i, j, k, nx, ny)] = line[i];
535 }
536 }
537 }
538 }
539 }
540
541 if ny > 1 && ibox < boxsizes[1].len() {
543 let bsize = boxsizes[1][ibox];
544 if bsize >= 3 {
545 for k in 0..nz {
546 for i in 0..nx {
547 let mut line: Vec<f64> = (0..ny).map(|j| result[idx3d(i, j, k, nx, ny)]).collect();
548 box_filter_line(&mut line, bsize);
549 for j in 0..ny {
550 result[idx3d(i, j, k, nx, ny)] = line[j];
551 }
552 }
553 }
554 }
555 }
556
557 if nz > 1 && ibox < boxsizes[2].len() {
559 let bsize = boxsizes[2][ibox];
560 if bsize >= 3 {
561 for j in 0..ny {
562 for i in 0..nx {
563 let mut line: Vec<f64> = (0..nz).map(|k| result[idx3d(i, j, k, nx, ny)]).collect();
564 box_filter_line(&mut line, bsize);
565 for k in 0..nz {
566 result[idx3d(i, j, k, nx, ny)] = line[k];
567 }
568 }
569 }
570 }
571 }
572 }
573
574 result
575}
576
577fn smooth_boxes_f32(image: &mut [f32], boxsizes: &[usize], grid: &Grid) {
585 let (nx, ny, nz) = grid.dims;
586 let dims = [nx, ny, nz];
587 let mut line: Vec<f32> = Vec::new();
588 for &bsize0 in boxsizes {
589 for d in 0..3 {
590 let len = dims[d];
591 let mut bsize = bsize0 + usize::from(bsize0 % 2 == 0);
593 if bsize as f64 > len as f64 / 2.0 {
594 bsize = len / 2;
595 if bsize % 2 == 0 {
596 bsize += 1;
597 }
598 }
599 if len == 1 || bsize < 3 {
600 continue;
601 }
602 let (stride, na, sa, nb, sb) = match d {
603 0 => (1, ny, nx, nz, nx * ny),
604 1 => (nx, nx, 1, nz, nx * ny),
605 _ => (nx * ny, nx, 1, ny, nx),
606 };
607 line.resize(len, 0.0);
608 for b in 0..nb {
609 for a in 0..na {
610 let base = a * sa + b * sb;
611 for k in 0..len {
612 line[k] = image[base + k * stride];
613 }
614 box_filter_line_f32(&mut line, bsize);
615 for k in 0..len {
616 image[base + k * stride] = line[k];
617 }
618 }
619 }
620 }
621 }
622}
623
624fn box_filter_line_f32(line: &mut [f32], boxsize: usize) {
626 let n = line.len();
627 let r = boxsize / 2;
628 if n < boxsize {
629 return;
630 }
631 let mut q: VecDeque<f32> = VecDeque::with_capacity(boxsize);
632 let mut lsum: f32 = line[..r].iter().sum();
633 q.extend(line[..r].iter().copied());
634 for i in 0..=r {
635 lsum += line[i + r];
636 q.push_back(line[i + r]);
637 line[i] = lsum / (r + i + 1) as f32;
638 }
639 for i in (r + 1)..(n - r) {
640 lsum += line[i + r] - q.pop_front().unwrap();
641 q.push_back(line[i + r]);
642 line[i] = lsum / boxsize as f32;
643 }
644 for i in (n - r)..n {
645 lsum -= q.pop_front().unwrap();
646 line[i] = lsum / (r + n - i) as f32;
647 }
648}
649
650fn mrt_sample(x: &[f64]) -> Vec<f64> {
654 let total = x.len();
655 if total == 0 {
656 return Vec::new();
657 }
658 let n = (1e5f64).min(total as f64);
659 let len = n.sqrt().ceil() as usize;
660 let stop = (total - len) as f64;
661 let mut out = Vec::with_capacity(len * len);
662 for k in 0..len {
663 let start = if len == 1 { 0.0 } else { stop * k as f64 / (len - 1) as f64 };
665 let s = start.round_ties_even() as usize;
666 out.extend(x[s..s + len].iter().copied().filter(|v| v.is_finite()));
667 }
668 if out.is_empty() {
669 out = x.iter().copied().filter(|v| v.is_finite()).collect();
670 }
671 out
672}
673
674fn quantile_sorted(sorted: &[f64], p: f64) -> f64 {
679 let n = sorted.len();
680 if n == 1 {
681 return sorted[0];
682 }
683 let m = 1.0 - p; let aleph = n as f64 * p + m;
685 let j = (aleph.trunc() as usize).clamp(1, n - 1);
686 let gamma = (aleph - j as f64).clamp(0.0, 1.0);
687 let (a, b) = (sorted[j - 1], sorted[j]);
688 let approx = a == b || (a - b).abs() <= f64::EPSILON.sqrt() * a.abs().max(b.abs());
689 if a.is_finite() && b.is_finite() && approx {
690 a + gamma * (b - a)
691 } else {
692 (1.0 - gamma) * a + gamma * b
693 }
694}
695
696pub fn robust_mask(mag: &[f64], grid: &Grid) -> Vec<u8> {
703 let n_total = check_volume("robust_mask: magnitude", mag.len(), grid);
704
705 let samples = mrt_sample(mag);
710 if samples.is_empty() {
711 return vec![0u8; n_total];
712 }
713 let mut sorted = samples.clone();
714 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
715 let q05 = quantile_sorted(&sorted, 0.05);
717 let q15 = quantile_sorted(&sorted, 0.15);
718 let q80 = quantile_sorted(&sorted, 0.80);
719 let q99 = quantile_sorted(&sorted, 0.99);
720 let mean = |v: Vec<f64>| if v.is_empty() { f64::NAN } else { v.iter().sum::<f64>() / v.len() as f64 };
721
722 let high_intensity = mean(samples.iter().copied().filter(|&v| q80 <= v && v <= q99).collect());
724 let mut noise = mean(samples.iter().copied().filter(|&v| v <= q15).collect());
725 if noise > high_intensity / 10.0 {
726 noise = mean(samples.iter().copied().filter(|&v| v <= q05).collect());
727 if noise > high_intensity / 10.0 {
728 noise = 0.0; }
730 }
731
732 let threshold = (5.0 * noise).max(high_intensity / 5.0);
734
735 let mut mask_f32: Vec<f32> = mag.iter()
741 .map(|&v| if v > threshold { 1.0 } else { 0.0 })
742 .collect();
743 smooth_boxes_f32(&mut mask_f32, &[5], grid);
744 let mut mask: Vec<u8> = mask_f32.iter()
745 .map(|&v| u8::from(v as f64 > 0.4))
746 .collect();
747
748 let max_hole_size = n_total / 20;
750 mask = fill_holes(&mask, grid, max_hole_size);
751
752 let mut mask_f32: Vec<f32> = mask.iter().map(|&v| v as f32).collect();
753 smooth_boxes_f32(&mut mask_f32, &[3, 3], grid);
754 mask = mask_f32.iter()
755 .map(|&v| u8::from(v as f64 > 0.6))
756 .collect();
757
758 mask
759}
760
761fn box_segment(
776 image: &[f64],
777 mask: &[u8],
778 nbox: usize,
779 nx: usize, ny: usize, nz: usize,
780) -> Vec<u8> {
781 let n_total = nx * ny * nz;
782 let mut vote_count = vec![0u8; n_total];
783
784 let box_shift_x = nx.div_ceil(nbox);
786 let box_shift_y = ny.div_ceil(nbox);
787 let box_shift_z = nz.div_ceil(nbox);
788
789 let mut cz = 0;
791 while cz < nz {
792 let mut cy = 0;
793 while cy < ny {
794 let mut cx = 0;
795 while cx < nx {
796 let x_start = cx.saturating_sub(box_shift_x);
799 let x_end = (cx + box_shift_x + 1).min(nx);
800 let y_start = cy.saturating_sub(box_shift_y);
801 let y_end = (cy + box_shift_y + 1).min(ny);
802 let z_start = cz.saturating_sub(box_shift_z);
803 let z_end = (cz + box_shift_z + 1).min(nz);
804
805 let mut box_vals: Vec<f64> = Vec::new();
807 for z in z_start..z_end {
808 for y in y_start..y_end {
809 for x in x_start..x_end {
810 let idx = idx3d(x, y, z, nx, ny);
811 if mask[idx] > 0 && image[idx].is_finite() {
812 box_vals.push(image[idx]);
813 }
814 }
815 }
816 }
817
818 if box_vals.is_empty() {
819 cx += box_shift_x;
820 continue;
821 }
822
823 box_vals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
825 let q90 = quantile_sorted(&box_vals, 0.9);
826
827 let width = 0.1;
829 let low = (1.0 - width) * q90;
830 let high = (1.0 + width) * q90;
831
832 for z in z_start..z_end {
834 for y in y_start..y_end {
835 for x in x_start..x_end {
836 let idx = idx3d(x, y, z, nx, ny);
837 if mask[idx] > 0 {
838 let v = image[idx];
839 if v > low && v < high {
840 vote_count[idx] = vote_count[idx].saturating_add(1);
841 }
842 }
843 }
844 }
845 }
846
847 cx += box_shift_x;
848 }
849 cy += box_shift_y;
850 }
851 cz += box_shift_z;
852 }
853
854 let mut segmented = vec![0u8; n_total];
856 for i in 0..n_total {
857 if vote_count[i] >= 2 && mask[i] > 0 {
858 segmented[i] = 1;
859 }
860 }
861
862 segmented
863}
864
865fn fill_and_smooth(
874 lowpass: &mut [f64],
875 stable_mean: f64,
876 sigma2: [f64; 3],
877 grid: &Grid,
878) {
879 let n_total = grid.n_total();
880
881 let mut weight = vec![1.2f64; n_total];
884
885 for i in 0..n_total {
886 if lowpass[i] < stable_mean / 4.0 ||
887 lowpass[i].is_nan() ||
888 lowpass[i] > 10.0 * stable_mean {
889 lowpass[i] = 3.0 * stable_mean;
890 weight[i] = 1.2 - 1.0; }
892 }
893
894 let nbox = 3; let smoothed = gaussian_smooth_3d(lowpass, sigma2, None, Some(&weight), nbox, grid);
897 lowpass.copy_from_slice(&smoothed);
898}
899
900pub fn get_sensitivity(
927 mag: &[f64],
928 grid: &Grid,
929 sigma_mm: f64,
930 nbox: usize,
931) -> Result<Vec<f64>, InputError> {
932 if nbox == 0 {
933 return Err(InputError::param("homogeneity nbox", nbox, ">= 1"));
934 }
935 let (nx, ny, nz) = grid.dims;
936 let (vx, vy, vz) = grid.voxel_size;
937 let n_total = check_volume("get_sensitivity: magnitude", mag.len(), grid);
938
939 let sigma = [sigma_mm / vx, sigma_mm / vy, sigma_mm / vz];
941
942 let mask = robust_mask(mag, grid);
944
945 let segmentation = box_segment(mag, &mask, nbox, nx, ny, nz);
947
948 let factor: f64 = 0.7;
950 let sigma1 = [
951 (1.0_f64 - factor * factor).sqrt() * sigma[0],
952 (1.0_f64 - factor * factor).sqrt() * sigma[1],
953 (1.0_f64 - factor * factor).sqrt() * sigma[2],
954 ];
955 let sigma2 = [
956 factor * sigma[0],
957 factor * sigma[1],
958 factor * sigma[2],
959 ];
960
961 let mut lowpass = gaussian_smooth_3d(mag, sigma1, Some(&segmentation), None, 8, grid);
963
964 let mut sum = 0.0;
966 let mut count = 0usize;
967 for i in 0..n_total {
968 if mask[i] > 0 && mag[i].is_finite() {
969 sum += mag[i];
970 count += 1;
971 }
972 }
973 let stable_mean = if count > 0 { sum / count as f64 } else { 1.0 };
974
975 fill_and_smooth(&mut lowpass, stable_mean, sigma2, grid);
977
978 Ok(lowpass)
979}
980
981pub fn makehomogeneous(
998 mag: &[f64],
999 grid: &Grid,
1000 sigma_mm: f64,
1001 nbox: usize,
1002) -> Result<Vec<f64>, InputError> {
1003 let n_total = check_volume("makehomogeneous: magnitude", mag.len(), grid);
1004 let sensitivity = get_sensitivity(mag, grid, sigma_mm, nbox)?;
1005
1006 let mut result = vec![0.0; n_total];
1007 for i in 0..n_total {
1008 if sensitivity[i] > 1e-10 && !sensitivity[i].is_nan() {
1009 result[i] = mag[i] / sensitivity[i];
1010 } else {
1011 result[i] = mag[i];
1012 }
1013 }
1014
1015 Ok(result)
1016}
1017
1018pub fn rss_combine(
1030 mags_flat: &[f64],
1031 n_echoes: usize,
1032 n_total: usize,
1033) -> Vec<f64> {
1034 assert_eq!(
1035 mags_flat.len(), n_echoes * n_total,
1036 "rss_combine: magnitudes hold {} values but {n_echoes} echoes of {n_total} voxels \
1037 need {}",
1038 mags_flat.len(), n_echoes * n_total
1039 );
1040 let mut result = vec![0.0; n_total];
1041
1042 for e in 0..n_echoes {
1043 let offset = e * n_total;
1044 for i in 0..n_total {
1045 let v = mags_flat[offset + i];
1046 result[i] += v * v;
1047 }
1048 }
1049
1050 for i in 0..n_total {
1051 result[i] = result[i].sqrt();
1052 }
1053
1054 result
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060
1061 fn grid(nx: usize, ny: usize, nz: usize) -> Grid {
1062 Grid::new(nx, ny, nz, 1.0, 1.0, 1.0)
1063 }
1064
1065 #[test]
1066 fn test_get_box_sizes() {
1067 let sizes = get_box_sizes(5.0, 3);
1069 assert_eq!(sizes.len(), 3);
1070 for &s in &sizes {
1073 assert!(s % 2 == 1, "Box size should be odd, got {}", s);
1074 assert!((3..=11).contains(&s), "Box size should be in reasonable range, got {}", s);
1075 }
1076 }
1077
1078 #[test]
1079 fn test_box_filter_line() {
1080 let mut line = vec![1.0; 10];
1082 box_filter_line(&mut line, 3);
1083 for &v in &line {
1084 assert!((v - 1.0).abs() < 1e-10, "Uniform line should stay uniform");
1085 }
1086 }
1087
1088 #[test]
1089 fn test_robust_mask_basic() {
1090 let mag = vec![100.0; 27];
1092 let mask = robust_mask(&mag, &grid(3, 3, 3));
1093 let masked_count: usize = mask.iter().map(|&v| v as usize).sum();
1095 assert!(masked_count > 0, "Should have some masked voxels");
1096 }
1097
1098 #[test]
1099 fn test_rss_combine() {
1100 let mags = vec![
1102 3.0, 0.0, 0.0, 5.0, 4.0, 0.0, 0.0, 12.0, ];
1105 let result = rss_combine(&mags, 2, 4);
1106
1107 assert!((result[0] - 5.0).abs() < 1e-10);
1109 assert!((result[1] - 0.0).abs() < 1e-10);
1111 assert!((result[3] - 13.0).abs() < 1e-10);
1113 }
1114
1115 fn make_sphere_phantom(n: usize, bias: bool) -> (Vec<f64>, Vec<u8>) {
1122 let center = n as f64 / 2.0;
1123 let radius = n as f64 / 2.0 - 2.0;
1124 let n_total = n * n * n;
1125 let mut mag = vec![0.0f64; n_total];
1126 let mut mask = vec![0u8; n_total];
1127
1128 for k in 0..n {
1129 for j in 0..n {
1130 for i in 0..n {
1131 let dx = i as f64 - center;
1132 let dy = j as f64 - center;
1133 let dz = k as f64 - center;
1134 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
1135 let idx = i + j * n + k * n * n;
1136
1137 if dist < radius {
1138 let base = 100.0;
1140 let bias_val = if bias {
1142 1.0 + 0.5 * (i as f64 / n as f64)
1143 } else {
1144 1.0
1145 };
1146 mag[idx] = base * bias_val;
1147 mask[idx] = 1;
1148 } else {
1149 mag[idx] = 1.0 + 0.5 * ((i + j + k) % 3) as f64;
1151 }
1152 }
1153 }
1154 }
1155
1156 (mag, mask)
1157 }
1158
1159 #[test]
1164 fn test_get_box_sizes_zero_sigma() {
1165 let sizes = get_box_sizes(0.0, 3);
1166 assert_eq!(sizes, vec![0, 0, 0]);
1167 }
1168
1169 #[test]
1170 fn test_get_box_sizes_zero_n() {
1171 let sizes = get_box_sizes(5.0, 0);
1172 assert!(sizes.is_empty());
1173 }
1174
1175 #[test]
1176 fn test_get_box_sizes_negative_sigma() {
1177 let sizes = get_box_sizes(-1.0, 3);
1178 assert_eq!(sizes, vec![0, 0, 0]);
1179 }
1180
1181 #[test]
1182 fn test_get_box_sizes_large_sigma() {
1183 let sizes = get_box_sizes(20.0, 4);
1184 assert_eq!(sizes.len(), 4);
1185 for &s in &sizes {
1186 assert!(s % 2 == 1, "Box size should be odd, got {}", s);
1187 assert!(s >= 3, "Box size should be at least 3, got {}", s);
1188 }
1189 }
1190
1191 #[test]
1196 fn test_check_box_sizes_clamps_to_half_image() {
1197 let mut boxsizes = vec![vec![99], vec![99], vec![99]];
1199 let dims = [10, 10, 10];
1200 check_box_sizes(&mut boxsizes, &dims);
1201 for bs in &boxsizes {
1202 for &b in bs {
1203 assert!(b <= dims[0], "Box size should be clamped, got {}", b);
1204 assert!(b % 2 == 1, "Box size should be odd, got {}", b);
1205 }
1206 }
1207 }
1208
1209 #[test]
1210 fn test_check_box_sizes_makes_even_odd() {
1211 let mut boxsizes = vec![vec![4], vec![6], vec![8]];
1212 let dims = [100, 100, 100];
1213 check_box_sizes(&mut boxsizes, &dims);
1214 for bs in &boxsizes {
1215 for &b in bs {
1216 assert!(b % 2 == 1, "Box size should be odd, got {}", b);
1217 }
1218 }
1219 }
1220
1221 #[test]
1226 fn test_box_filter_line_too_small_boxsize() {
1227 let mut line = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1229 let original = line.clone();
1230 box_filter_line(&mut line, 1);
1231 assert_eq!(line, original);
1232 }
1233
1234 #[test]
1235 fn test_box_filter_line_larger_than_data() {
1236 let mut line = vec![1.0, 2.0];
1238 let original = line.clone();
1239 box_filter_line(&mut line, 5);
1240 assert_eq!(line, original);
1241 }
1242
1243 #[test]
1244 fn test_box_filter_line_smoothing_effect() {
1245 let mut line = vec![0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
1247 box_filter_line(&mut line, 5);
1248 assert!(line[3] < 10.0, "Spike should be smoothed");
1250 assert!(line[2] > 0.0 || line[4] > 0.0, "Neighbors should gain intensity");
1252 for &v in &line {
1254 assert!(v.is_finite(), "All values should be finite");
1255 }
1256 }
1257
1258 #[test]
1263 fn test_box_filter_line_weighted_too_small() {
1264 let mut line = vec![1.0, 2.0];
1265 let mut weight = vec![1.0, 1.0];
1266 let orig_l = line.clone();
1267 let orig_w = weight.clone();
1268 box_filter_line_weighted(&mut line, &mut weight, 1);
1269 assert_eq!(line, orig_l);
1270 assert_eq!(weight, orig_w);
1271 }
1272
1273 #[test]
1274 fn test_box_filter_line_weighted_uniform() {
1275 let mut line = vec![5.0; 10];
1276 let mut weight = vec![1.0; 10];
1277 box_filter_line_weighted(&mut line, &mut weight, 3);
1278 for i in 2..8 {
1281 assert!(
1282 (line[i] - 5.0).abs() < 0.5,
1283 "Uniform weighted line should stay near 5.0, got {} at index {}",
1284 line[i], i
1285 );
1286 }
1287 }
1288
1289 #[test]
1290 fn test_box_filter_line_weighted_varying_weights() {
1291 let mut line = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1292 let mut weight = vec![1.0, 1.0, 1.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0];
1293 box_filter_line_weighted(&mut line, &mut weight, 3);
1294 for &v in &line {
1296 assert!(v.is_finite(), "All values should be finite after weighted filter");
1297 }
1298 for &w in &weight {
1299 assert!(w.is_finite(), "All weights should be finite after weighted filter");
1300 }
1301 }
1302
1303 #[test]
1308 fn test_nan_box_filter_line_too_small() {
1309 let mut line = vec![1.0, 2.0];
1310 let original = line.clone();
1311 nan_box_filter_line(&mut line, 5, &mut Vec::new());
1312 assert_eq!(line, original);
1313 }
1314
1315 #[test]
1316 fn test_nan_box_filter_line_no_nans() {
1317 let mut line = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1319 nan_box_filter_line(&mut line, 3, &mut Vec::new());
1320 for &v in &line {
1322 assert!(v.is_finite(), "All values should be finite after nan box filter");
1323 }
1324 }
1328
1329 #[test]
1330 fn test_nan_box_filter_line_with_nans() {
1331 let mut line = vec![5.0, 5.0, 5.0, f64::NAN, f64::NAN, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0];
1333 nan_box_filter_line(&mut line, 3, &mut Vec::new());
1334 let finite_count = line.iter().filter(|v| v.is_finite()).count();
1336 assert!(finite_count >= 6, "Most values should be finite, got {}", finite_count);
1337 }
1338
1339 #[test]
1340 fn test_nan_box_filter_line_all_nan_except_edges() {
1341 let mut line = vec![1.0; 12];
1343 for i in 2..10 {
1344 line[i] = f64::NAN;
1345 }
1346 nan_box_filter_line(&mut line, 3, &mut Vec::new());
1347 assert!(line[0].is_finite() || line[0].is_nan());
1349 }
1350
1351 #[test]
1356 fn test_gaussian_smooth_3d_uniform_no_mask() {
1357 let n = 8;
1358 let data = vec![5.0; n * n * n];
1359 let result = gaussian_smooth_3d(&data, [2.0, 2.0, 2.0], None, None, 3, &grid(n, n, n));
1360 assert_eq!(result.len(), n * n * n);
1361 for &v in &result {
1363 assert!(
1364 (v - 5.0).abs() < 0.5,
1365 "Uniform data should stay near 5.0, got {}",
1366 v
1367 );
1368 }
1369 }
1370
1371 #[test]
1372 fn test_gaussian_smooth_3d_with_mask() {
1373 let n = 10;
1374 let (mag, mask) = make_sphere_phantom(n, false);
1375 let result = gaussian_smooth_3d(&mag, [1.5, 1.5, 1.5], Some(&mask), None, 4, &grid(n, n, n));
1377 assert_eq!(result.len(), n * n * n);
1378 for i in 0..result.len() {
1380 if mask[i] > 0 {
1381 assert!(result[i].is_finite(), "Masked voxel at {} should be finite", i);
1382 }
1383 }
1384 }
1385
1386 #[test]
1387 fn test_gaussian_smooth_3d_with_weights() {
1388 let n = 8;
1389 let data = vec![10.0; n * n * n];
1390 let weight = vec![1.0; n * n * n];
1391 let result = gaussian_smooth_3d(
1392 &data, [1.5, 1.5, 1.5], None, Some(&weight), 3, &grid(n, n, n),
1393 );
1394 assert_eq!(result.len(), n * n * n);
1395 for &v in &result {
1396 assert!(v.is_finite(), "Result should be finite");
1397 }
1398 }
1399
1400 #[test]
1405 fn test_gaussian_smooth_3d_boxsizes_uniform() {
1406 let n = 8;
1407 let data = vec![3.0; n * n * n];
1408 let boxsizes = vec![vec![3, 3], vec![3, 3], vec![3, 3]];
1409 let result = gaussian_smooth_3d_boxsizes(&data, &boxsizes, 2, &grid(n, n, n));
1410 assert_eq!(result.len(), n * n * n);
1411 for &v in &result {
1412 assert!(
1413 (v - 3.0).abs() < 0.5,
1414 "Uniform data should stay near 3.0, got {}",
1415 v
1416 );
1417 }
1418 }
1419
1420 #[test]
1421 fn test_gaussian_smooth_3d_boxsizes_spike() {
1422 let n = 10;
1423 let n_total = n * n * n;
1424 let mut data = vec![0.0; n_total];
1425 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
1427 data[center] = 100.0;
1428 let boxsizes = vec![vec![5, 5], vec![5, 5], vec![5, 5]];
1429 let result = gaussian_smooth_3d_boxsizes(&data, &boxsizes, 2, &grid(n, n, n));
1430 assert_eq!(result.len(), n_total);
1431 assert!(
1433 result[center] < 100.0,
1434 "Spike should be smoothed, got {}",
1435 result[center]
1436 );
1437 for &v in &result {
1439 assert!(v.is_finite(), "All values should be finite");
1440 }
1441 }
1442
1443 #[test]
1448 fn test_robust_mask_sphere() {
1449 let n = 12;
1450 let (mag, _) = make_sphere_phantom(n, false);
1451 let mask = robust_mask(&mag, &grid(n, n, n));
1452 assert_eq!(mask.len(), n * n * n);
1453 let masked_count: usize = mask.iter().map(|&v| v as usize).sum();
1454 assert!(masked_count > 0, "Should have masked voxels for sphere phantom");
1456 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
1458 assert_eq!(mask[center], 1, "Center of sphere should be masked");
1459 }
1460
1461 #[test]
1462 fn test_robust_mask_empty() {
1463 let mag = vec![0.0; 27];
1465 let mask = robust_mask(&mag, &grid(3, 3, 3));
1466 let count: usize = mask.iter().map(|&v| v as usize).sum();
1467 assert_eq!(count, 0, "Zero magnitude should produce empty mask");
1468 }
1469
1470 #[test]
1471 fn test_robust_mask_nan_values() {
1472 let mut mag = vec![100.0; 125];
1474 mag[0] = f64::NAN;
1475 mag[10] = f64::NAN;
1476 mag[50] = f64::INFINITY;
1477 let mask = robust_mask(&mag, &grid(5, 5, 5));
1478 assert_eq!(mask.len(), 125);
1479 for &v in &mask {
1481 assert!(v == 0 || v == 1, "Mask values should be 0 or 1");
1482 }
1483 }
1484
1485 #[test]
1490 fn test_box_segment_uniform() {
1491 let n = 10;
1492 let (mag, mask) = make_sphere_phantom(n, false);
1493 let seg = box_segment(&mag, &mask, 3, n, n, n);
1494 assert_eq!(seg.len(), n * n * n);
1495 let seg_count: usize = seg.iter().map(|&v| v as usize).sum();
1497 assert!(seg_count > 0, "Box segment should find some tissue voxels");
1498 }
1499
1500 #[test]
1501 fn test_box_segment_empty_mask() {
1502 let n = 8;
1503 let mag = vec![100.0; n * n * n];
1504 let mask = vec![0u8; n * n * n]; let seg = box_segment(&mag, &mask, 3, n, n, n);
1506 let count: usize = seg.iter().map(|&v| v as usize).sum();
1507 assert_eq!(count, 0, "Empty mask should produce no segmentation");
1508 }
1509
1510 #[test]
1515 fn test_fill_and_smooth_basic() {
1516 let n = 10;
1517 let n_total = n * n * n;
1518 let stable_mean = 100.0;
1519 let mut lowpass = vec![stable_mean; n_total];
1520 lowpass[0] = 0.0;
1522 lowpass[100] = f64::NAN;
1523 lowpass[200] = 2000.0; let sigma2 = [2.0, 2.0, 2.0];
1526 fill_and_smooth(&mut lowpass, stable_mean, sigma2, &grid(n, n, n));
1527
1528 for (i, &v) in lowpass.iter().enumerate() {
1530 assert!(v.is_finite(), "Value at {} should be finite, got {}", i, v);
1531 }
1532 }
1533
1534 #[test]
1535 fn test_fill_and_smooth_preserves_approximate_mean() {
1536 let n = 8;
1537 let n_total = n * n * n;
1538 let stable_mean = 50.0;
1539 let mut lowpass = vec![stable_mean; n_total];
1540 let sigma2 = [1.5, 1.5, 1.5];
1541 fill_and_smooth(&mut lowpass, stable_mean, sigma2, &grid(n, n, n));
1542
1543 let mean: f64 = lowpass.iter().sum::<f64>() / n_total as f64;
1545 assert!(
1546 (mean - stable_mean).abs() < stable_mean * 0.5,
1547 "Mean should be approximately preserved, got {} vs {}",
1548 mean,
1549 stable_mean
1550 );
1551 }
1552
1553 #[test]
1558 fn test_get_sensitivity_sphere() {
1559 let n = 12;
1560 let (mag, _) = make_sphere_phantom(n, true);
1561 let sensitivity = get_sensitivity(&mag, &grid(n, n, n), 4.0, 5).unwrap();
1562 assert_eq!(sensitivity.len(), n * n * n);
1563 let center = n / 2 + (n / 2) * n + (n / 2) * n * n;
1565 assert!(
1566 sensitivity[center].is_finite(),
1567 "Sensitivity at center should be finite"
1568 );
1569 assert!(
1570 sensitivity[center] > 0.0,
1571 "Sensitivity at center should be positive, got {}",
1572 sensitivity[center]
1573 );
1574 }
1575
1576 #[test]
1577 fn test_get_sensitivity_output_size() {
1578 let n = 10;
1579 let (mag, _) = make_sphere_phantom(n, false);
1580 let sensitivity = get_sensitivity(&mag, &grid(n, n, n), 3.0, 3).unwrap();
1581 assert_eq!(sensitivity.len(), n * n * n);
1582 }
1583
1584 #[test]
1589 fn test_makehomogeneous_output_size_and_finite() {
1590 let n = 12;
1591 let (mag, _) = make_sphere_phantom(n, true);
1592 let result = makehomogeneous(&mag, &grid(n, n, n), 4.0, 5).unwrap();
1593 assert_eq!(result.len(), n * n * n);
1594 for (i, &v) in result.iter().enumerate() {
1596 assert!(v.is_finite(), "Output at {} should be finite, got {}", i, v);
1597 }
1598 }
1599
1600 #[test]
1601 fn test_makehomogeneous_reduces_bias() {
1602 let n = 12;
1603 let (mag_biased, mask) = make_sphere_phantom(n, true);
1604 let result = makehomogeneous(&mag_biased, &grid(n, n, n), 4.0, 5).unwrap();
1605
1606 let mut original_vals = Vec::new();
1608 let mut corrected_vals = Vec::new();
1609 for i in 0..(n * n * n) {
1610 if mask[i] > 0 {
1611 original_vals.push(mag_biased[i]);
1612 corrected_vals.push(result[i]);
1613 }
1614 }
1615
1616 let orig_mean = original_vals.iter().sum::<f64>() / original_vals.len() as f64;
1618 let orig_std = (original_vals
1619 .iter()
1620 .map(|v| (v - orig_mean).powi(2))
1621 .sum::<f64>()
1622 / original_vals.len() as f64)
1623 .sqrt();
1624 let orig_cv = orig_std / orig_mean;
1625
1626 let corr_mean = corrected_vals.iter().sum::<f64>() / corrected_vals.len() as f64;
1627 let corr_std = (corrected_vals
1628 .iter()
1629 .map(|v| (v - corr_mean).powi(2))
1630 .sum::<f64>()
1631 / corrected_vals.len() as f64)
1632 .sqrt();
1633 let corr_cv = corr_std / corr_mean;
1634
1635 assert!(
1636 corr_cv <= orig_cv * 2.0,
1637 "Corrected CV ({}) should not be much worse than original CV ({})",
1638 corr_cv,
1639 orig_cv
1640 );
1641 }
1642
1643 #[test]
1644 fn test_makehomogeneous_no_bias_preserves() {
1645 let n = 12;
1646 let (mag, mask) = make_sphere_phantom(n, false);
1647 let result = makehomogeneous(&mag, &grid(n, n, n), 4.0, 5).unwrap();
1648
1649 for i in 0..(n * n * n) {
1651 if mask[i] > 0 {
1652 assert!(
1653 result[i] > 0.0,
1654 "Inside sphere voxel {} should be positive, got {}",
1655 i,
1656 result[i]
1657 );
1658 }
1659 }
1660 }
1661
1662 #[test]
1663 fn test_makehomogeneous_anisotropic_voxels() {
1664 let n = 12;
1665 let (mag, _) = make_sphere_phantom(n, true);
1666 let result = makehomogeneous(&mag, &Grid::new(n, n, n, 0.5, 0.5, 2.0), 4.0, 5).unwrap();
1668 assert_eq!(result.len(), n * n * n);
1669 for &v in &result {
1670 assert!(v.is_finite(), "All output values should be finite");
1671 }
1672 }
1673
1674 #[test]
1679 fn test_gaussian_smooth_3d_mask_reverse_passes() {
1680 let n = 10;
1682 let (mag, mask) = make_sphere_phantom(n, false);
1683 let result = gaussian_smooth_3d(&mag, [1.5, 1.5, 1.5], Some(&mask), None, 4, &grid(n, n, n));
1685 assert_eq!(result.len(), n * n * n);
1686 for i in 0..result.len() {
1688 if mask[i] > 0 {
1689 }
1691 }
1692 let finite_count = result.iter().filter(|v| v.is_finite()).count();
1694 assert!(finite_count > 0, "Should have some finite values");
1695 }
1696
1697 #[test]
1712 #[should_panic(expected = "makehomogeneous: magnitude has 216 voxels but the grid is 8x8x8 (512 voxels)")]
1713 fn makehomogeneous_refuses_magnitude_shorter_than_grid() {
1714 let _ = makehomogeneous(&vec![1.0; 6 * 6 * 6], &grid(8, 8, 8), 4.0, 5);
1715 }
1716
1717 #[test]
1718 #[should_panic(expected = "makehomogeneous: magnitude has 1000 voxels but the grid is 8x8x8 (512 voxels)")]
1719 fn makehomogeneous_refuses_magnitude_longer_than_grid() {
1720 let _ = makehomogeneous(&vec![1.0; 10 * 10 * 10], &grid(8, 8, 8), 4.0, 5);
1723 }
1724
1725 #[test]
1726 #[should_panic(expected = "robust_mask: magnitude has 216 voxels but the grid is 8x8x8 (512 voxels)")]
1727 fn robust_mask_refuses_magnitude_not_matching_grid() {
1728 robust_mask(&vec![1.0; 6 * 6 * 6], &grid(8, 8, 8));
1729 }
1730
1731 #[test]
1732 #[should_panic(expected = "gaussian_smooth_3d_boxsizes: data has 216 voxels but the grid is 8x8x8 (512 voxels)")]
1733 fn gaussian_smooth_3d_boxsizes_refuses_data_not_matching_grid() {
1734 let boxsizes = vec![vec![5], vec![5], vec![5]];
1736 gaussian_smooth_3d_boxsizes(&vec![1.0; 6 * 6 * 6], &boxsizes, 1, &grid(8, 8, 8));
1737 }
1738
1739 #[test]
1740 #[should_panic(expected = "gaussian_smooth_3d: mask has 216 voxels but the grid is 8x8x8 (512 voxels)")]
1741 fn gaussian_smooth_3d_refuses_mask_not_matching_grid() {
1742 let data = vec![1.0; 8 * 8 * 8];
1743 let mask = vec![1u8; 6 * 6 * 6];
1744 gaussian_smooth_3d(&data, [1.5, 1.5, 1.5], Some(&mask), None, 3, &grid(8, 8, 8));
1745 }
1746
1747 #[test]
1748 #[should_panic(expected = "gaussian_smooth_3d: weight has 216 voxels but the grid is 8x8x8 (512 voxels)")]
1749 fn gaussian_smooth_3d_refuses_weight_not_matching_grid() {
1750 let data = vec![1.0; 8 * 8 * 8];
1751 let weight = vec![1.0; 6 * 6 * 6];
1752 gaussian_smooth_3d(&data, [1.5, 1.5, 1.5], None, Some(&weight), 3, &grid(8, 8, 8));
1753 }
1754
1755 #[test]
1756 #[should_panic(expected = "rss_combine: magnitudes hold 600 values but 3 echoes of 216 voxels need 648")]
1757 fn rss_combine_refuses_truncated_echo_stack() {
1758 rss_combine(&vec![1.0; 600], 3, 6 * 6 * 6);
1759 }
1760
1761 #[test]
1762 fn makehomogeneous_accepts_a_matching_volume() {
1763 let n = 8;
1765 let out = makehomogeneous(&vec![1.0; n * n * n], &grid(n, n, n), 4.0, 5).unwrap();
1766 assert_eq!(out.len(), n * n * n);
1767 }
1768
1769 #[test]
1772 fn defaults_are_mriresearchtools_and_agree_with_the_pipeline_config() {
1773 let p = HomogeneityParams::default();
1774 assert_eq!((p.sigma_mm, p.nbox), (7.0, 15));
1775 let m = crate::pipeline::MaskingConfig::default();
1776 assert_eq!((m.homogeneity_sigma_mm, m.homogeneity_nbox), (p.sigma_mm, p.nbox));
1777 }
1778
1779 #[test]
1781 fn zero_nbox_is_an_error() {
1782 let n = 8;
1783 let mag = vec![1.0; n * n * n];
1784 let err = makehomogeneous(&mag, &grid(n, n, n), 4.0, 0).unwrap_err();
1785 assert!(err.0.contains("homogeneity nbox = 0") && err.0.contains(">= 1"), "{err}");
1786 assert!(get_sensitivity(&mag, &grid(n, n, n), 4.0, 0).is_err());
1787 }
1788}