1use crate::fft::Fft3dWorkspaceF32;
38use crate::inversion::medi::{
39 bdiv_periodic_inplace_f32, fgrad_periodic_inplace_f32, gradient_mask_f32,
40};
41use crate::kernels::dipole::dipole_kernel_f32;
42use crate::utils::padding::{next_fast_fft_size, pad3d, unpad3d};
43use crate::utils::simd_ops::{
44 apply_gradient_weights_f32, axpy_f32, compute_p_weights_f32, dot_product_f32, xpby_f32,
45};
46use crate::Grid;
47use num_complex::Complex32;
48
49const TWO_PI: f32 = std::f32::consts::TAU;
50
51#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
53#[derive(Clone, Debug)]
54pub struct ChiSepIlsqrParams {
55 pub cf: f64,
57 pub dr_pos: f64,
59 pub dr_neg: f64,
61 pub lambda1: f64,
63 pub percentage: f64,
65 pub r2p_min: f64,
67 pub r2p_max: f64,
69 pub max_iter: usize,
71 pub tol: f64,
73 pub cg_max_iter: usize,
75 pub cg_tol: f64,
77}
78
79impl Default for ChiSepIlsqrParams {
80 fn default() -> Self {
81 Self {
82 cf: 123.2e6,
83 dr_pos: 137.0,
84 dr_neg: 137.0,
85 lambda1: 1.0,
86 percentage: 0.9,
87 r2p_min: 1.0,
88 r2p_max: 30.0,
89 max_iter: 30,
90 tol: 0.01,
91 cg_max_iter: 30,
92 cg_tol: 0.05,
93 }
94 }
95}
96
97struct Ws {
99 n: usize,
100 nx: usize,
101 ny: usize,
102 nz: usize,
103 vsx: f32,
104 vsy: f32,
105 vsz: f32,
106 fft_ws: Fft3dWorkspaceF32,
107 gx: Vec<f32>,
108 gy: Vec<f32>,
109 gz: Vec<f32>,
110 wx: Vec<f32>,
111 wy: Vec<f32>,
112 wz: Vec<f32>,
113 div: Vec<f32>,
114 dip: Vec<f32>,
115 cbuf: Vec<Complex32>,
116}
117
118impl Ws {
119 fn new(grid: &Grid) -> Self {
120 let (nx, ny, nz) = grid.dims;
121 let (vsx, vsy, vsz) = grid.voxel_size;
122 let n = nx * ny * nz;
123 Self {
124 n,
125 nx,
126 ny,
127 nz,
128 vsx: vsx as f32,
129 vsy: vsy as f32,
130 vsz: vsz as f32,
131 fft_ws: Fft3dWorkspaceF32::new(nx, ny, nz),
132 gx: vec![0.0; n],
133 gy: vec![0.0; n],
134 gz: vec![0.0; n],
135 wx: vec![0.0; n],
136 wy: vec![0.0; n],
137 wz: vec![0.0; n],
138 div: vec![0.0; n],
139 dip: vec![0.0; n],
140 cbuf: vec![Complex32::new(0.0, 0.0); n],
141 }
142 }
143
144 #[allow(clippy::too_many_arguments)]
146 fn tv_apply(&mut self, x: &[f32], mx: &[f32], my: &[f32], mz: &[f32], vr: &[f32]) {
147 fgrad_periodic_inplace_f32(
148 &mut self.gx, &mut self.gy, &mut self.gz, x, self.nx, self.ny, self.nz, self.vsx,
149 self.vsy, self.vsz,
150 );
151 apply_gradient_weights_f32(
152 &mut self.wx, &mut self.wy, &mut self.wz, mx, my, mz, vr, &self.gx, &self.gy, &self.gz,
153 );
154 bdiv_periodic_inplace_f32(
155 &mut self.div, &self.wx, &self.wy, &self.wz, self.nx, self.ny, self.nz, self.vsx,
156 self.vsy, self.vsz,
157 );
158 }
159
160 fn dipole_apply(&mut self, x: &[f32], d_kernel: &[f32]) {
162 self.fft_ws
163 .apply_dipole_inplace(x, d_kernel, &mut self.dip, &mut self.cbuf);
164 }
165}
166
167fn irls_weights(ws: &mut Ws, x: &[f32], mx: &[f32], my: &[f32], mz: &[f32], vr: &mut [f32]) {
169 let eps = 1.0e-6_f32;
170 fgrad_periodic_inplace_f32(
171 &mut ws.gx, &mut ws.gy, &mut ws.gz, x, ws.nx, ws.ny, ws.nz, ws.vsx, ws.vsy, ws.vsz,
172 );
173 compute_p_weights_f32(vr, mx, my, mz, &ws.gx, &ws.gy, &ws.gz, eps);
174}
175
176#[allow(clippy::too_many_arguments)]
201pub fn chi_sep_ilsqr<F>(
202 local_field: &[f64],
203 r2prime: &[f64],
204 magnitude: &[f64],
205 qsm: &[f64],
206 mask: &[u8],
207 grid: &Grid,
208 bdir: (f64, f64, f64),
209 params: &ChiSepIlsqrParams,
210 progress: F,
211) -> (Vec<f64>, Vec<f64>, Vec<f64>)
212where
213 F: FnMut(usize, usize),
214{
215 let dims = grid.dims;
216 let fast = (
217 next_fast_fft_size(dims.0),
218 next_fast_fft_size(dims.1),
219 next_fast_fft_size(dims.2),
220 );
221 if fast == dims {
222 return chi_sep_ilsqr_core(
223 local_field, r2prime, magnitude, qsm, mask, grid, bdir, params, progress,
224 );
225 }
226 let (vsx, vsy, vsz) = grid.voxel_size;
227 let pgrid = Grid::new(fast.0, fast.1, fast.2, vsx, vsy, vsz);
228 let (chi_pos, chi_neg, chi_total) = chi_sep_ilsqr_core(
229 &pad3d(local_field, dims, fast),
230 &pad3d(r2prime, dims, fast),
231 &pad3d(magnitude, dims, fast),
232 &pad3d(qsm, dims, fast),
233 &pad3d(mask, dims, fast),
234 &pgrid,
235 bdir,
236 params,
237 progress,
238 );
239 (
240 unpad3d(&chi_pos, fast, dims),
241 unpad3d(&chi_neg, fast, dims),
242 unpad3d(&chi_total, fast, dims),
243 )
244}
245
246#[allow(clippy::too_many_arguments)]
247fn chi_sep_ilsqr_core<F>(
248 local_field: &[f64],
249 r2prime: &[f64],
250 magnitude: &[f64],
251 qsm: &[f64],
252 mask: &[u8],
253 grid: &Grid,
254 bdir: (f64, f64, f64),
255 params: &ChiSepIlsqrParams,
256 mut progress: F,
257) -> (Vec<f64>, Vec<f64>, Vec<f64>)
258where
259 F: FnMut(usize, usize),
260{
261 let (nx, ny, nz) = grid.dims;
262 let n = nx * ny * nz;
263 assert_eq!(local_field.len(), n, "local_field length must match grid");
264 assert_eq!(r2prime.len(), n, "r2prime length must match grid");
265 assert_eq!(magnitude.len(), n, "magnitude length must match grid");
266 assert_eq!(qsm.len(), n, "qsm length must match grid");
267 assert_eq!(mask.len(), n, "mask length must match grid");
268
269 let cf_ppm = (params.cf * 1.0e-6) as f32; let dr_p = params.dr_pos as f32;
271 let dr_q = params.dr_neg as f32;
272 let lambda1 = params.lambda1 as f32;
273 let tol = params.tol as f32;
274
275 let mut ws = Ws::new(grid);
276 let d_kernel = dipole_kernel_f32(grid, (bdir.0 as f32, bdir.1 as f32, bdir.2 as f32));
277
278 let mut mag_mean = 0.0_f64;
280 let mut n_mask = 0usize;
281 for i in 0..n {
282 if mask[i] != 0 {
283 mag_mean += magnitude[i];
284 n_mask += 1;
285 }
286 }
287 assert!(n_mask > 0, "mask is empty");
288 mag_mean /= n_mask as f64;
289 let wf2: Vec<f32> = (0..n)
290 .map(|i| {
291 if mask[i] == 0 {
292 0.0
293 } else {
294 let w = (magnitude[i] / mag_mean) as f32;
295 w * w
296 }
297 })
298 .collect();
299 let wr2: Vec<f32> = (0..n)
301 .map(|i| {
302 if mask[i] == 0 {
303 0.0
304 } else if r2prime[i] > params.r2p_max || r2prime[i] < params.r2p_min {
305 wf2[i] / 100.0
306 } else {
307 wf2[i]
308 }
309 })
310 .collect();
311
312 let mag_f32: Vec<f32> = magnitude.iter().map(|&v| v as f32).collect();
314 let r2p_f32: Vec<f32> = r2prime
315 .iter()
316 .zip(mask.iter())
317 .map(|(&v, &m)| if m != 0 { v as f32 } else { 0.0 })
318 .collect();
319 let pct = params.percentage as f32;
320 let (vsx, vsy, vsz) = (ws.vsx, ws.vsy, ws.vsz);
321 let (mmx, mmy, mmz) = gradient_mask_f32(&mag_f32, mask, nx, ny, nz, vsx, vsy, vsz, pct);
322 let (mrx, mry, mrz) = gradient_mask_f32(&r2p_f32, mask, nx, ny, nz, vsx, vsy, vsz, pct);
323
324 let field_f32: Vec<f32> = local_field
327 .iter()
328 .zip(mask.iter())
329 .map(|(&v, &m)| if m != 0 { (v * params.cf * 1.0e-6) as f32 } else { 0.0 })
330 .collect();
331
332 let dr_sum = dr_p + dr_q;
336 let mut chi_pos = vec![0.0_f32; n];
337 let mut chi_neg = vec![0.0_f32; n];
338 for i in 0..n {
339 if mask[i] == 0 {
340 continue;
341 }
342 let q = qsm[i] as f32;
343 let r = r2prime[i] as f32;
344 chi_pos[i] = ((dr_q * q + r) / dr_sum).max(0.0);
345 chi_neg[i] = ((dr_p * q - r) / dr_sum).min(0.0);
346 }
347
348 let field_w = TWO_PI * TWO_PI;
351
352 let mut vr_tot = vec![0.0_f32; n];
353 let mut vr_pos = vec![0.0_f32; n];
354 let mut vr_neg = vec![0.0_f32; n];
355 let n2 = 2 * n;
356 let mut rhs = vec![0.0_f32; n2];
357 let mut dx = vec![0.0_f32; n2];
358 let mut prev_total = vec![0.0_f32; n];
359 for i in 0..n {
360 prev_total[i] = chi_pos[i] + chi_neg[i];
361 }
362 let mut total = vec![0.0_f32; n];
364 let mut stage = vec![0.0_f32; n];
365 let mut cg_r = vec![0.0_f32; n2];
366 let mut cg_p = vec![0.0_f32; n2];
367 let mut cg_ap = vec![0.0_f32; n2];
368
369 for iter in 0..params.max_iter {
370 progress(iter + 1, params.max_iter);
371
372 for i in 0..n {
374 total[i] = chi_pos[i] + chi_neg[i];
375 }
376 irls_weights(&mut ws, &total, &mmx, &mmy, &mmz, &mut vr_tot);
377 irls_weights(&mut ws, &chi_pos, &mrx, &mry, &mrz, &mut vr_pos);
378 irls_weights(&mut ws, &chi_neg, &mrx, &mry, &mrz, &mut vr_neg);
379
380 ws.dipole_apply(&total, &d_kernel);
384 for i in 0..n {
385 stage[i] = wf2[i] * (field_f32[i] - cf_ppm * ws.dip[i]);
386 }
387 ws.dipole_apply(&stage, &d_kernel);
388 for i in 0..n {
389 let g = -field_w * cf_ppm * ws.dip[i];
390 rhs[i] = g;
391 rhs[n + i] = g;
392 }
393
394 for i in 0..n {
397 if mask[i] == 0 {
398 continue;
399 }
400 let rr = r2p_f32[i] - dr_p * chi_pos[i] + dr_q * chi_neg[i];
401 let wrr = wr2[i] * rr;
402 rhs[i] -= dr_p * wrr;
403 rhs[n + i] += dr_q * wrr;
404 }
405
406 ws.tv_apply(&total, &mmx, &mmy, &mmz, &vr_tot);
408 for i in 0..n {
409 let g = 2.0 * lambda1 * ws.div[i];
410 rhs[i] += g;
411 rhs[n + i] += g;
412 }
413 ws.tv_apply(&chi_pos, &mrx, &mry, &mrz, &vr_pos);
414 for i in 0..n {
415 rhs[i] += lambda1 * ws.div[i];
416 }
417 ws.tv_apply(&chi_neg, &mrx, &mry, &mrz, &vr_neg);
418 for i in 0..n {
419 rhs[n + i] += lambda1 * ws.div[i];
420 }
421
422 for v in rhs.iter_mut() {
423 *v = -*v;
424 }
425
426 cg_solve(
428 &mut ws, &d_kernel, &wf2, &wr2, &mmx, &mmy, &mmz, &mrx, &mry, &mrz, &vr_tot, &vr_pos,
429 &vr_neg, lambda1, field_w, cf_ppm, dr_p, dr_q, mask, &rhs, &mut dx, &mut stage,
430 &mut cg_r, &mut cg_p, &mut cg_ap, params.cg_tol as f32, params.cg_max_iter,
431 );
432
433 for i in 0..n {
435 if mask[i] == 0 {
436 continue;
437 }
438 chi_pos[i] = (chi_pos[i] + dx[i]).max(0.0);
439 chi_neg[i] = (chi_neg[i] + dx[n + i]).min(0.0);
440 }
441
442 let mut num = 0.0_f64;
444 let mut den = 0.0_f64;
445 for i in 0..n {
446 let t = chi_pos[i] + chi_neg[i];
447 let d = (t - prev_total[i]) as f64;
448 num += d * d;
449 den += (prev_total[i] as f64) * (prev_total[i] as f64);
450 prev_total[i] = t;
451 }
452 if den > 0.0 && (num / den).sqrt() < tol as f64 {
453 break;
454 }
455 }
456
457 let chi_pos_out: Vec<f64> = chi_pos.iter().map(|&v| v as f64).collect();
458 let chi_neg_out: Vec<f64> = chi_neg.iter().map(|&v| v as f64).collect();
459 let chi_total: Vec<f64> = (0..n).map(|i| chi_pos_out[i] + chi_neg_out[i]).collect();
460 (chi_pos_out, chi_neg_out, chi_total)
461}
462
463#[allow(clippy::too_many_arguments)]
470fn apply_operator(
471 ws: &mut Ws,
472 d_kernel: &[f32],
473 wf2: &[f32],
474 wr2: &[f32],
475 mmx: &[f32],
476 mmy: &[f32],
477 mmz: &[f32],
478 mrx: &[f32],
479 mry: &[f32],
480 mrz: &[f32],
481 vr_tot: &[f32],
482 vr_pos: &[f32],
483 vr_neg: &[f32],
484 lambda1: f32,
485 field_w: f32,
486 cf_ppm: f32,
487 dr_p: f32,
488 dr_q: f32,
489 mask: &[u8],
490 dx: &[f32],
491 out: &mut [f32],
492 stage: &mut [f32],
493) {
494 let n = ws.n;
495 let (d_pos, d_neg) = dx.split_at(n);
496
497 for i in 0..n {
501 stage[i] = d_pos[i] + d_neg[i];
502 }
503
504 ws.tv_apply(stage, mmx, mmy, mmz, vr_tot);
506 for i in 0..n {
507 let g = 2.0 * lambda1 * ws.div[i];
508 out[i] = g;
509 out[n + i] = g;
510 }
511
512 ws.dipole_apply(stage, d_kernel);
514 for i in 0..n {
515 stage[i] = wf2[i] * ws.dip[i];
516 }
517 ws.dipole_apply(stage, d_kernel);
518 let scale = field_w * cf_ppm * cf_ppm;
519 for i in 0..n {
520 let f = scale * ws.dip[i];
521 out[i] += f;
522 out[n + i] += f;
523 }
524
525 ws.tv_apply(d_pos, mrx, mry, mrz, vr_pos);
527 for i in 0..n {
528 out[i] += lambda1 * ws.div[i];
529 }
530 ws.tv_apply(d_neg, mrx, mry, mrz, vr_neg);
531 for i in 0..n {
532 out[n + i] += lambda1 * ws.div[i];
533 }
534
535 for i in 0..n {
537 if mask[i] == 0 {
538 continue;
539 }
540 let lin = wr2[i] * (dr_p * d_pos[i] - dr_q * d_neg[i]);
541 out[i] += dr_p * lin;
542 out[n + i] -= dr_q * lin;
543 }
544}
545
546#[allow(clippy::too_many_arguments)]
548fn cg_solve(
549 ws: &mut Ws,
550 d_kernel: &[f32],
551 wf2: &[f32],
552 wr2: &[f32],
553 mmx: &[f32],
554 mmy: &[f32],
555 mmz: &[f32],
556 mrx: &[f32],
557 mry: &[f32],
558 mrz: &[f32],
559 vr_tot: &[f32],
560 vr_pos: &[f32],
561 vr_neg: &[f32],
562 lambda1: f32,
563 field_w: f32,
564 cf_ppm: f32,
565 dr_p: f32,
566 dr_q: f32,
567 mask: &[u8],
568 b: &[f32],
569 x: &mut [f32],
570 stage: &mut [f32],
571 r: &mut [f32],
572 p: &mut [f32],
573 ap: &mut [f32],
574 tol: f32,
575 max_iter: usize,
576) {
577 x.fill(0.0);
578 r.copy_from_slice(b);
579 p.copy_from_slice(b);
580
581 let b_norm = dot_product_f32(b, b).sqrt();
582 if b_norm < 1e-12 {
583 return;
584 }
585 let mut rsold = dot_product_f32(r, r);
586
587 for _ in 0..max_iter {
588 apply_operator(
589 ws, d_kernel, wf2, wr2, mmx, mmy, mmz, mrx, mry, mrz, vr_tot, vr_pos, vr_neg, lambda1,
590 field_w, cf_ppm, dr_p, dr_q, mask, p, ap, stage,
591 );
592 let pap = dot_product_f32(p, ap);
593 if pap.abs() < 1e-20 {
594 break;
595 }
596 let alpha = rsold / pap;
597 axpy_f32(x, alpha, p);
598 axpy_f32(r, -alpha, ap);
599 let rsnew = dot_product_f32(r, r);
600 if rsnew.sqrt() < tol * b_norm {
601 break;
602 }
603 xpby_f32(p, r, rsnew / rsold);
604 rsold = rsnew;
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use crate::fft::{fft3d_real, ifft3d_real};
612 use crate::kernels::dipole::dipole_kernel;
613
614 fn make_sphere(nx: usize, ny: usize, nz: usize, c: f64, r: f64) -> Vec<f64> {
615 let mut vol = vec![0.0; nx * ny * nz];
616 for k in 0..nz {
617 for j in 0..ny {
618 for i in 0..nx {
619 let (dx, dy, dz) = (i as f64 - c, j as f64 - c, k as f64 - c);
620 if dx * dx + dy * dy + dz * dz <= r * r {
621 vol[i + j * nx + k * nx * ny] = 1.0;
622 }
623 }
624 }
625 }
626 vol
627 }
628
629 #[test]
632 fn test_chi_sep_ilsqr_recovers_two_shell_phantom() {
633 let (nx, ny, nz) = (32, 32, 32);
634 let n = nx * ny * nz;
635 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
636 let bdir = (0.0, 0.0, 1.0);
637 let cf = 123.2e6_f64;
638
639 let inner = make_sphere(nx, ny, nz, 16.0, 4.0);
640 let outer = make_sphere(nx, ny, nz, 16.0, 8.0);
641 let brain = make_sphere(nx, ny, nz, 16.0, 12.0);
642
643 let mut chi_pos_t = vec![0.0_f64; n];
644 let mut chi_neg_t = vec![0.0_f64; n];
645 for i in 0..n {
646 if inner[i] > 0.5 {
647 chi_pos_t[i] = 0.08;
648 } else if outer[i] > 0.5 {
649 chi_neg_t[i] = -0.05;
650 }
651 }
652 let chi_total_t: Vec<f64> = (0..n).map(|i| chi_pos_t[i] + chi_neg_t[i]).collect();
653
654 let d = dipole_kernel(&grid, bdir);
656 let cf_fft = fft3d_real(&chi_total_t, nx, ny, nz);
657 let f_fft: Vec<_> = cf_fft.iter().zip(d.iter()).map(|(&c, &dk)| c * dk).collect();
658 let field_ppm: Vec<f64> = ifft3d_real(&f_fft, nx, ny, nz);
659
660 let dr = 137.0_f64;
661 let r2prime: Vec<f64> = (0..n)
662 .map(|i| dr * (chi_pos_t[i].abs() + chi_neg_t[i].abs()))
663 .collect();
664 let mask: Vec<u8> = brain.iter().map(|&v| (v > 0.5) as u8).collect();
665 let magnitude: Vec<f64> = (0..n)
666 .map(|i| if mask[i] != 0 { 100.0 } else { 0.0 })
667 .collect();
668
669 let params = ChiSepIlsqrParams {
670 cf,
671 ..ChiSepIlsqrParams::default()
672 };
673 let (chi_pos, chi_neg, chi_total) = chi_sep_ilsqr(
675 &field_ppm, &r2prime, &magnitude, &chi_total_t, &mask, &grid, bdir, ¶ms, |_, _| {},
676 );
677
678 for i in 0..n {
679 assert!(chi_pos[i] >= 0.0, "chi+ must be non-negative");
680 assert!(chi_neg[i] <= 0.0, "chi- must be non-positive");
681 assert!((chi_total[i] - chi_pos[i] - chi_neg[i]).abs() < 1e-10);
682 }
683
684 let mean_in = |v: &Vec<f64>, region: &Vec<f64>| {
687 let (mut s, mut c) = (0.0, 0usize);
688 for i in 0..n {
689 if region[i] > 0.5 {
690 s += v[i];
691 c += 1;
692 }
693 }
694 s / c as f64
695 };
696 let pos_core = mean_in(&chi_pos, &inner);
697 let shell: Vec<f64> = (0..n)
698 .map(|i| if outer[i] > 0.5 && inner[i] < 0.5 { 1.0 } else { 0.0 })
699 .collect();
700 let neg_shell = mean_in(&chi_neg, &shell);
701 assert!(
702 (pos_core - 0.08).abs() < 0.02,
703 "chi+ core mean {:.4} vs true 0.08",
704 pos_core
705 );
706 assert!(
707 (neg_shell + 0.05).abs() < 0.02,
708 "chi- shell mean {:.4} vs true -0.05",
709 neg_shell
710 );
711 }
712
713 #[test]
717 #[ignore]
718 fn perf_probe() {
719 use std::time::Instant;
720 let grid = Grid::new(164, 205, 205, 1.0, 1.0, 1.0);
721 let n = 164 * 205 * 205;
722 let mut ws = Ws::new(&grid);
723 let x = vec![0.5_f32; n];
724 let m = vec![1.0_f32; n];
725 let vr = vec![1.0_f32; n];
726 let dk = crate::kernels::dipole::dipole_kernel_f32(&grid, (0.0, 0.0, 1.0));
727 let t = Instant::now();
728 for _ in 0..5 {
729 ws.dipole_apply(&x, &dk);
730 }
731 println!("dipole_apply 164x205x205 x5: {:?}", t.elapsed());
732 let t = Instant::now();
733 for _ in 0..5 {
734 ws.tv_apply(&x, &m, &m, &m, &vr);
735 }
736 println!("tv_apply x5: {:?}", t.elapsed());
737
738 let pgrid = Grid::new(180, 216, 216, 1.0, 1.0, 1.0);
740 let pn = 180 * 216 * 216;
741 let mut pws = Ws::new(&pgrid);
742 let px = vec![0.5_f32; pn];
743 let pdk = crate::kernels::dipole::dipole_kernel_f32(&pgrid, (0.0, 0.0, 1.0));
744 let t = Instant::now();
745 for _ in 0..5 {
746 pws.dipole_apply(&px, &pdk);
747 }
748 println!("dipole_apply 180x216x216 x5: {:?}", t.elapsed());
749 }
750
751 #[test]
754 fn test_padding_path_preserves_shape_and_values() {
755 let (nx, ny, nz) = (7, 3, 3); let n = nx * ny * nz;
757 let grid = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
758 let dr = 137.0;
759 let qsm: Vec<f64> = (0..n).map(|i| 0.02 + 0.001 * (i % 5) as f64).collect();
760 let r2prime: Vec<f64> = qsm.iter().map(|&q| dr * q).collect(); let field = vec![0.0_f64; n];
762 let magnitude = vec![1.0_f64; n];
763 let mask = vec![1u8; n];
764 let params = ChiSepIlsqrParams {
765 max_iter: 0,
766 ..ChiSepIlsqrParams::default()
767 };
768 let (p, q, t) = chi_sep_ilsqr(
769 &field, &r2prime, &magnitude, &qsm, &mask, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {},
770 );
771 assert_eq!(p.len(), n, "output must be cropped back to input size");
772 for i in 0..n {
773 assert!(
774 (p[i] - qsm[i]).abs() < 1e-6 && q[i].abs() < 1e-6,
775 "voxel {}: init through pad/crop path should be exact",
776 i
777 );
778 assert!((t[i] - p[i] - q[i]).abs() < 1e-10);
779 }
780 }
781
782 #[test]
785 fn test_chi_sep_ilsqr_init_solves_linear_system() {
786 let grid = Grid::new(2, 1, 1, 1.0, 1.0, 1.0);
787 let qsm = vec![0.1, 0.02];
789 let dr = 137.0;
790 let r2prime = vec![dr * 0.1, dr * (0.06 + 0.04)];
791 let field = vec![0.0, 0.0]; let magnitude = vec![1.0, 1.0];
793 let mask = vec![1u8, 1];
794 let params = ChiSepIlsqrParams {
795 max_iter: 0,
796 ..ChiSepIlsqrParams::default()
797 };
798 let (p, q, _) = chi_sep_ilsqr(
799 &field, &r2prime, &magnitude, &qsm, &mask, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {},
800 );
801 assert!((p[0] - 0.1).abs() < 1e-6 && q[0].abs() < 1e-6, "pure para voxel");
802 assert!(
803 (p[1] - 0.06).abs() < 1e-6 && (q[1] + 0.04).abs() < 1e-6,
804 "mixed voxel: got ({:.4}, {:.4})",
805 p[1],
806 q[1]
807 );
808 }
809}