1use crate::fft::Fft3dWorkspace;
37use crate::inversion::admm::prepare_fansi_spectral;
38use crate::utils::special::{erfc, erfcx};
39use crate::utils::wavelet::WaveletPlan;
40use crate::Grid;
41use num_complex::Complex64;
42#[cfg(feature = "parallel")]
43use rayon::prelude::*;
44
45const EPS: f64 = 2.220446049250313e-16;
46
47const PARAM_CHUNK: usize = 1 << 16;
50
51#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
53#[derive(Clone, Debug)]
54pub struct AmpPeParams {
55 pub wave_order: usize,
57 pub nlevel: usize,
59 pub wave_pec: f64,
61 pub simulated_te: f64,
63 pub max_linearization_ite: usize,
65 pub b0: f64,
67 pub gyro_ratio: f64,
69 pub damp_rate_sig: f64,
71 pub damp_rate_par: f64,
73 pub max_pe_spar_ite: usize,
75 pub max_pe_est_ite: usize,
77 pub cvg_thd: f64,
79 pub tikhonov_beta: f64,
81}
82
83impl Default for AmpPeParams {
84 fn default() -> Self {
85 Self {
86 wave_order: 1,
87 nlevel: 3,
88 wave_pec: 0.85,
89 simulated_te: 8e-3,
90 max_linearization_ite: 25,
91 b0: 3.0,
92 gyro_ratio: 42.58,
93 damp_rate_sig: 0.01,
94 damp_rate_par: 0.1,
95 max_pe_spar_ite: 5,
96 max_pe_est_ite: 5,
97 cvg_thd: 1e-6,
98 tikhonov_beta: 2e-2,
99 }
100 }
101}
102
103fn var(v: &[f64]) -> f64 {
105 let n = v.len();
106 if n < 2 {
107 return 0.0;
108 }
109 let mean = v.iter().sum::<f64>() / n as f64;
110 v.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / (n as f64 - 1.0)
111}
112
113fn var_complex(v: &[Complex64]) -> f64 {
116 let n = v.len();
117 if n < 2 {
118 return 0.0;
119 }
120 let mean = v.iter().sum::<Complex64>() / n as f64;
121 v.iter().map(|&x| (x - mean).norm_sqr()).sum::<f64>() / (n as f64 - 1.0)
122}
123
124struct DipoleOp {
126 fft_ws: Fft3dWorkspace,
127 kernel: Vec<f64>,
128 n: usize,
129 mask_idx: Vec<usize>,
130 buf: Vec<Complex64>,
131 full: Vec<f64>,
132 scatter: Vec<f64>,
136}
137
138impl DipoleOp {
139 fn new(grid: &Grid, bdir: (f64, f64, f64), mask_idx: Vec<usize>) -> Self {
140 let (fft_ws, kernel, _ee2) = prepare_fansi_spectral(grid, bdir);
141 let n = grid.n_total();
142 Self {
143 fft_ws,
144 kernel,
145 n,
146 mask_idx,
147 buf: vec![Complex64::new(0.0, 0.0); n],
148 full: vec![0.0; n],
149 scatter: vec![0.0; n],
150 }
151 }
152
153 fn apply(&mut self, x: &[f64], out: &mut [f64]) {
155 for (b, &xi) in self.buf.iter_mut().zip(x) {
156 *b = Complex64::new(xi, 0.0);
157 }
158 self.fft_ws.fft3d(&mut self.buf);
159 for (b, &k) in self.buf.iter_mut().zip(&self.kernel) {
160 *b *= k;
161 }
162 self.fft_ws.ifft3d(&mut self.buf);
163 for (o, b) in out.iter_mut().zip(&self.buf) {
164 *o = b.re;
165 }
166 }
167
168 fn field_masked(&mut self, x_full: &[f64]) -> Vec<f64> {
170 let mut full = std::mem::take(&mut self.full);
171 self.apply(x_full, &mut full);
172 let out = self.mask_idx.iter().map(|&j| full[j]).collect();
173 self.full = full;
174 out
175 }
176
177 fn adjoint_full(&mut self, y_masked: &[f64]) -> Vec<f64> {
179 let mut scattered = std::mem::take(&mut self.scatter);
180 for (k, &j) in self.mask_idx.iter().enumerate() {
181 scattered[j] = y_masked[k];
182 }
183 let mut out = vec![0.0; self.n];
184 self.apply(&scattered, &mut out);
185 self.scatter = scattered;
186 out
187 }
188}
189
190pub fn amp_pe(
203 local_field: &[f64],
204 mask: &[u8],
205 magnitude: Option<&[f64]>,
206 grid: &Grid,
207 bdir: (f64, f64, f64),
208 params: &AmpPeParams,
209 mut progress: impl FnMut(usize, usize),
210) -> Vec<f64> {
211 let orig = (grid.nx(), grid.ny(), grid.nz());
212 let pad_mult = 1usize << params.nlevel;
213 let pad = (
214 orig.0.div_ceil(pad_mult) * pad_mult,
215 orig.1.div_ceil(pad_mult) * pad_mult,
216 orig.2.div_ceil(pad_mult) * pad_mult,
217 );
218 let pgrid = Grid::new(
219 pad.0,
220 pad.1,
221 pad.2,
222 grid.voxel_size.0,
223 grid.voxel_size.1,
224 grid.voxel_size.2,
225 );
226
227 let field_p = pad_volume(local_field, orig, pad);
229 let mask_f: Vec<f64> = mask.iter().map(|&m| if m != 0 { 1.0 } else { 0.0 }).collect();
230 let mask_p = pad_volume(&mask_f, orig, pad);
231 let imag_p = match magnitude {
232 Some(mag) => pad_volume(&mag.iter().map(|&v| v.abs()).collect::<Vec<_>>(), orig, pad),
233 None => mask_p.clone(),
234 };
235 let have_mag = magnitude.is_some();
236
237 let n = pad.0 * pad.1 * pad.2;
238 let mask_idx: Vec<usize> = (0..n).filter(|&i| mask_p[i] > 0.5).collect();
239 let m = mask_idx.len();
240
241 let te = params.simulated_te;
242 let mut_cst = params.gyro_ratio * params.b0 * 2.0 * std::f64::consts::PI * te;
243
244 let phase_image: Vec<f64> = mask_idx.iter().map(|&j| field_p[j] * mut_cst).collect();
246
247 let weight_vect: Vec<f64> = if have_mag {
249 let wv: Vec<f64> = mask_idx.iter().map(|&j| imag_p[j]).collect();
250 let mean = (wv.iter().sum::<f64>() / m as f64).max(EPS);
251 wv.iter().map(|&v| v / mean).collect()
252 } else {
253 vec![1.0; m]
254 };
255
256 let mut dip = DipoleOp::new(&pgrid, bdir, mask_idx.clone());
257 let kernel_energy = dip.kernel.iter().map(|&d| d * d).sum::<f64>() / n as f64;
259
260 let plan = WaveletPlan::new(params.wave_order, pad, params.nlevel);
262 debug_assert_eq!(plan.coef_len(), n);
263
264 let field_masked: Vec<f64> = (0..n).map(|i| if mask_p[i] > 0.5 { field_p[i] } else { 0.0 }).collect();
268 let chi_l2 = chi_l2_seed(&field_masked, &mask_p, &dip.kernel, params.tikhonov_beta, pad);
269 let x_init_par: Vec<f64> = (0..n).map(|i| if mask_p[i] > 0.5 { chi_l2[i] } else { 0.0 }).collect();
270 let x_init_par_psi = plan.forward(&x_init_par);
271
272 let wave_mask = build_wave_mask(&plan, &imag_p, &mask_p, have_mag, params.wave_pec, n);
274
275 let mut st = GampState {
277 x_hat_meas: vec![0.0; n],
278 tau_x_meas: var(&x_init_par),
279 s_hat_meas: vec![Complex64::new(0.0, 0.0); m],
280 x_hat_psi: vec![0.0; n],
281 p_hat_psi: vec![0.0; n],
282 tau_x_hat_psi: var(&x_init_par_psi),
283 tau_p_psi: var(&x_init_par_psi), damp: params.damp_rate_sig,
285 damp_ceiling: params.damp_rate_sig,
286 };
287 let abs_psi: Vec<f64> = x_init_par_psi.iter().map(|v| v.abs()).collect();
288 let mut lambda = 1.0 / (var(&abs_psi) / 2.0).sqrt();
289
290 let cfg = GampCfg {
291 m,
292 n,
293 mut_cst,
294 kernel_energy,
295 max_pe_spar_ite: params.max_pe_spar_ite,
296 max_pe_est_ite: params.max_pe_est_ite,
297 cvg_thd: params.cvg_thd,
298 kappa: params.damp_rate_par,
299 };
302
303 let total = 2 * params.max_linearization_ite;
304 let mut x_curr = vec![0.0; n];
305 let mut tau_w_1 = 1e-12;
306
307 for it in 0..params.max_linearization_ite {
309 progress(it + 1, total);
310 let (a_x, der_1st, y_upd) = linearize(&mut dip, &x_curr, &phase_image, &weight_vect, mut_cst);
311 tau_w_1 = gamp_awgn(&mut dip, &plan, &cfg, &mut st, &mut lambda, tau_w_1, &der_1st, &weight_vect, &y_upd, &wave_mask);
312 x_curr = st.x_hat_meas.clone();
313 let _ = a_x;
314 }
315
316 let a_x_init = dip.field_masked(&x_curr);
318 let resid: Vec<Complex64> = (0..m)
319 .map(|j| {
320 let axm = a_x_init[j] * mut_cst;
321 weight_vect[j] * (Complex64::new(0.0, axm).exp() - Complex64::new(0.0, phase_image[j]).exp())
322 })
323 .collect();
324 let resid_abs: Vec<f64> = resid.iter().map(|c| c.norm()).collect();
325 let resid_std = (resid_abs.iter().map(|v| v * v).sum::<f64>() / m as f64).sqrt();
326 let outliers: Vec<Complex64> = resid
327 .iter()
328 .zip(&resid_abs)
329 .filter(|(_, &a)| a > 3.0 * resid_std)
330 .map(|(&c, _)| c)
331 .collect();
332 let gamma_est = outliers.len() as f64 / m as f64;
333 let psi_est = var_complex(&outliers);
334
335 let mut mix = MixState {
337 theta: 0.0,
338 phi: tau_w_1,
339 omega: 1.0,
340 gamma: gamma_est,
341 psi: psi_est,
342 };
343 for it in 0..params.max_linearization_ite {
344 progress(params.max_linearization_ite + it + 1, total);
345 let (a_x, der_1st, y_upd) = linearize(&mut dip, &x_curr, &phase_image, &weight_vect, mut_cst);
346 gamp_awgn_mix(&mut dip, &plan, &cfg, &mut st, &mut mix, &mut lambda, &der_1st, &weight_vect, &y_upd, &wave_mask);
347 x_curr = st.x_hat_meas.clone();
348 let _ = a_x;
349 }
350
351 for i in 0..n {
353 if mask_p[i] <= 0.5 {
354 x_curr[i] = 0.0;
355 }
356 }
357 crop_volume(&x_curr, pad, orig)
358}
359
360struct GampState {
362 x_hat_meas: Vec<f64>, tau_x_meas: f64,
364 s_hat_meas: Vec<Complex64>, x_hat_psi: Vec<f64>, p_hat_psi: Vec<f64>, tau_x_hat_psi: f64,
368 tau_p_psi: f64,
369 damp: f64,
375 damp_ceiling: f64,
380}
381
382struct GampSnapshot {
384 x_hat_meas: Vec<f64>,
385 tau_x_meas: f64,
386 s_hat_meas: Vec<Complex64>,
387 x_hat_psi: Vec<f64>,
388 p_hat_psi: Vec<f64>,
389 tau_x_hat_psi: f64,
390 tau_p_psi: f64,
391 lambda: f64,
392 mix: Option<MixState>,
393}
394
395impl GampState {
396 fn snapshot(&self, lambda: f64, mix: Option<&MixState>) -> GampSnapshot {
397 GampSnapshot {
398 x_hat_meas: self.x_hat_meas.clone(),
399 tau_x_meas: self.tau_x_meas,
400 s_hat_meas: self.s_hat_meas.clone(),
401 x_hat_psi: self.x_hat_psi.clone(),
402 p_hat_psi: self.p_hat_psi.clone(),
403 tau_x_hat_psi: self.tau_x_hat_psi,
404 tau_p_psi: self.tau_p_psi,
405 lambda,
406 mix: mix.map(|m| MixState { ..*m }),
407 }
408 }
409
410 fn restore(&mut self, snap: &GampSnapshot, lambda: &mut f64, mix: Option<&mut MixState>) {
411 self.x_hat_meas.copy_from_slice(&snap.x_hat_meas);
412 self.tau_x_meas = snap.tau_x_meas;
413 self.s_hat_meas.copy_from_slice(&snap.s_hat_meas);
414 self.x_hat_psi.copy_from_slice(&snap.x_hat_psi);
415 self.p_hat_psi.copy_from_slice(&snap.p_hat_psi);
416 self.tau_x_hat_psi = snap.tau_x_hat_psi;
417 self.tau_p_psi = snap.tau_p_psi;
418 *lambda = snap.lambda;
419 if let (Some(dst), Some(src)) = (mix, snap.mix.as_ref()) {
420 *dst = MixState { ..*src };
421 }
422 }
423}
424
425const DAMP_DIVERGE_FACTOR: f64 = 2.0;
446const DAMP_SHRINK: f64 = 0.5;
448const DAMP_GROW: f64 = 1.1;
450const DAMP_MIN: f64 = 1e-4;
452const MAX_BACKTRACKS: usize = 20;
454
455#[cfg(test)]
456thread_local! {
457 static GUARD_FIRINGS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
460}
461
462#[inline]
464fn note_guard_firing() {
465 #[cfg(test)]
466 GUARD_FIRINGS.with(|c| c.set(c.get() + 1));
467}
468
469struct GampCfg {
471 m: usize,
472 n: usize,
473 mut_cst: f64,
474 kernel_energy: f64,
475 max_pe_spar_ite: usize,
476 max_pe_est_ite: usize,
477 cvg_thd: f64,
478 kappa: f64,
479}
480
481struct MixState {
483 theta: f64,
484 phi: f64,
485 omega: f64,
486 gamma: f64,
487 psi: f64,
488}
489
490fn linearize(
494 dip: &mut DipoleOp,
495 x_curr: &[f64],
496 phase_image: &[f64],
497 weight_vect: &[f64],
498 mut_cst: f64,
499) -> (Vec<f64>, Vec<Complex64>, Vec<Complex64>) {
500 let field = dip.field_masked(x_curr);
501 let m = field.len();
502 let a_x: Vec<f64> = (0..m).map(|j| field[j] * mut_cst).collect();
503 let der_1st: Vec<Complex64> = a_x.iter().map(|&v| Complex64::new(0.0, 1.0) * Complex64::new(0.0, v).exp()).collect();
504 let y_upd: Vec<Complex64> = (0..m)
505 .map(|j| {
506 let e_phase = Complex64::new(0.0, phase_image[j]).exp();
507 let e_ax = Complex64::new(0.0, a_x[j]).exp();
508 weight_vect[j] * (der_1st[j] * a_x[j] + e_phase - e_ax)
509 })
510 .collect();
511 (a_x, der_1st, y_upd)
512}
513
514fn frob_qsm_sq(cfg: &GampCfg, der_1st: &[Complex64], weight_vect: &[f64]) -> f64 {
519 let sw: f64 = der_1st
520 .iter()
521 .zip(weight_vect)
522 .map(|(&d, &w)| (d * w).norm_sqr())
523 .sum();
524 cfg.mut_cst * cfg.mut_cst * cfg.kernel_energy * sw
525}
526
527fn a_qsm_mult(dip: &mut DipoleOp, x_img: &[f64], der_1st: &[Complex64], weight_vect: &[f64], mut_cst: f64) -> Vec<Complex64> {
529 let field = dip.field_masked(x_img);
530 maybe_par_iter!(field)
531 .zip(maybe_par_iter!(der_1st))
532 .zip(maybe_par_iter!(weight_vect))
533 .map(|((&fj, &dj), &wj)| dj * wj * (fj * mut_cst))
534 .collect()
535}
536
537fn a_qsm_multtr_real(dip: &mut DipoleOp, s: &[Complex64], der_1st: &[Complex64], weight_vect: &[f64], mut_cst: f64) -> Vec<f64> {
539 let re: Vec<f64> = maybe_par_iter!(s)
541 .zip(maybe_par_iter!(der_1st))
542 .zip(maybe_par_iter!(weight_vect))
543 .map(|((&sj, &dj), &wj)| (sj * (dj * wj).conj()).re)
544 .collect();
545 let mut adj = dip.adjoint_full(&re);
546 maybe_par_iter_mut!(adj).for_each(|v| *v *= mut_cst);
547 adj
548}
549
550#[allow(clippy::too_many_arguments)]
552fn gamp_awgn(
553 dip: &mut DipoleOp,
554 plan: &WaveletPlan,
555 cfg: &GampCfg,
556 st: &mut GampState,
557 lambda: &mut f64,
558 tau_w_1_in: f64,
559 der_1st: &[Complex64],
560 weight_vect: &[f64],
561 y: &[Complex64],
562 wave_mask: &[bool],
563) -> f64 {
564 let frob_sq = frob_qsm_sq(cfg, der_1st, weight_vect);
565 let mut tau_w_1 = tau_w_1_in;
566 let (m, n) = (cfg.m, cfg.n);
567
568 let mut best_cost = f64::INFINITY;
569 let mut good: Option<GampSnapshot> = None;
570 let mut good_tau_w = tau_w_1;
571 let mut accepted = 0usize;
572 let mut backtracks = 0usize;
573
574 while accepted < cfg.max_pe_spar_ite && backtracks <= MAX_BACKTRACKS {
575 let tau_p_meas_1 = frob_sq / m as f64 * st.tau_x_meas;
577 let axm = a_qsm_mult(dip, &st.x_hat_meas, der_1st, weight_vect, cfg.mut_cst);
579 let p_hat_meas_1: Vec<Complex64> = (0..m).map(|j| axm[j] - tau_p_meas_1 * st.s_hat_meas[j]).collect();
580
581 let mse: f64 = maybe_par_chunks!(p_hat_meas_1.as_slice(), PARAM_CHUNK)
584 .zip(maybe_par_chunks!(y, PARAM_CHUNK))
585 .map(|(pc, yc)| pc.iter().zip(yc).map(|(p, yy)| (p - yy).norm_sqr()).sum::<f64>())
586 .collect::<Vec<f64>>()
587 .iter()
588 .sum::<f64>()
589 / m as f64;
590
591 let fit = data_fit(&axm, y, m);
594 if !fit.is_finite() || fit > best_cost * DAMP_DIVERGE_FACTOR {
595 note_guard_firing();
596 backtracks += 1;
597 if let Some(g) = good.as_ref() {
598 st.restore(g, lambda, None);
599 tau_w_1 = good_tau_w;
600 }
601 st.damp = (st.damp * DAMP_SHRINK).max(DAMP_MIN);
602 st.damp_ceiling = st.damp;
603 if st.damp <= DAMP_MIN {
604 break; }
606 continue;
607 }
608 if fit < best_cost {
609 best_cost = fit;
610 good = Some(st.snapshot(*lambda, None));
611 good_tau_w = tau_w_1;
612 }
613 st.damp = (st.damp * DAMP_GROW).min(st.damp_ceiling);
614
615 let tau_w_new = mse + tau_p_meas_1;
616 for _ in 0..cfg.max_pe_est_ite {
617 tau_w_1 += cfg.kappa * (tau_w_new - tau_w_1);
618 }
619
620 let tau_s_meas_1 = 1.0 / (tau_w_1 + tau_p_meas_1);
621 for j in 0..m {
622 st.s_hat_meas[j] = (y[j] - p_hat_meas_1[j]) * tau_s_meas_1;
623 }
624
625 let tau_r_meas_1 = 1.0 / (frob_sq / n as f64 * tau_s_meas_1);
627 let mtr = a_qsm_multtr_real(dip, &st.s_hat_meas, der_1st, weight_vect, cfg.mut_cst);
628 let r_hat_meas_1: Vec<f64> = maybe_par_iter!(st.x_hat_meas)
629 .zip(maybe_par_iter!(mtr))
630 .map(|(&x, &mv)| x + tau_r_meas_1 * mv)
631 .collect();
632
633 let cvg = wavelet_block(plan, cfg, st, lambda, tau_r_meas_1, &r_hat_meas_1, wave_mask);
634 accepted += 1;
635 if cvg < cfg.cvg_thd {
636 break;
637 }
638 }
639 tau_w_1
640}
641
642fn data_fit(axm: &[Complex64], y: &[Complex64], m: usize) -> f64 {
644 maybe_par_chunks!(axm, PARAM_CHUNK)
645 .zip(maybe_par_chunks!(y, PARAM_CHUNK))
646 .map(|(ac, yc)| ac.iter().zip(yc).map(|(a, yy)| (a - yy).norm_sqr()).sum::<f64>())
647 .collect::<Vec<f64>>()
648 .iter()
649 .sum::<f64>()
650 / m as f64
651}
652
653#[allow(clippy::too_many_arguments)]
655fn gamp_awgn_mix(
656 dip: &mut DipoleOp,
657 plan: &WaveletPlan,
658 cfg: &GampCfg,
659 st: &mut GampState,
660 mix: &mut MixState,
661 lambda: &mut f64,
662 der_1st: &[Complex64],
663 weight_vect: &[f64],
664 y: &[Complex64],
665 wave_mask: &[bool],
666) {
667 let frob_sq = frob_qsm_sq(cfg, der_1st, weight_vect);
668 let (m, n) = (cfg.m, cfg.n);
669
670 let mut best_cost = f64::INFINITY;
671 let mut good: Option<GampSnapshot> = None;
672 let mut accepted = 0usize;
673 let mut backtracks = 0usize;
674
675 while accepted < cfg.max_pe_spar_ite && backtracks <= MAX_BACKTRACKS {
676 let tau_p_meas_1 = frob_sq / m as f64 * st.tau_x_meas;
677 let axm = a_qsm_mult(dip, &st.x_hat_meas, der_1st, weight_vect, cfg.mut_cst);
678 let p_hat_meas_1: Vec<Complex64> = (0..m).map(|j| axm[j] - tau_p_meas_1 * st.s_hat_meas[j]).collect();
679 let r_noise: Vec<Complex64> = (0..m).map(|j| y[j] - p_hat_meas_1[j]).collect();
680
681 let fit = data_fit(&axm, y, m);
683 if !fit.is_finite() || fit > best_cost * DAMP_DIVERGE_FACTOR {
684 note_guard_firing();
685 backtracks += 1;
686 if let Some(g) = good.as_ref() {
687 st.restore(g, lambda, Some(mix));
688 }
689 st.damp = (st.damp * DAMP_SHRINK).max(DAMP_MIN);
690 st.damp_ceiling = st.damp;
691 if st.damp <= DAMP_MIN {
692 break;
693 }
694 continue;
695 }
696 if fit < best_cost {
697 best_cost = fit;
698 good = Some(st.snapshot(*lambda, Some(mix)));
699 }
700 st.damp = (st.damp * DAMP_GROW).min(st.damp_ceiling);
701
702 for _ in 0..cfg.max_pe_est_ite {
703 mix_output_parameter_est(&r_noise, tau_p_meas_1, mix, cfg.kappa);
704 }
705 let (noise_update, tau_z) = mix_output_function(&r_noise, tau_p_meas_1, mix);
706 let tau_s_meas_1 = 1.0 / tau_p_meas_1 * (1.0 - tau_z / tau_p_meas_1);
708 for j in 0..m {
709 let z_hat = y[j] - noise_update[j];
710 st.s_hat_meas[j] = 1.0 / tau_p_meas_1 * (z_hat - p_hat_meas_1[j]);
711 }
712
713 let tau_r_meas_1 = 1.0 / (frob_sq / n as f64 * tau_s_meas_1);
714 let mtr = a_qsm_multtr_real(dip, &st.s_hat_meas, der_1st, weight_vect, cfg.mut_cst);
715 let r_hat_meas_1: Vec<f64> = maybe_par_iter!(st.x_hat_meas)
716 .zip(maybe_par_iter!(mtr))
717 .map(|(&x, &mv)| x + tau_r_meas_1 * mv)
718 .collect();
719
720 let cvg = wavelet_block(plan, cfg, st, lambda, tau_r_meas_1, &r_hat_meas_1, wave_mask);
721 accepted += 1;
722 if cvg < cfg.cvg_thd {
723 break;
724 }
725 }
726}
727
728fn wavelet_block(
733 plan: &WaveletPlan,
734 cfg: &GampCfg,
735 st: &mut GampState,
736 lambda: &mut f64,
737 tau_r_meas_1: f64,
738 r_hat_meas_1: &[f64],
739 wave_mask: &[bool],
740) -> f64 {
741 let damp = st.damp;
744 let tau_s_psi = 1.0 / (tau_r_meas_1 + st.tau_p_psi);
745 let s_hat_psi: Vec<f64> = maybe_par_iter!(r_hat_meas_1)
746 .zip(maybe_par_iter!(st.p_hat_psi))
747 .map(|(&r, &p)| (r - p) * tau_s_psi)
748 .collect();
749
750 let tau_r_psi = 1.0 / tau_s_psi;
752 let analysis = plan.forward(&s_hat_psi); let r_hat_psi: Vec<f64> = maybe_par_iter!(st.x_hat_psi)
754 .zip(maybe_par_iter!(analysis))
755 .map(|(&x, &a)| x + tau_r_psi * a)
756 .collect();
757
758 let abs_r: Vec<f64> = maybe_par_iter!(r_hat_psi).map(|v| v.abs()).collect();
759 for _ in 0..cfg.max_pe_est_ite {
760 *lambda = input_parameter_est(&abs_r, tau_r_psi, *lambda, cfg.kappa);
761 }
762
763 let (x_hat_psi_new, tau_x_hat_psi) = input_function(&r_hat_psi, tau_r_psi, *lambda, wave_mask);
764 st.x_hat_psi = x_hat_psi_new;
765 st.tau_x_hat_psi = tau_x_hat_psi;
766
767 st.tau_p_psi = tau_x_hat_psi; let synth = plan.inverse(&st.x_hat_psi); let tau_p_psi = st.tau_p_psi;
770 maybe_par_iter_mut!(st.p_hat_psi)
771 .zip(maybe_par_iter!(synth))
772 .zip(maybe_par_iter!(s_hat_psi))
773 .for_each(|((p, &sy), &sh)| *p = sy - tau_p_psi * sh);
774
775 let tau_x_meas_pre = st.tau_x_meas;
776 let tau_new = (st.tau_p_psi * tau_r_meas_1) / (st.tau_p_psi + tau_r_meas_1);
777 st.tau_x_meas = tau_x_meas_pre + damp * (tau_new - tau_x_meas_pre);
778
779 let denom = st.tau_p_psi + tau_r_meas_1;
780 let mut change_sq = 0.0;
781 let mut norm_sq = 0.0;
782 for ((xh, &r), &p) in st.x_hat_meas.iter_mut().zip(r_hat_meas_1).zip(&st.p_hat_psi) {
783 let x_new = (st.tau_p_psi * r + tau_r_meas_1 * p) / denom;
784 let updated = *xh + damp * (x_new - *xh);
785 let d = updated - *xh;
786 change_sq += d * d;
787 norm_sq += updated * updated;
788 *xh = updated;
789 }
790 change_sq.sqrt() / norm_sq.sqrt().max(EPS)
791}
792
793fn input_function(r_hat: &[f64], tau_r: f64, lambda: f64, wave_mask: &[bool]) -> (Vec<f64>, f64) {
796 let thresh = lambda * tau_r;
797 let mut x0 = vec![0.0; r_hat.len()];
798 let mut nnz = 0usize;
799 for i in 0..r_hat.len() {
800 let v = if wave_mask[i] {
801 r_hat[i]
802 } else {
803 let a = r_hat[i].abs() - thresh;
804 if a > 0.0 {
805 a * r_hat[i].signum()
806 } else {
807 0.0
808 }
809 };
810 if v != 0.0 {
811 nnz += 1;
812 }
813 x0[i] = v;
814 }
815 let tau_x = tau_r * nnz as f64 / r_hat.len() as f64;
816 (x0, tau_x)
817}
818
819fn input_parameter_est(r_hat: &[f64], tau_r: f64, lambda: f64, kappa: f64) -> f64 {
821 let s = (0.5 / tau_r).sqrt();
822 let partials: Vec<(f64, f64)> = maybe_par_chunks!(r_hat, PARAM_CHUNK)
825 .map(|chunk| {
826 let mut s1 = 0.0;
827 let mut s2 = 0.0;
828 for &r in chunk {
829 let arg = tau_r * lambda - r;
830 let b0 = lambda / 2.0 * erfc(s * arg);
832 let der = (2.0 * tau_r / std::f64::consts::PI).sqrt() / erfcx(s * arg) + r - tau_r * lambda;
833 let fst = 1.0 / lambda - der;
834 let scd = -1.0 / (lambda * lambda) + (tau_r + (r - tau_r * lambda) * der) - der * der;
835 let w = b0 / (b0 + EPS);
836 s1 += w * fst;
837 s2 += w * scd;
838 }
839 (s1, s2)
840 })
841 .collect();
842 let (sum1, sum2) = partials.iter().fold((0.0, 0.0), |a, p| (a.0 + p.0, a.1 + p.1));
843 let lambda_new = if sum2 < 0.0 {
844 lambda - sum1 / sum2
845 } else if sum1 > 0.0 {
846 lambda * 1.1
847 } else {
848 lambda * 0.9
849 };
850 let lambda_new = lambda_new.max(1e-12);
851 lambda + kappa * (lambda_new - lambda)
852}
853
854fn mix_output_function(r_hat: &[Complex64], tau_r: f64, mix: &MixState) -> (Vec<Complex64>, f64) {
857 let (omega, theta, phi, gamma, psi) = (mix.omega, mix.theta, mix.phi, mix.gamma, mix.psi);
858 let mut x_hat = vec![Complex64::new(0.0, 0.0); r_hat.len()];
859 let partials: Vec<f64> = maybe_par_chunks!(r_hat, PARAM_CHUNK)
861 .zip(maybe_par_chunks_mut!(x_hat, PARAM_CHUNK))
862 .map(|(rc, xc)| {
863 let mut tau_sum = 0.0;
864 for (&r, xh_out) in rc.iter().zip(xc.iter_mut()) {
865 let diff = theta - r.norm(); let e1 = (-(diff / (phi + tau_r).sqrt()).powi(2)).exp();
868 let block1 = (1.0 - gamma) * omega * (tau_r / (phi + tau_r)) * e1;
869 let mean1 = (theta * tau_r + r * phi) / (phi + tau_r);
870 let block_nmr1 = block1 * mean1;
871 let e2 = (-(r.norm() / (psi + tau_r).sqrt()).powi(2)).exp();
873 let block2 = gamma * (tau_r / (psi + tau_r)) * e2;
874 let mean2 = r * psi / (psi + tau_r);
875 let block_nmr2 = block2 * mean2;
876
877 let nmr = block_nmr1 + block_nmr2;
878 let dnm = block1 + block2;
879 let xh = if dnm == 0.0 { r } else { nmr / dnm };
880 *xh_out = xh;
881
882 let nmr_sq1 = block1 * (phi * tau_r / (phi + tau_r) + mean1.norm_sqr());
883 let nmr_sq2 = block2 * (psi * tau_r / (psi + tau_r) + mean2.norm_sqr());
884 let nmr_sq = nmr_sq1 + nmr_sq2;
885 let tau_seq = if dnm == 0.0 { 0.0 } else { nmr_sq / dnm - xh.norm_sqr() };
886 tau_sum += tau_seq;
887 }
888 tau_sum
889 })
890 .collect();
891 let tau_sum: f64 = partials.iter().sum();
892 let tau_x = (tau_sum / r_hat.len() as f64).max(1e-12);
893 (x_hat, tau_x)
894}
895
896fn mix_output_parameter_est(r_hat: &[Complex64], tau_r: f64, mix: &mut MixState, kappa: f64) {
898 let (omega, theta, phi, gamma, psi) = (mix.omega, mix.theta, mix.phi, mix.gamma, mix.psi);
899 let partials: Vec<(f64, f64, f64, f64)> = maybe_par_chunks!(r_hat, PARAM_CHUNK)
901 .map(|chunk| {
902 let (mut sum_b1, mut sum_b2, mut sum_phi_num, mut sum_psi_num) = (0.0, 0.0, 0.0, 0.0);
903 for &r in chunk {
904 let d1 = r.norm() - theta; let t1 = (1.0 - gamma) * omega / (tau_r + phi) * (-(d1 / (tau_r + phi).sqrt()).powi(2)).exp();
906 let t2 = gamma / (tau_r + psi) * (-(r.norm() / (tau_r + psi).sqrt()).powi(2)).exp();
907 let sum = t1 + t2;
908 let (b1, b2) = if sum == 0.0 { (0.0, 1.0) } else { (t1 / sum, t2 / sum) };
909 sum_b1 += b1;
910 sum_b2 += b2;
911 sum_phi_num += b1 * d1 * d1;
912 sum_psi_num += b2 * r.norm_sqr();
913 }
914 (sum_b1, sum_b2, sum_phi_num, sum_psi_num)
915 })
916 .collect();
917 let (sum_b1, sum_b2, sum_phi_num, sum_psi_num) = partials
918 .iter()
919 .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));
920 let omega_new = 1.0;
922 mix.omega = omega + kappa * (omega_new - omega);
923 mix.theta = 0.0;
924 let mut phi_new = if sum_b1 != 0.0 { sum_phi_num / sum_b1 - tau_r } else { phi };
926 if !phi_new.is_finite() || phi_new < 0.0 {
927 phi_new = phi;
928 }
929 mix.phi = phi + kappa * (phi_new - phi);
930 let mut psi_new = if sum_b2 != 0.0 { sum_psi_num / sum_b2 - tau_r } else { psi };
932 if psi_new < 0.0 {
933 psi_new = psi;
934 }
935 mix.psi = psi + kappa * (psi_new - psi);
936}
937
938fn chi_l2_seed(phase: &[f64], mask: &[f64], kernel: &[f64], beta: f64, dims: (usize, usize, usize)) -> Vec<f64> {
941 let (nx, ny, nz) = dims;
942 let n = nx * ny * nz;
943 let tau = std::f64::consts::TAU;
944 let mut e2 = vec![0.0f64; n];
946 for k in 0..nz {
947 for j in 0..ny {
948 for i in 0..nx {
949 let idx = i + j * nx + k * nx * ny;
950 let ex = Complex64::new(1.0, 0.0) - Complex64::new(0.0, tau * i as f64 / nx as f64).exp();
951 let ey = Complex64::new(1.0, 0.0) - Complex64::new(0.0, tau * j as f64 / ny as f64).exp();
952 let ez = Complex64::new(1.0, 0.0) - Complex64::new(0.0, tau * k as f64 / nz as f64).exp();
953 e2[idx] = ex.norm_sqr() + ey.norm_sqr() + ez.norm_sqr();
954 }
955 }
956 }
957 let mut ws = Fft3dWorkspace::new(nx, ny, nz);
958 let mut buf: Vec<Complex64> = phase.iter().map(|&p| Complex64::new(p, 0.0)).collect();
959 ws.fft3d(&mut buf);
960 for i in 0..n {
961 let k2 = kernel[i] * kernel[i];
962 buf[i] = kernel[i] * buf[i] / (k2 + beta * e2[i] + EPS);
963 }
964 ws.ifft3d(&mut buf);
965 (0..n).map(|i| buf[i].re * mask[i]).collect()
966}
967
968fn build_wave_mask(plan: &WaveletPlan, imag: &[f64], mask: &[f64], have_mag: bool, wave_pec: f64, n: usize) -> Vec<bool> {
971 if !have_mag {
972 return vec![true; n];
973 }
974 let imag_masked: Vec<f64> = (0..n).map(|i| if mask[i] > 0.5 { imag[i] } else { 0.0 }).collect();
975 let magwav = plan.forward(&imag_masked);
976 let mut abs_sorted: Vec<f64> = magwav.iter().map(|v| v.abs()).collect();
977 abs_sorted.sort_by(|a, b| b.partial_cmp(a).unwrap()); let total: f64 = abs_sorted.iter().sum();
979 let mut cum = 0.0;
981 let mut count = 0usize;
982 for &v in &abs_sorted {
983 cum += v;
984 if cum / total <= wave_pec {
985 count += 1;
986 } else {
987 break;
988 }
989 }
990 let thd = abs_sorted[count.max(1) - 1];
991 magwav.iter().map(|v| v.abs() > thd).collect()
992}
993
994fn pad_volume(v: &[f64], orig: (usize, usize, usize), pad: (usize, usize, usize)) -> Vec<f64> {
995 if orig == pad {
996 return v.to_vec();
997 }
998 let (ox, oy, oz) = orig;
999 let (px, py, pz) = pad;
1000 let mut out = vec![0.0; px * py * pz];
1001 for k in 0..oz {
1002 for j in 0..oy {
1003 for i in 0..ox {
1004 out[i + j * px + k * px * py] = v[i + j * ox + k * ox * oy];
1005 }
1006 }
1007 }
1008 out
1009}
1010
1011fn crop_volume(v: &[f64], pad: (usize, usize, usize), orig: (usize, usize, usize)) -> Vec<f64> {
1012 if orig == pad {
1013 return v.to_vec();
1014 }
1015 let (ox, oy, oz) = orig;
1016 let (px, py, _pz) = pad;
1017 let mut out = vec![0.0; ox * oy * oz];
1018 for k in 0..oz {
1019 for j in 0..oy {
1020 for i in 0..ox {
1021 out[i + j * ox + k * ox * oy] = v[i + j * px + k * px * py];
1022 }
1023 }
1024 }
1025 out
1026}
1027
1028#[cfg(test)]
1029mod tests {
1030 use super::*;
1031
1032 #[test]
1033 fn amp_pe_masks_output() {
1034 let n = 16;
1036 let field: Vec<f64> = (0..n * n * n)
1037 .map(|i| {
1038 let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
1039 0.02 * (((x + 2 * y + 3 * z) as f64) * 0.3).sin()
1040 })
1041 .collect();
1042 let mut mask = vec![0u8; n * n * n];
1043 for z in 4..12 {
1045 for y in 4..12 {
1046 for x in 4..12 {
1047 mask[x + y * n + z * n * n] = 1;
1048 }
1049 }
1050 }
1051 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1052 let params = AmpPeParams { max_linearization_ite: 3, ..Default::default() };
1053 let chi = amp_pe(&field, &mask, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
1054 assert_eq!(chi.len(), n * n * n);
1055 for (i, &v) in chi.iter().enumerate() {
1056 assert!(v.is_finite(), "chi[{i}] not finite: {v}");
1057 if mask[i] == 0 {
1058 assert_eq!(v, 0.0, "chi must be zero outside mask at {i}");
1059 }
1060 }
1061 }
1062
1063 #[test]
1064 fn amp_pe_ignores_out_of_mask_field() {
1065 let n = 16;
1069 let base: Vec<f64> = (0..n * n * n)
1070 .map(|i| {
1071 let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
1072 0.02 * (((x + 2 * y + 3 * z) as f64) * 0.3).sin()
1073 })
1074 .collect();
1075 let mut mask = vec![0u8; n * n * n];
1076 for z in 4..12 {
1077 for y in 4..12 {
1078 for x in 4..12 {
1079 mask[x + y * n + z * n * n] = 1;
1080 }
1081 }
1082 }
1083 let mut with_bg = base.clone();
1085 for i in 0..with_bg.len() {
1086 if mask[i] == 0 {
1087 with_bg[i] += 0.5 * ((i % 11) as f64 - 5.0);
1088 }
1089 }
1090 let masked: Vec<f64> = (0..base.len()).map(|i| if mask[i] != 0 { base[i] } else { 0.0 }).collect();
1092
1093 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1094 let params = AmpPeParams { max_linearization_ite: 4, ..Default::default() };
1095 let a = amp_pe(&with_bg, &mask, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
1096 let b = amp_pe(&masked, &mask, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
1097 let maxerr = a.iter().zip(&b).map(|(x, y)| (x - y).abs()).fold(0.0, f64::max);
1098 assert!(maxerr < 1e-9, "out-of-mask field leaked into result: maxerr={maxerr}");
1099 }
1100
1101 #[test]
1110 fn damping_guard_is_inert_on_well_conditioned_data() {
1111 let n = 16;
1112 let field: Vec<f64> = (0..n * n * n)
1113 .map(|i| {
1114 let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
1115 0.02 * (((x + 2 * y + 3 * z) as f64) * 0.3).sin()
1116 })
1117 .collect();
1118 let mag: Vec<f64> = (0..n * n * n)
1121 .map(|i| {
1122 let (x, y) = ((i % n) as f64, ((i / n) % n) as f64);
1123 100.0 + 30.0 * (x * 0.4).cos() + 20.0 * (y * 0.25).sin()
1124 })
1125 .collect();
1126 let mut mask = vec![0u8; n * n * n];
1127 for z in 4..12 {
1128 for y in 4..12 {
1129 for x in 4..12 {
1130 mask[x + y * n + z * n * n] = 1;
1131 }
1132 }
1133 }
1134 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1135 let params = AmpPeParams { max_linearization_ite: 4, ..Default::default() };
1136
1137 GUARD_FIRINGS.with(|c| c.set(0));
1138 let chi = amp_pe(&field, &mask, Some(&mag), &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
1139 let fired = GUARD_FIRINGS.with(|c| c.get());
1140
1141 assert_eq!(
1142 fired, 0,
1143 "divergence guard fired {fired}x on well-conditioned data — the trigger has been \
1144 loosened, and every AMP-PE result will have silently changed"
1145 );
1146 assert!(chi.iter().all(|v| v.is_finite()));
1147 }
1148
1149 #[test]
1150 fn amp_pe_finite_on_ramp() {
1151 let n = 16;
1152 let field: Vec<f64> = (0..n * n * n).map(|i| ((i % 7) as f64 - 3.0) * 0.01).collect();
1153 let mask = vec![1u8; n * n * n];
1154 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
1155 let params = AmpPeParams { max_linearization_ite: 3, ..Default::default() };
1156 let chi = amp_pe(&field, &mask, None, &grid, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
1157 for &v in &chi {
1158 assert!(v.is_finite());
1159 }
1160 }
1161}