1use crate::fft::Fft3dWorkspace;
72use crate::inversion::admm::prepare_fansi_spectral;
73use crate::utils::special::{erfc, erfcx};
74use crate::error::InputError;
75use crate::utils::wavelet::{Periodization, WaveletPlan};
76use crate::Grid;
77use num_complex::Complex64;
78#[cfg(feature = "parallel")]
79use rayon::prelude::*;
80
81const EPS: f64 = 2.220446049250313e-16;
82
83const PARAM_CHUNK: usize = 1 << 16;
86
87#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
89#[derive(Clone, Debug)]
90pub struct AmpPeParams {
91 pub wave_order: usize,
93 pub nlevel: usize,
95 pub wave_pec: f64,
97 pub simulated_te: f64,
99 pub max_linearization_ite: usize,
101 pub b0: f64,
103 pub gyro_ratio: f64,
106 pub damp_rate_sig: f64,
108 pub damp_rate_par: f64,
110 pub max_pe_spar_ite: usize,
112 pub max_pe_est_ite: usize,
114 pub cvg_thd: f64,
116 pub tikhonov_beta: f64,
118 pub normalize_weight: bool,
122}
123
124impl Default for AmpPeParams {
125 fn default() -> Self {
126 Self {
127 wave_order: 1,
128 nlevel: 3,
129 wave_pec: 0.85,
130 simulated_te: 8e-3,
131 max_linearization_ite: 25,
132 b0: 3.0,
133 gyro_ratio: 42.58,
134 damp_rate_sig: 0.01,
135 damp_rate_par: 0.1,
136 max_pe_spar_ite: 5,
137 max_pe_est_ite: 5,
138 cvg_thd: 1e-6,
139 tikhonov_beta: 2e-2,
140 normalize_weight: false,
141 }
142 }
143}
144
145impl AmpPeParams {
146 pub fn validate(&self) -> Result<(), InputError> {
152 crate::utils::wavelet::check_order("AMP-PE wave_order", self.wave_order)?;
153 if self.nlevel > MAX_NLEVEL {
154 return Err(InputError::param("AMP-PE nlevel", self.nlevel, &format!("<= {MAX_NLEVEL}")));
155 }
156 Ok(())
157 }
158}
159
160pub const MAX_NLEVEL: usize = usize::BITS as usize - 1;
163
164fn var(v: &[f64]) -> f64 {
166 let n = v.len();
167 if n < 2 {
168 return 0.0;
169 }
170 let mean = v.iter().sum::<f64>() / n as f64;
171 v.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n as f64 - 1.0)
172}
173
174fn var_complex(v: &[Complex64]) -> f64 {
177 let n = v.len();
178 if n < 2 {
179 return 0.0;
180 }
181 let mean = v.iter().sum::<Complex64>() / n as f64;
182 v.iter().map(|&x| (x - mean).norm_sqr()).sum::<f64>() / (n as f64 - 1.0)
183}
184
185struct DipoleOp {
187 fft_ws: Fft3dWorkspace,
188 kernel: Vec<f64>,
189 n: usize,
190 mask_idx: Vec<usize>,
191 buf: Vec<Complex64>,
192 full: Vec<f64>,
193 scatter: Vec<f64>,
197}
198
199impl DipoleOp {
200 fn new(grid: &Grid, bdir: (f64, f64, f64), mask_idx: Vec<usize>) -> Self {
201 let (fft_ws, kernel, _ee2) = prepare_fansi_spectral(grid, bdir);
202 let n = grid.n_total();
203 Self {
204 fft_ws,
205 kernel,
206 n,
207 mask_idx,
208 buf: vec![Complex64::new(0.0, 0.0); n],
209 full: vec![0.0; n],
210 scatter: vec![0.0; n],
211 }
212 }
213
214 fn apply(&mut self, x: &[f64], out: &mut [f64]) {
216 for (b, &xi) in self.buf.iter_mut().zip(x) {
217 *b = Complex64::new(xi, 0.0);
218 }
219 self.fft_ws.fft3d(&mut self.buf);
220 for (b, &k) in self.buf.iter_mut().zip(&self.kernel) {
221 *b *= k;
222 }
223 self.fft_ws.ifft3d(&mut self.buf);
224 for (o, b) in out.iter_mut().zip(&self.buf) {
225 *o = b.re;
226 }
227 }
228
229 fn field_masked(&mut self, x_full: &[f64]) -> Vec<f64> {
231 let mut full = std::mem::take(&mut self.full);
232 self.apply(x_full, &mut full);
233 let out = self.mask_idx.iter().map(|&j| full[j]).collect();
234 self.full = full;
235 out
236 }
237
238 fn adjoint_full(&mut self, y_masked: &[f64]) -> Vec<f64> {
240 let mut scattered = std::mem::take(&mut self.scatter);
241 for (k, &j) in self.mask_idx.iter().enumerate() {
242 scattered[j] = y_masked[k];
243 }
244 let mut out = vec![0.0; self.n];
245 self.apply(&scattered, &mut out);
246 self.scatter = scattered;
247 out
248 }
249}
250
251pub fn amp_pe(
268 local_field: &[f64],
269 mask: &[u8],
270 magnitude: Option<&[f64]>,
271 morphology_magnitude: Option<&[f64]>,
272 grid: &Grid,
273 bdir: (f64, f64, f64),
274 params: &AmpPeParams,
275 mut progress: impl FnMut(usize, usize),
276) -> Result<Vec<f64>, InputError> {
277 params.validate()?;
278 let orig = (grid.nx(), grid.ny(), grid.nz());
279 let pad_mult = 1usize << params.nlevel;
280 let up = |n: usize| n.div_ceil(pad_mult).checked_mul(pad_mult);
281 let pad = match (up(orig.0), up(orig.1), up(orig.2)) {
282 (Some(a), Some(b), Some(c)) if a.checked_mul(b).and_then(|ab| ab.checked_mul(c)).is_some() => (a, b, c),
283 _ => {
284 return Err(InputError(format!(
285 "AMP-PE nlevel = {} pads the {}x{}x{} grid past the addressable size",
286 params.nlevel, orig.0, orig.1, orig.2,
287 )))
288 }
289 };
290 let pgrid = Grid::new(
291 pad.0,
292 pad.1,
293 pad.2,
294 grid.voxel_size.0,
295 grid.voxel_size.1,
296 grid.voxel_size.2,
297 );
298
299 let field_p = pad_volume(local_field, orig, pad);
301 let mask_f: Vec<f64> = mask.iter().map(|&m| if m != 0 { 1.0 } else { 0.0 }).collect();
302 let mask_p = pad_volume(&mask_f, orig, pad);
303 let imag_p = match magnitude {
304 Some(mag) => pad_volume(&mag.iter().map(|&v| v.abs()).collect::<Vec<_>>(), orig, pad),
305 None => mask_p.clone(),
306 };
307 let have_mag = magnitude.is_some();
308 let morph_p = match morphology_magnitude {
309 Some(mag) => pad_volume(&mag.iter().map(|&v| v.abs()).collect::<Vec<_>>(), orig, pad),
310 None => imag_p.clone(),
311 };
312
313 let n = pad.0 * pad.1 * pad.2;
314 let mask_idx: Vec<usize> = (0..n).filter(|&i| mask_p[i] > 0.5).collect();
315 let m = mask_idx.len();
316
317 let te = params.simulated_te;
318 let mut_cst = params.gyro_ratio * params.b0 * 2.0 * std::f64::consts::PI * te;
319
320 let phase_image: Vec<f64> = mask_idx.iter().map(|&j| field_p[j] * mut_cst).collect();
322
323 let weight_vect: Vec<f64> = if have_mag {
326 let wv: Vec<f64> = mask_idx.iter().map(|&j| imag_p[j]).collect();
327 if params.normalize_weight {
328 let mean = (wv.iter().sum::<f64>() / m as f64).max(EPS);
329 wv.iter().map(|&v| v / mean).collect()
330 } else {
331 wv
332 }
333 } else {
334 vec![1.0; m]
335 };
336
337 let mut dip = DipoleOp::new(&pgrid, bdir, mask_idx.clone());
338 let kernel_energy = dip.kernel.iter().map(|&d| d * d).sum::<f64>() / n as f64;
340
341 let plan = WaveletPlan::new(params.wave_order, pad, params.nlevel, Periodization::Matlab);
343 debug_assert_eq!(plan.coef_len(), n);
344
345 let field_masked: Vec<f64> = (0..n).map(|i| if mask_p[i] > 0.5 { field_p[i] } else { 0.0 }).collect();
349 let chi_l2 = chi_l2_seed(&field_masked, &mask_p, &dip.kernel, params.tikhonov_beta, pad);
350 let x_init_par: Vec<f64> = (0..n).map(|i| if mask_p[i] > 0.5 { chi_l2[i] } else { 0.0 }).collect();
351 let x_init_par_psi = plan.forward(&x_init_par);
352
353 let wave_mask = build_wave_mask(&plan, &morph_p, &mask_p, have_mag || morphology_magnitude.is_some(), params.wave_pec, n);
355
356 let mut st = GampState {
358 x_hat_meas: vec![0.0; n],
359 tau_x_meas: var(&x_init_par),
360 s_hat_meas: vec![Complex64::new(0.0, 0.0); m],
361 x_hat_psi: vec![0.0; n],
362 p_hat_psi: vec![0.0; n],
363 tau_x_hat_psi: var(&x_init_par_psi),
364 tau_p_psi: var(&x_init_par_psi), damp: params.damp_rate_sig,
366 damp_ceiling: params.damp_rate_sig,
367 };
368 let abs_psi: Vec<f64> = x_init_par_psi.iter().map(|v| v.abs()).collect();
369 let mut lambda = 1.0 / (var(&abs_psi) / 2.0).sqrt();
370
371 let cfg = GampCfg {
372 m,
373 n,
374 mut_cst,
375 kernel_energy,
376 max_pe_spar_ite: params.max_pe_spar_ite,
377 max_pe_est_ite: params.max_pe_est_ite,
378 cvg_thd: params.cvg_thd,
379 kappa: params.damp_rate_par,
380 };
383
384 let total = 2 * params.max_linearization_ite;
385 let mut x_curr = vec![0.0; n];
386 let mut tau_w_1 = 1e-12;
387
388 for it in 0..params.max_linearization_ite {
390 progress(it + 1, total);
391 let (a_x, der_1st, y_upd) = linearize(&mut dip, &x_curr, &phase_image, &weight_vect, mut_cst);
392 tau_w_1 = gamp_awgn(&mut dip, &plan, &cfg, &mut st, &mut lambda, tau_w_1, &der_1st, &weight_vect, &y_upd, &wave_mask);
393 x_curr = st.x_hat_meas.clone();
394 let _ = a_x;
395 }
396
397 let a_x_init = dip.field_masked(&x_curr);
399 let resid: Vec<Complex64> = (0..m)
400 .map(|j| {
401 let axm = a_x_init[j] * mut_cst;
402 weight_vect[j] * (Complex64::new(0.0, axm).exp() - Complex64::new(0.0, phase_image[j]).exp())
403 })
404 .collect();
405 let resid_abs: Vec<f64> = resid.iter().map(|c| c.norm()).collect();
406 let resid_std = (resid_abs.iter().map(|v| v * v).sum::<f64>() / m as f64).sqrt();
407 let outliers: Vec<Complex64> = resid
408 .iter()
409 .zip(&resid_abs)
410 .filter(|(_, &a)| a > 3.0 * resid_std)
411 .map(|(&c, _)| c)
412 .collect();
413 let gamma_est = outliers.len() as f64 / m as f64;
414 let psi_est = var_complex(&outliers);
415
416 let mut mix = MixState {
418 theta: 0.0,
419 phi: tau_w_1,
420 omega: 1.0,
421 gamma: gamma_est,
422 psi: psi_est,
423 };
424 for it in 0..params.max_linearization_ite {
425 progress(params.max_linearization_ite + it + 1, total);
426 let (a_x, der_1st, y_upd) = linearize(&mut dip, &x_curr, &phase_image, &weight_vect, mut_cst);
427 gamp_awgn_mix(&mut dip, &plan, &cfg, &mut st, &mut mix, &mut lambda, &der_1st, &weight_vect, &y_upd, &wave_mask);
428 x_curr = st.x_hat_meas.clone();
429 let _ = a_x;
430 }
431
432 for i in 0..n {
434 if mask_p[i] <= 0.5 {
435 x_curr[i] = 0.0;
436 }
437 }
438 Ok(crop_volume(&x_curr, pad, orig))
439}
440
441struct GampState {
443 x_hat_meas: Vec<f64>, tau_x_meas: f64,
445 s_hat_meas: Vec<Complex64>, x_hat_psi: Vec<f64>, p_hat_psi: Vec<f64>, tau_x_hat_psi: f64,
449 tau_p_psi: f64,
450 damp: f64,
456 damp_ceiling: f64,
461}
462
463struct GampSnapshot {
465 x_hat_meas: Vec<f64>,
466 tau_x_meas: f64,
467 s_hat_meas: Vec<Complex64>,
468 x_hat_psi: Vec<f64>,
469 p_hat_psi: Vec<f64>,
470 tau_x_hat_psi: f64,
471 tau_p_psi: f64,
472 lambda: f64,
473 mix: Option<MixState>,
474}
475
476impl GampState {
477 fn snapshot(&self, lambda: f64, mix: Option<&MixState>) -> GampSnapshot {
478 GampSnapshot {
479 x_hat_meas: self.x_hat_meas.clone(),
480 tau_x_meas: self.tau_x_meas,
481 s_hat_meas: self.s_hat_meas.clone(),
482 x_hat_psi: self.x_hat_psi.clone(),
483 p_hat_psi: self.p_hat_psi.clone(),
484 tau_x_hat_psi: self.tau_x_hat_psi,
485 tau_p_psi: self.tau_p_psi,
486 lambda,
487 mix: mix.map(|m| MixState { ..*m }),
488 }
489 }
490
491 fn restore(&mut self, snap: &GampSnapshot, lambda: &mut f64, mix: Option<&mut MixState>) {
492 self.x_hat_meas.copy_from_slice(&snap.x_hat_meas);
493 self.tau_x_meas = snap.tau_x_meas;
494 self.s_hat_meas.copy_from_slice(&snap.s_hat_meas);
495 self.x_hat_psi.copy_from_slice(&snap.x_hat_psi);
496 self.p_hat_psi.copy_from_slice(&snap.p_hat_psi);
497 self.tau_x_hat_psi = snap.tau_x_hat_psi;
498 self.tau_p_psi = snap.tau_p_psi;
499 *lambda = snap.lambda;
500 if let (Some(dst), Some(src)) = (mix, snap.mix.as_ref()) {
501 *dst = MixState { ..*src };
502 }
503 }
504}
505
506const DAMP_DIVERGE_FACTOR: f64 = 2.0;
527const DAMP_SHRINK: f64 = 0.5;
529const DAMP_GROW: f64 = 1.1;
531const DAMP_MIN: f64 = 1e-4;
533const MAX_BACKTRACKS: usize = 20;
535
536#[cfg(test)]
537thread_local! {
538 static GUARD_FIRINGS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
541}
542
543#[inline]
545fn note_guard_firing() {
546 #[cfg(test)]
547 GUARD_FIRINGS.with(|c| c.set(c.get() + 1));
548}
549
550struct GampCfg {
552 m: usize,
553 n: usize,
554 mut_cst: f64,
555 kernel_energy: f64,
556 max_pe_spar_ite: usize,
557 max_pe_est_ite: usize,
558 cvg_thd: f64,
559 kappa: f64,
560}
561
562struct MixState {
564 theta: f64,
565 phi: f64,
566 omega: f64,
567 gamma: f64,
568 psi: f64,
569}
570
571fn linearize(
575 dip: &mut DipoleOp,
576 x_curr: &[f64],
577 phase_image: &[f64],
578 weight_vect: &[f64],
579 mut_cst: f64,
580) -> (Vec<f64>, Vec<Complex64>, Vec<Complex64>) {
581 let field = dip.field_masked(x_curr);
582 let m = field.len();
583 let a_x: Vec<f64> = (0..m).map(|j| field[j] * mut_cst).collect();
584 let der_1st: Vec<Complex64> = a_x.iter().map(|&v| Complex64::new(0.0, 1.0) * Complex64::new(0.0, v).exp()).collect();
585 let y_upd: Vec<Complex64> = (0..m)
586 .map(|j| {
587 let e_phase = Complex64::new(0.0, phase_image[j]).exp();
588 let e_ax = Complex64::new(0.0, a_x[j]).exp();
589 weight_vect[j] * (der_1st[j] * a_x[j] + e_phase - e_ax)
590 })
591 .collect();
592 (a_x, der_1st, y_upd)
593}
594
595fn frob_qsm_sq(cfg: &GampCfg, der_1st: &[Complex64], weight_vect: &[f64]) -> f64 {
600 let sw: f64 = der_1st
601 .iter()
602 .zip(weight_vect)
603 .map(|(&d, &w)| (d * w).norm_sqr())
604 .sum();
605 cfg.mut_cst * cfg.mut_cst * cfg.kernel_energy * sw
606}
607
608fn a_qsm_mult(dip: &mut DipoleOp, x_img: &[f64], der_1st: &[Complex64], weight_vect: &[f64], mut_cst: f64) -> Vec<Complex64> {
610 let field = dip.field_masked(x_img);
611 maybe_par_iter!(field)
612 .zip(maybe_par_iter!(der_1st))
613 .zip(maybe_par_iter!(weight_vect))
614 .map(|((&fj, &dj), &wj)| dj * wj * (fj * mut_cst))
615 .collect()
616}
617
618fn a_qsm_multtr_real(dip: &mut DipoleOp, s: &[Complex64], der_1st: &[Complex64], weight_vect: &[f64], mut_cst: f64) -> Vec<f64> {
620 let re: Vec<f64> = maybe_par_iter!(s)
622 .zip(maybe_par_iter!(der_1st))
623 .zip(maybe_par_iter!(weight_vect))
624 .map(|((&sj, &dj), &wj)| (sj * (dj * wj).conj()).re)
625 .collect();
626 let mut adj = dip.adjoint_full(&re);
627 maybe_par_iter_mut!(adj).for_each(|v| *v *= mut_cst);
628 adj
629}
630
631#[allow(clippy::too_many_arguments)]
633fn gamp_awgn(
634 dip: &mut DipoleOp,
635 plan: &WaveletPlan,
636 cfg: &GampCfg,
637 st: &mut GampState,
638 lambda: &mut f64,
639 tau_w_1_in: f64,
640 der_1st: &[Complex64],
641 weight_vect: &[f64],
642 y: &[Complex64],
643 wave_mask: &[bool],
644) -> f64 {
645 let frob_sq = frob_qsm_sq(cfg, der_1st, weight_vect);
646 let mut tau_w_1 = tau_w_1_in;
647 let (m, n) = (cfg.m, cfg.n);
648
649 let mut best_cost = f64::INFINITY;
650 let mut good: Option<GampSnapshot> = None;
651 let mut good_tau_w = tau_w_1;
652 let mut accepted = 0usize;
653 let mut backtracks = 0usize;
654
655 while accepted < cfg.max_pe_spar_ite && backtracks <= MAX_BACKTRACKS {
656 let tau_p_meas_1 = frob_sq / m as f64 * st.tau_x_meas;
658 let axm = a_qsm_mult(dip, &st.x_hat_meas, der_1st, weight_vect, cfg.mut_cst);
660 let p_hat_meas_1: Vec<Complex64> = (0..m).map(|j| axm[j] - tau_p_meas_1 * st.s_hat_meas[j]).collect();
661
662 let mse: f64 = maybe_par_chunks!(p_hat_meas_1.as_slice(), PARAM_CHUNK)
665 .zip(maybe_par_chunks!(y, PARAM_CHUNK))
666 .map(|(pc, yc)| pc.iter().zip(yc).map(|(p, yy)| (p - yy).norm_sqr()).sum::<f64>())
667 .collect::<Vec<f64>>()
668 .iter()
669 .sum::<f64>()
670 / m as f64;
671
672 let fit = data_fit(&axm, y, m);
675 if !fit.is_finite() || fit > best_cost * DAMP_DIVERGE_FACTOR {
676 note_guard_firing();
677 backtracks += 1;
678 if let Some(g) = good.as_ref() {
679 st.restore(g, lambda, None);
680 tau_w_1 = good_tau_w;
681 }
682 st.damp = (st.damp * DAMP_SHRINK).max(DAMP_MIN);
683 st.damp_ceiling = st.damp;
684 if st.damp <= DAMP_MIN {
685 break; }
687 continue;
688 }
689 if fit < best_cost {
690 best_cost = fit;
691 good = Some(st.snapshot(*lambda, None));
692 good_tau_w = tau_w_1;
693 }
694 st.damp = (st.damp * DAMP_GROW).min(st.damp_ceiling);
695
696 let tau_w_new = mse + tau_p_meas_1;
697 for _ in 0..cfg.max_pe_est_ite {
698 tau_w_1 += cfg.kappa * (tau_w_new - tau_w_1);
699 }
700
701 let tau_s_meas_1 = 1.0 / (tau_w_1 + tau_p_meas_1);
702 for j in 0..m {
703 st.s_hat_meas[j] = (y[j] - p_hat_meas_1[j]) * tau_s_meas_1;
704 }
705
706 let tau_r_meas_1 = 1.0 / (frob_sq / n as f64 * tau_s_meas_1);
708 let mtr = a_qsm_multtr_real(dip, &st.s_hat_meas, der_1st, weight_vect, cfg.mut_cst);
709 let r_hat_meas_1: Vec<f64> = maybe_par_iter!(st.x_hat_meas)
710 .zip(maybe_par_iter!(mtr))
711 .map(|(&x, &mv)| x + tau_r_meas_1 * mv)
712 .collect();
713
714 let cvg = wavelet_block(plan, cfg, st, lambda, tau_r_meas_1, &r_hat_meas_1, wave_mask);
715 accepted += 1;
716 if cvg < cfg.cvg_thd {
717 break;
718 }
719 }
720 tau_w_1
721}
722
723fn data_fit(axm: &[Complex64], y: &[Complex64], m: usize) -> f64 {
725 maybe_par_chunks!(axm, PARAM_CHUNK)
726 .zip(maybe_par_chunks!(y, PARAM_CHUNK))
727 .map(|(ac, yc)| ac.iter().zip(yc).map(|(a, yy)| (a - yy).norm_sqr()).sum::<f64>())
728 .collect::<Vec<f64>>()
729 .iter()
730 .sum::<f64>()
731 / m as f64
732}
733
734#[allow(clippy::too_many_arguments)]
736fn gamp_awgn_mix(
737 dip: &mut DipoleOp,
738 plan: &WaveletPlan,
739 cfg: &GampCfg,
740 st: &mut GampState,
741 mix: &mut MixState,
742 lambda: &mut f64,
743 der_1st: &[Complex64],
744 weight_vect: &[f64],
745 y: &[Complex64],
746 wave_mask: &[bool],
747) {
748 let frob_sq = frob_qsm_sq(cfg, der_1st, weight_vect);
749 let (m, n) = (cfg.m, cfg.n);
750
751 let mut best_cost = f64::INFINITY;
752 let mut good: Option<GampSnapshot> = None;
753 let mut accepted = 0usize;
754 let mut backtracks = 0usize;
755
756 while accepted < cfg.max_pe_spar_ite && backtracks <= MAX_BACKTRACKS {
757 let tau_p_meas_1 = frob_sq / m as f64 * st.tau_x_meas;
758 let axm = a_qsm_mult(dip, &st.x_hat_meas, der_1st, weight_vect, cfg.mut_cst);
759 let p_hat_meas_1: Vec<Complex64> = (0..m).map(|j| axm[j] - tau_p_meas_1 * st.s_hat_meas[j]).collect();
760 let r_noise: Vec<Complex64> = (0..m).map(|j| y[j] - p_hat_meas_1[j]).collect();
761
762 let fit = data_fit(&axm, y, m);
764 if !fit.is_finite() || fit > best_cost * DAMP_DIVERGE_FACTOR {
765 note_guard_firing();
766 backtracks += 1;
767 if let Some(g) = good.as_ref() {
768 st.restore(g, lambda, Some(mix));
769 }
770 st.damp = (st.damp * DAMP_SHRINK).max(DAMP_MIN);
771 st.damp_ceiling = st.damp;
772 if st.damp <= DAMP_MIN {
773 break;
774 }
775 continue;
776 }
777 if fit < best_cost {
778 best_cost = fit;
779 good = Some(st.snapshot(*lambda, Some(mix)));
780 }
781 st.damp = (st.damp * DAMP_GROW).min(st.damp_ceiling);
782
783 for _ in 0..cfg.max_pe_est_ite {
784 mix_output_parameter_est(&r_noise, tau_p_meas_1, mix, cfg.kappa);
785 }
786 let (noise_update, tau_z) = mix_output_function(&r_noise, tau_p_meas_1, mix);
787 let tau_s_meas_1 = 1.0 / tau_p_meas_1 * (1.0 - tau_z / tau_p_meas_1);
789 for j in 0..m {
790 let z_hat = y[j] - noise_update[j];
791 st.s_hat_meas[j] = 1.0 / tau_p_meas_1 * (z_hat - p_hat_meas_1[j]);
792 }
793
794 let tau_r_meas_1 = 1.0 / (frob_sq / n as f64 * tau_s_meas_1);
795 let mtr = a_qsm_multtr_real(dip, &st.s_hat_meas, der_1st, weight_vect, cfg.mut_cst);
796 let r_hat_meas_1: Vec<f64> = maybe_par_iter!(st.x_hat_meas)
797 .zip(maybe_par_iter!(mtr))
798 .map(|(&x, &mv)| x + tau_r_meas_1 * mv)
799 .collect();
800
801 let cvg = wavelet_block(plan, cfg, st, lambda, tau_r_meas_1, &r_hat_meas_1, wave_mask);
802 accepted += 1;
803 if cvg < cfg.cvg_thd {
804 break;
805 }
806 }
807}
808
809fn wavelet_block(
814 plan: &WaveletPlan,
815 cfg: &GampCfg,
816 st: &mut GampState,
817 lambda: &mut f64,
818 tau_r_meas_1: f64,
819 r_hat_meas_1: &[f64],
820 wave_mask: &[bool],
821) -> f64 {
822 let damp = st.damp;
825 let tau_s_psi = 1.0 / (tau_r_meas_1 + st.tau_p_psi);
826 let s_hat_psi: Vec<f64> = maybe_par_iter!(r_hat_meas_1)
827 .zip(maybe_par_iter!(st.p_hat_psi))
828 .map(|(&r, &p)| (r - p) * tau_s_psi)
829 .collect();
830
831 let tau_r_psi = 1.0 / tau_s_psi;
833 let analysis = plan.forward(&s_hat_psi); let r_hat_psi: Vec<f64> = maybe_par_iter!(st.x_hat_psi)
835 .zip(maybe_par_iter!(analysis))
836 .map(|(&x, &a)| x + tau_r_psi * a)
837 .collect();
838
839 let abs_r: Vec<f64> = maybe_par_iter!(r_hat_psi).map(|v| v.abs()).collect();
840 for _ in 0..cfg.max_pe_est_ite {
841 *lambda = input_parameter_est(&abs_r, tau_r_psi, *lambda, cfg.kappa);
842 }
843
844 let (x_hat_psi_new, tau_x_hat_psi) = input_function(&r_hat_psi, tau_r_psi, *lambda, wave_mask);
845 st.x_hat_psi = x_hat_psi_new;
846 st.tau_x_hat_psi = tau_x_hat_psi;
847
848 st.tau_p_psi = tau_x_hat_psi; let synth = plan.inverse(&st.x_hat_psi); let tau_p_psi = st.tau_p_psi;
851 maybe_par_iter_mut!(st.p_hat_psi)
852 .zip(maybe_par_iter!(synth))
853 .zip(maybe_par_iter!(s_hat_psi))
854 .for_each(|((p, &sy), &sh)| *p = sy - tau_p_psi * sh);
855
856 let tau_x_meas_pre = st.tau_x_meas;
857 let tau_new = (st.tau_p_psi * tau_r_meas_1) / (st.tau_p_psi + tau_r_meas_1);
858 st.tau_x_meas = tau_x_meas_pre + damp * (tau_new - tau_x_meas_pre);
859
860 let denom = st.tau_p_psi + tau_r_meas_1;
861 let mut change_sq = 0.0;
862 let mut norm_sq = 0.0;
863 for ((xh, &r), &p) in st.x_hat_meas.iter_mut().zip(r_hat_meas_1).zip(&st.p_hat_psi) {
864 let x_new = (st.tau_p_psi * r + tau_r_meas_1 * p) / denom;
865 let updated = *xh + damp * (x_new - *xh);
866 let d = updated - *xh;
867 change_sq += d * d;
868 norm_sq += updated * updated;
869 *xh = updated;
870 }
871 change_sq.sqrt() / norm_sq.sqrt().max(EPS)
872}
873
874fn input_function(r_hat: &[f64], tau_r: f64, lambda: f64, wave_mask: &[bool]) -> (Vec<f64>, f64) {
877 let thresh = lambda * tau_r;
878 let mut x0 = vec![0.0; r_hat.len()];
879 let mut nnz = 0usize;
880 for i in 0..r_hat.len() {
881 let v = if wave_mask[i] {
882 r_hat[i]
883 } else {
884 let a = r_hat[i].abs() - thresh;
885 if a > 0.0 {
886 a * r_hat[i].signum()
887 } else {
888 0.0
889 }
890 };
891 if v != 0.0 {
892 nnz += 1;
893 }
894 x0[i] = v;
895 }
896 let tau_x = tau_r * nnz as f64 / r_hat.len() as f64;
897 (x0, tau_x)
898}
899
900fn input_parameter_est(r_hat: &[f64], tau_r: f64, lambda: f64, kappa: f64) -> f64 {
902 let s = (0.5 / tau_r).sqrt();
903 let partials: Vec<(f64, f64)> = maybe_par_chunks!(r_hat, PARAM_CHUNK)
906 .map(|chunk| {
907 let mut s1 = 0.0;
908 let mut s2 = 0.0;
909 for &r in chunk {
910 let arg = tau_r * lambda - r;
911 let b0 = lambda / 2.0 * erfc(s * arg);
913 let der = (2.0 * tau_r / std::f64::consts::PI).sqrt() / erfcx(s * arg) + r - tau_r * lambda;
914 let fst = 1.0 / lambda - der;
915 let scd = -1.0 / (lambda * lambda) + (tau_r + (r - tau_r * lambda) * der) - der * der;
916 let w = b0 / (b0 + EPS);
917 s1 += w * fst;
918 s2 += w * scd;
919 }
920 (s1, s2)
921 })
922 .collect();
923 let (sum1, sum2) = partials.iter().fold((0.0, 0.0), |a, p| (a.0 + p.0, a.1 + p.1));
924 let lambda_new = if sum2 < 0.0 {
925 lambda - sum1 / sum2
926 } else if sum1 > 0.0 {
927 lambda * 1.1
928 } else {
929 lambda * 0.9
930 };
931 let lambda_new = lambda_new.max(1e-12);
932 lambda + kappa * (lambda_new - lambda)
933}
934
935fn mix_output_function(r_hat: &[Complex64], tau_r: f64, mix: &MixState) -> (Vec<Complex64>, f64) {
938 let (omega, theta, phi, gamma, psi) = (mix.omega, mix.theta, mix.phi, mix.gamma, mix.psi);
939 let mut x_hat = vec![Complex64::new(0.0, 0.0); r_hat.len()];
940 let partials: Vec<f64> = maybe_par_chunks!(r_hat, PARAM_CHUNK)
942 .zip(maybe_par_chunks_mut!(x_hat, PARAM_CHUNK))
943 .map(|(rc, xc)| {
944 let mut tau_sum = 0.0;
945 for (&r, xh_out) in rc.iter().zip(xc.iter_mut()) {
946 let diff = theta - r.norm(); let e1 = (-(diff / (phi + tau_r).sqrt()).powi(2)).exp();
949 let block1 = (1.0 - gamma) * omega * (tau_r / (phi + tau_r)) * e1;
950 let mean1 = (theta * tau_r + r * phi) / (phi + tau_r);
951 let block_nmr1 = block1 * mean1;
952 let e2 = (-(r.norm() / (psi + tau_r).sqrt()).powi(2)).exp();
954 let block2 = gamma * (tau_r / (psi + tau_r)) * e2;
955 let mean2 = r * psi / (psi + tau_r);
956 let block_nmr2 = block2 * mean2;
957
958 let nmr = block_nmr1 + block_nmr2;
959 let dnm = block1 + block2;
960 let xh = if dnm == 0.0 { r } else { nmr / dnm };
961 *xh_out = xh;
962
963 let nmr_sq1 = block1 * (phi * tau_r / (phi + tau_r) + mean1.norm_sqr());
964 let nmr_sq2 = block2 * (psi * tau_r / (psi + tau_r) + mean2.norm_sqr());
965 let nmr_sq = nmr_sq1 + nmr_sq2;
966 let tau_seq = if dnm == 0.0 { 0.0 } else { nmr_sq / dnm - xh.norm_sqr() };
967 tau_sum += tau_seq;
968 }
969 tau_sum
970 })
971 .collect();
972 let tau_sum: f64 = partials.iter().sum();
973 let tau_x = (tau_sum / r_hat.len() as f64).max(1e-12);
974 (x_hat, tau_x)
975}
976
977fn mix_output_parameter_est(r_hat: &[Complex64], tau_r: f64, mix: &mut MixState, kappa: f64) {
979 let (omega, theta, phi, gamma, psi) = (mix.omega, mix.theta, mix.phi, mix.gamma, mix.psi);
980 let partials: Vec<(f64, f64, f64, f64)> = maybe_par_chunks!(r_hat, PARAM_CHUNK)
982 .map(|chunk| {
983 let (mut sum_b1, mut sum_b2, mut sum_phi_num, mut sum_psi_num) = (0.0, 0.0, 0.0, 0.0);
984 for &r in chunk {
985 let d1 = r.norm() - theta; let t1 = (1.0 - gamma) * omega / (tau_r + phi) * (-(d1 / (tau_r + phi).sqrt()).powi(2)).exp();
987 let t2 = gamma / (tau_r + psi) * (-(r.norm() / (tau_r + psi).sqrt()).powi(2)).exp();
988 let sum = t1 + t2;
989 let (b1, b2) = if sum == 0.0 { (0.0, 1.0) } else { (t1 / sum, t2 / sum) };
990 sum_b1 += b1;
991 sum_b2 += b2;
992 sum_phi_num += b1 * d1 * d1;
993 sum_psi_num += b2 * r.norm_sqr();
994 }
995 (sum_b1, sum_b2, sum_phi_num, sum_psi_num)
996 })
997 .collect();
998 let (sum_b1, sum_b2, sum_phi_num, sum_psi_num) = partials
999 .iter()
1000 .fold((0.0, 0.0, 0.0, 0.0), |a, p| (a.0 + p.0, a.1 + p.1, a.2 + p.2, a.3 + p.3));
1001 let omega_new = 1.0;
1003 mix.omega = omega + kappa * (omega_new - omega);
1004 mix.theta = 0.0;
1005 let mut phi_new = if sum_b1 != 0.0 { sum_phi_num / sum_b1 - tau_r } else { phi };
1007 if !phi_new.is_finite() || phi_new < 0.0 {
1008 phi_new = phi;
1009 }
1010 mix.phi = phi + kappa * (phi_new - phi);
1011 let mut psi_new = if sum_b2 != 0.0 { sum_psi_num / sum_b2 - tau_r } else { psi };
1013 if psi_new < 0.0 {
1014 psi_new = psi;
1015 }
1016 mix.psi = psi + kappa * (psi_new - psi);
1017}
1018
1019fn chi_l2_seed(phase: &[f64], mask: &[f64], kernel: &[f64], beta: f64, dims: (usize, usize, usize)) -> Vec<f64> {
1022 let (nx, ny, nz) = dims;
1023 let n = nx * ny * nz;
1024 let tau = std::f64::consts::TAU;
1025 let mut e2 = vec![0.0f64; n];
1027 for k in 0..nz {
1028 for j in 0..ny {
1029 for i in 0..nx {
1030 let idx = i + j * nx + k * nx * ny;
1031 let ex = Complex64::new(1.0, 0.0) - Complex64::new(0.0, tau * i as f64 / nx as f64).exp();
1032 let ey = Complex64::new(1.0, 0.0) - Complex64::new(0.0, tau * j as f64 / ny as f64).exp();
1033 let ez = Complex64::new(1.0, 0.0) - Complex64::new(0.0, tau * k as f64 / nz as f64).exp();
1034 e2[idx] = ex.norm_sqr() + ey.norm_sqr() + ez.norm_sqr();
1035 }
1036 }
1037 }
1038 let mut ws = Fft3dWorkspace::new(nx, ny, nz);
1039 let mut buf: Vec<Complex64> = phase.iter().map(|&p| Complex64::new(p, 0.0)).collect();
1040 ws.fft3d(&mut buf);
1041 for i in 0..n {
1042 let k2 = kernel[i] * kernel[i];
1043 buf[i] = kernel[i] * buf[i] / (k2 + beta * e2[i] + EPS);
1044 }
1045 ws.ifft3d(&mut buf);
1046 (0..n).map(|i| buf[i].re * mask[i]).collect()
1047}
1048
1049fn build_wave_mask(plan: &WaveletPlan, imag: &[f64], mask: &[f64], have_mag: bool, wave_pec: f64, n: usize) -> Vec<bool> {
1052 if !have_mag {
1053 return vec![true; n];
1054 }
1055 let imag_masked: Vec<f64> = (0..n).map(|i| if mask[i] > 0.5 { imag[i] } else { 0.0 }).collect();
1056 let magwav = plan.forward(&imag_masked);
1057 let mut abs_sorted: Vec<f64> = magwav.iter().map(|v| v.abs()).collect();
1058 abs_sorted.sort_by(|a, b| b.partial_cmp(a).unwrap()); let total: f64 = abs_sorted.iter().sum();
1060 let mut cum = 0.0;
1062 let mut count = 0usize;
1063 for &v in &abs_sorted {
1064 cum += v;
1065 if cum / total <= wave_pec {
1066 count += 1;
1067 } else {
1068 break;
1069 }
1070 }
1071 let thd = abs_sorted[count.max(1) - 1];
1072 magwav.iter().map(|v| v.abs() > thd).collect()
1073}
1074
1075fn pad_volume(v: &[f64], orig: (usize, usize, usize), pad: (usize, usize, usize)) -> Vec<f64> {
1076 if orig == pad {
1077 return v.to_vec();
1078 }
1079 let (ox, oy, oz) = orig;
1080 let (px, py, pz) = pad;
1081 let mut out = vec![0.0; px * py * pz];
1082 for k in 0..oz {
1083 for j in 0..oy {
1084 for i in 0..ox {
1085 out[i + j * px + k * px * py] = v[i + j * ox + k * ox * oy];
1086 }
1087 }
1088 }
1089 out
1090}
1091
1092fn crop_volume(v: &[f64], pad: (usize, usize, usize), orig: (usize, usize, usize)) -> Vec<f64> {
1093 if orig == pad {
1094 return v.to_vec();
1095 }
1096 let (ox, oy, oz) = orig;
1097 let (px, py, _pz) = pad;
1098 let mut out = vec![0.0; ox * oy * oz];
1099 for k in 0..oz {
1100 for j in 0..oy {
1101 for i in 0..ox {
1102 out[i + j * ox + k * ox * oy] = v[i + j * px + k * px * py];
1103 }
1104 }
1105 }
1106 out
1107}
1108
1109#[cfg(test)]
1110mod tests {
1111 use super::*;
1112
1113 #[test]
1114 fn amp_pe_masks_output() {
1115 let n = 16;
1117 let field: Vec<f64> = (0..n * n * n)
1118 .map(|i| {
1119 let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
1120 0.02 * (((x + 2 * y + 3 * z) as f64) * 0.3).sin()
1121 })
1122 .collect();
1123 let mut mask = vec![0u8; n * n * n];
1124 for z in 4..12 {
1126 for y in 4..12 {
1127 for x in 4..12 {
1128 mask[x + y * n + z * n * n] = 1;
1129 }
1130 }
1131 }
1132 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1133 let params = AmpPeParams { max_linearization_ite: 3, ..Default::default() };
1134 let chi = amp_pe(&field, &mask, None, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {}).unwrap();
1135 assert_eq!(chi.len(), n * n * n);
1136 for (i, &v) in chi.iter().enumerate() {
1137 assert!(v.is_finite(), "chi[{i}] not finite: {v}");
1138 if mask[i] == 0 {
1139 assert_eq!(v, 0.0, "chi must be zero outside mask at {i}");
1140 }
1141 }
1142 }
1143
1144 #[test]
1145 fn amp_pe_ignores_out_of_mask_field() {
1146 let n = 16;
1150 let base: Vec<f64> = (0..n * n * n)
1151 .map(|i| {
1152 let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
1153 0.02 * (((x + 2 * y + 3 * z) as f64) * 0.3).sin()
1154 })
1155 .collect();
1156 let mut mask = vec![0u8; n * n * n];
1157 for z in 4..12 {
1158 for y in 4..12 {
1159 for x in 4..12 {
1160 mask[x + y * n + z * n * n] = 1;
1161 }
1162 }
1163 }
1164 let mut with_bg = base.clone();
1166 for i in 0..with_bg.len() {
1167 if mask[i] == 0 {
1168 with_bg[i] += 0.5 * ((i % 11) as f64 - 5.0);
1169 }
1170 }
1171 let masked: Vec<f64> = (0..base.len()).map(|i| if mask[i] != 0 { base[i] } else { 0.0 }).collect();
1173
1174 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1175 let params = AmpPeParams { max_linearization_ite: 4, ..Default::default() };
1176 let a = amp_pe(&with_bg, &mask, None, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {}).unwrap();
1177 let b = amp_pe(&masked, &mask, None, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {}).unwrap();
1178 let maxerr = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f64::max);
1179 assert!(maxerr < 1e-9, "out-of-mask field leaked into result: maxerr={maxerr}");
1180 }
1181
1182 #[test]
1191 fn damping_guard_is_inert_on_well_conditioned_data() {
1192 let n = 16;
1193 let field: Vec<f64> = (0..n * n * n)
1194 .map(|i| {
1195 let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
1196 0.02 * (((x + 2 * y + 3 * z) as f64) * 0.3).sin()
1197 })
1198 .collect();
1199 let mag: Vec<f64> = (0..n * n * n)
1202 .map(|i| {
1203 let (x, y) = ((i % n) as f64, ((i / n) % n) as f64);
1204 100.0 + 30.0 * (x * 0.4).cos() + 20.0 * (y * 0.25).sin()
1205 })
1206 .collect();
1207 let mut mask = vec![0u8; n * n * n];
1208 for z in 4..12 {
1209 for y in 4..12 {
1210 for x in 4..12 {
1211 mask[x + y * n + z * n * n] = 1;
1212 }
1213 }
1214 }
1215 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1216 let params = AmpPeParams { max_linearization_ite: 4, ..Default::default() };
1217
1218 GUARD_FIRINGS.with(|c| c.set(0));
1219 let chi = amp_pe(&field, &mask, Some(&mag), None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {}).unwrap();
1220 let fired = GUARD_FIRINGS.with(|c| c.get());
1221
1222 assert_eq!(
1223 fired, 0,
1224 "divergence guard fired {fired}x on well-conditioned data — the trigger has been \
1225 loosened, and every AMP-PE result will have silently changed"
1226 );
1227 assert!(chi.iter().all(|v| v.is_finite()));
1228 }
1229
1230 #[test]
1231 fn amp_pe_finite_on_ramp() {
1232 let n = 16;
1233 let field: Vec<f64> = (0..n * n * n).map(|i| ((i % 7) as f64 - 3.0) * 0.01).collect();
1234 let mask = vec![1u8; n * n * n];
1235 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1236 let params = AmpPeParams { max_linearization_ite: 3, ..Default::default() };
1237 let chi = amp_pe(&field, &mask, None, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {}).unwrap();
1238 for &v in &chi {
1239 assert!(v.is_finite());
1240 }
1241 }
1242
1243 #[test]
1246 fn bad_wave_order_and_nlevel_are_errors() {
1247 let grid = Grid::new(4, 4, 4, 1.0, 1.0, 1.0);
1248 let run = |p: AmpPeParams| amp_pe(&[0.0; 64], &[1u8; 64], None, None, &grid, (0.0, 0.0, 1.0), &p, |_, _| {});
1249 for o in [0, 5] {
1250 let err = run(AmpPeParams { wave_order: o, ..Default::default() }).unwrap_err();
1251 assert!(err.0.contains("AMP-PE wave_order") && err.0.contains("from 1 to 4"), "{err}");
1252 }
1253 let err = run(AmpPeParams { nlevel: 64, ..Default::default() }).unwrap_err();
1254 assert!(err.0.contains("AMP-PE nlevel = 64") && err.0.contains("<= 63"), "{err}");
1255 let err = run(AmpPeParams { nlevel: 63, ..Default::default() }).unwrap_err();
1256 assert!(err.0.contains("past the addressable size"), "{err}");
1257 }
1258}