Skip to main content

qsm_core/inversion/
tfi.rs

1//! TFI (preconditioned Total Field Inversion)
2//!
3//! Single-step QSM: inverts the total field, before background removal, over the whole field of
4//! view. Background sources outside the mask are part of the solution, made cheap to reach by a
5//! preconditioner `P`, and the susceptibility is `χ = P·y`:
6//!
7//! min_y ||wG ∇(P y)||₁ + 2λ ||m (D P y − f)||²₂
8//!
9//! # What follows which source
10//!
11//! Cornell's own TFI script (`TFI_L1.m`) is not public. The 2024.11.26 MEDI toolbox has the TFI
12//! solver inside `MEDI_L1.m`: when `RDF.mat` holds a preconditioner `P` (`parse_QSM_input.m`),
13//! `MEDI_L1` solves for `y` with `χ = P·y`. This port follows that code line for line:
14//!
15//! - **2024 `MEDI_L1.m` and its helpers** (`fgrad.m`, `bdiv.m`, `gradient_mask.m`,
16//!   `dataterm_mask.m`, `cgsolve.m`, `dipole_kernel.m`): the `'linear'` data term (and the
17//!   toolbox's default exponential one, as an option), the data weight `m` from `N_std`, the edge mask `wG` and then `wG.*Mask`
18//!   (no regularisation outside the mask), the lagged-diffusivity weight
19//!   `1/sqrt(|wG ∇(P y)|² + 1e-6·P²)` (epsilon scaled by `P²`), the operator
20//!   `PH(reg(P dx)) + 2λ·PH(Dconv(|w|² Dconv(P dx)))`, the right-hand side, the Gauss-Newton
21//!   stop `||dy||/||y||`, MERIT, the output `P y` in ppm masked by `Mask`, and the defaults of
22//!   `parse_QSM_input.m` (λ = 1000, MERIT on, 10 Gauss-Newton and 100 CG iterations, tolerances
23//!   0.1 and 0.01, `percentage` 0.9).
24//! - **Liu et al. 2017**, for what the toolbox does not settle for TFI: `P = 1` inside the mask
25//!   and `P_B = 30` outside (eq. 3; 30 is the paper's in-vivo value); the input is the total
26//!   field over the whole field of view, so there is no SMV step (an SMV high-pass would remove
27//!   the background TFI is meant to fit); and the data term is the linear one of eq. 5, which is
28//!   `MEDI_L1`'s `'linear'` option rather than its exponential default. (With the exponential
29//!   term, `MEDI_L1` itself fails on in-vivo total fields in radians: the field wraps inside
30//!   `exp(i·)`. Set `linear: false` to run the toolbox default.)
31//!
32//! Not ported: the R2*-based preconditioner (eq. 4), zero padding and the CSF zero reference.
33//!
34//! # Parity
35//! Against TFI as the verified QSM-CI `tfi-cornell` container runs it (MEDI 2024.11.26 `MEDI_L1.m`
36//! with a preconditioner, identical to licensed MATLAB R2023b), astewartau/QSM.rs#407. Two
37//! departures are deliberate, each with the original's behaviour as an option:
38//! - **Dipole kernel on odd grids** ([`TfiKernel`]). `dipole_kernel.m` builds its k grid as
39//!   `-N/2:N/2-1`, half a frequency step off the DFT grid on an odd axis, so it is not the dipole
40//!   kernel there. On QSM-CI's simulated head (164×205×205) that alone made the port's χ 0.77× the
41//!   original's (42 % NRMSE); with [`TfiKernel::MediOddGrid`] 2.5 %. The DFT-grid kernel fits the
42//!   true χ better (demeaned, in the mask: NRMSE 62.1 % against 71.4 %, r 0.785 against 0.718), so it
43//!   stays the default. On even grids the two are the same.
44//! - **Precision** ([`TfiPrecision`]). `MEDI_L1.m` runs in single precision (`tempn =
45//!   single(N_std)` makes the data weight single, and MATLAB's mixed arithmetic carries that into
46//!   everything). On the fixture's small odd phantom (`tests/tfi_cornell_parity.rs`) the port with
47//!   both options is 2.9e-6 relative after one Gauss-Newton step and 4.7e-4 with all defaults; in
48//!   double precision 4.7e-3. The container itself differs by 3.0e-4 there on another CPU.
49//!
50//! Beyond that the original is not reproducible at full size: its 100-iteration CG solves at
51//! `cg_tol` 0.01 in single precision amplify rounding. In the same container, 1e-7 relative noise
52//! on the simulated head's total field moves its χ by 1.6 % NRMSE, and the container on another
53//! CPU (QSM-CI's runner against this VM) differs from itself by 2.8 % on the KKI travelling head.
54//! On the reference's own inputs the port with both options is 3.6 % (sim) and 1.26 % (KKI) from
55//! it; with the DFT-grid kernel in double precision, 42 % and 1.25 %.
56//!
57//! # Reference pipeline
58//! From Liu et al. 2017 (Methods, in-vivo experiments); not stated above:
59//! - Brain mask (the `M` where `P = 1`): BET. Field map: nonlinear (complex) field estimation
60//!   followed by graph-cut phase unwrapping. No background removal or high-pass before TFI.
61//! - In vivo the paper uses the R2*-based preconditioner of eq. 4, `P = 1` in `M ∩ {R2* < 30 s⁻¹}`
62//!   and `P_B` elsewhere, with `P_B` = 30 (10 for the phantom). Only eq. 3 is ported here.
63//! - Data: 3 T, 6 echoes, TE₁ 3.5 ms, ΔTE 3.6 ms, 1×1×3 mm³, monopolar readout.
64//! - The paper's solver settings differ from `MEDI_L1`'s defaults, which this port uses: λ = 1e-3
65//!   on the L1 term (not the data term), Gauss-Newton stop `||dy||/||y|| < 0.01`, up to 300 CG
66//!   iterations. CSF is the reference for reported values.
67//!
68//! # Units
69//! Like the other inversions, `tfi` takes the field in ppm and returns ppm. `MEDI_L1` works on the
70//! field in radians accumulated over the echo spacing (`delta_TE`) at `CF = γ·B0`; the field is
71//! converted with `2π·CF·delta_TE·1e-6` first and back at the end, as `MEDI_L1.m` does. The
72//! result depends on this scale, so `b0` and `echo_times` are scan parameters
73//! that the pipeline fills from metadata, and [`TfiParams::te`] picks the echo time the
74//! conversion uses (by default the reference's choice; see there).
75//!
76//! # Magnitude and data weight
77//! As for [`medi`](super::medi), the echo magnitudes ([`EchoMagnitudes`]) are required: the edge
78//! mask uses their root sum of squares (`iMag = sqrt(sum(abs(iField).^2,4))`) and the data weight
79//! `m = Mask./N_std` (mean 1 in the mask) uses `Fit_ppm_complex.m`'s noise map
80//! ([`fit_ppm_complex_noise`](crate::utils::fit_ppm_complex_noise)), as MEDI's pipeline makes
81//! them, so TFI always weights the data as the reference does.
82//!
83//! Reference:
84//! Liu, Z., Kee, Y., Zhou, D., Wang, Y., Spincemaille, P. (2017). "Preconditioned total field
85//! inversion (TFI) method for quantitative susceptibility mapping." Magnetic Resonance in
86//! Medicine, 78(1):303-315. https://doi.org/10.1002/mrm.26331
87//!
88//! Reference implementation: MEDI toolbox 2024.11.26 (Weill Cornell), `functions/MEDI_L1.m`.
89//!
90//! # Pipeline rules
91//!
92//! The facts above that a configuration can depart from are published as rules in
93//! [`crate::pipeline::rules::RULES`] (each with its strength, basis, reason and source): `tfi.mask-bet`, `tfi.te-spacing`.
94
95use num_complex::Complex64;
96use crate::fft::Fft3dWorkspace;
97use crate::kernels::dipole::{dipole_kernel, dipole_kernel_medi};
98use crate::{EchoTime, Grid};
99
100use super::medi::{
101    bounding_box, cgsolve, dataterm_mask, dconv, dconv_weighted, fgrad, gradient_mask, par_fill,
102    par_update, reg, CgBuffers, EchoMagnitudes,
103};
104
105/// Which dipole kernel [`tfi`] uses: they differ only on grids with an odd number of voxels
106/// along some axis.
107#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
109pub enum TfiKernel {
110    /// `1/3 − (k·B0)²/|k|²` sampled on the DFT frequency grid ([`dipole_kernel`]), zero at DC,
111    /// on every grid. The default.
112    #[default]
113    Dft,
114    /// `dipole_kernel.m` exactly as MATLAB builds it ([`dipole_kernel_medi`]): on an odd axis its
115    /// `-N/2:N/2-1` grid samples half a frequency step off the DFT grid, with no zero at DC. The
116    /// parity option for the MEDI toolbox's TFI on odd grids (astewartau/QSM.rs#407).
117    MediOddGrid,
118}
119
120/// Arithmetic of [`tfi`]'s Gauss-Newton iterations.
121#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
122#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
123pub enum TfiPrecision {
124    /// Double precision throughout. The default.
125    #[default]
126    Double,
127    /// MATLAB's single precision, as `MEDI_L1.m` ends up running: it makes the data weight from
128    /// `tempn = single(N_std)`, and MATLAB's mixed arithmetic then makes the weight, the data,
129    /// every operator output, every CG vector and the iterate single (an array op with one
130    /// single operand is single). Here: every array is f32, with f32 FFTs, and the double
131    /// operands (`P`, the edge mask, `ε`, after MERIT the reweighted data weight) are rounded to
132    /// single where they meet a single one; inner products are accumulated in double and
133    /// rounded. The parity option for the MEDI toolbox's TFI (astewartau/QSM.rs#407).
134    Single,
135}
136
137/// TFI parameters (2024 `MEDI_L1.m` / `parse_QSM_input.m` defaults; `P_B` and the linear data
138/// term from Liu et al. 2017)
139#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
140#[derive(Clone, Debug)]
141pub struct TfiParams {
142    /// Weight of the data-fidelity term (`'lambda'`): larger fits the data more closely
143    pub lambda: f64,
144    /// Preconditioner outside the mask, `P_B` (1 inside); Liu et al. 2017 use 30 in vivo
145    pub precond: f64,
146    /// MERIT: reweight the noise by the data residual after each iteration (`'merit'`)
147    pub merit: bool,
148    /// Linear data term `||m (D P y − f)||²` (`'linear'`, Liu et al. 2017 eq. 5); `false` gives
149    /// `MEDI_L1`'s default exponential term `||m (exp(i D P y) − exp(i f))||²`
150    pub linear: bool,
151    /// Data weighting: 0 uniform (1 everywhere), 1 SNR (`mask / n_std`, mean 1 in the mask)
152    pub data_weighting: i32,
153    /// Edge fraction for the gradient mask (`'percentage'`)
154    pub percentage: f64,
155    /// CG relative residual tolerance
156    pub cg_tol: f64,
157    /// Maximum CG iterations per Gauss-Newton step
158    pub cg_max_iter: usize,
159    /// Maximum Gauss-Newton iterations
160    pub max_iter: usize,
161    /// Stop when ||dy|| / ||y|| falls below this
162    pub tol: f64,
163    /// Field strength in Tesla (scan parameter, for the radian conversion)
164    pub b0: f64,
165    /// Echo times in seconds, in acquisition order (scan parameter, for the radian
166    /// conversion).
167    pub echo_times: Vec<f64>,
168    /// Which echo time the radian conversion uses. Default [`EchoTime::Spacing`]: the MEDI
169    /// toolbox's `MEDI_L1.m` works on the field in radians over `delta_TE`
170    /// (`x/(2*pi*delta_TE*CF)*1e6`), which its readers set to `TE(2) - TE(1)`, or TE for one echo
171    /// (`Read_Siemens_DICOM.m` lines 176-178).
172    pub te: EchoTime,
173    /// Dipole kernel (default [`TfiKernel::Dft`]). `MEDI_L1.m` calls `dipole_kernel.m`, whose
174    /// k-space grid is half a step off on axes with an odd number of voxels; there it is not the
175    /// dipole field. On QSM-CI's simulated head (164×205×205) that scales χ by ~1.3 against the
176    /// DFT-grid kernel and fits the true χ worse (in the mask, demeaned: NRMSE 71.4 % against
177    /// 62.1 %, r 0.718 against 0.785), so the DFT-grid kernel is the default and
178    /// [`TfiKernel::MediOddGrid`] the parity option. On even grids they are the same.
179    pub kernel: TfiKernel,
180    /// Arithmetic (default [`TfiPrecision::Double`]). `MEDI_L1.m` runs in single precision, by
181    /// accident of `tempn = single(N_std)` (see [`TfiPrecision::Single`]); the CG solves at
182    /// `cg_tol` 0.01 then stop at different points from double precision, which moves χ by about
183    /// 1 % (KKI travelling head) from the same inputs. Double precision solves the same equations
184    /// more accurately, so it is the default and single precision the parity option.
185    pub precision: TfiPrecision,
186}
187
188impl Default for TfiParams {
189    fn default() -> Self {
190        Self {
191            lambda: 1000.0,
192            precond: 30.0,
193            merit: true,
194            linear: true,
195            data_weighting: 1,
196            percentage: 0.9,
197            cg_tol: 0.01,
198            cg_max_iter: 100,
199            max_iter: 10,
200            tol: 0.1,
201            b0: 3.0,
202            echo_times: vec![0.005],
203            te: EchoTime::Spacing,
204            kernel: TfiKernel::Dft,
205            precision: TfiPrecision::Double,
206        }
207    }
208}
209
210/// Preconditioned total field inversion (2024 `MEDI_L1.m` with a preconditioner)
211///
212/// # Arguments
213/// * `total_field` - Total field in ppm, before background removal (nx * ny * nz)
214/// * `mask` - Binary mask (nx * ny * nz), 1 = brain: `P = 1` and the data are fitted here
215/// * `magnitude` - Echo magnitudes, or a combined magnitude (see module docs)
216/// * `grid` - Volume grid (dimensions and voxel sizes)
217/// * `bdir` - B0 field direction
218/// * `params` - TFI parameters, including field strength and echo spacing
219/// * `progress` - Progress callback `(current_step, total_steps)` over CG iterations
220///
221/// # Returns
222/// Susceptibility `P·y` in ppm, zero outside the mask
223pub fn tfi(
224    total_field: &[f64],
225    mask: &[u8],
226    magnitude: EchoMagnitudes,
227    grid: &Grid,
228    bdir: (f64, f64, f64),
229    params: &TfiParams,
230    progress: impl FnMut(usize, usize),
231) -> Vec<f64> {
232    let n = grid.n_total();
233    // iMag = sqrt(sum(abs(iField).^2,4)); [~, N_std] = Fit_ppm_complex(iField)
234    let EchoMagnitudes { data: m, n_echoes } = magnitude;
235    assert!(n_echoes > 0 && m.len() == n * n_echoes, "magnitude is not n_echoes volumes");
236    let imag: Vec<f64> = (0..n)
237        .map(|i| (0..n_echoes).map(|e| m[i + n * e].powi(2)).sum::<f64>().sqrt())
238        .collect();
239    let n_std = crate::utils::fit_ppm_complex_noise(m, n_echoes);
240    tfi_l1(total_field, mask, &imag, &n_std, grid, bdir, params, progress)
241}
242
243/// `MEDI_L1.m` with the preconditioner, on an `iMag` and an `N_std`
244#[allow(clippy::too_many_arguments)]
245fn tfi_l1(
246    total_field: &[f64],
247    mask: &[u8],
248    magnitude: &[f64],
249    n_std: &[f64],
250    grid: &Grid,
251    bdir: (f64, f64, f64),
252    params: &TfiParams,
253    mut progress: impl FnMut(usize, usize),
254) -> Vec<f64> {
255    let n = grid.n_total();
256    let vs = [grid.vsx(), grid.vsy(), grid.vsz()];
257    let dims = grid.dims;
258    let mut fft = Fft3dWorkspace::new(grid.nx(), grid.ny(), grid.nz());
259    let mut buf = vec![Complex64::new(0.0, 0.0); n];
260
261    // ppm -> radians over delta_TE; MEDI_L1 converts back with x/(2*pi*delta_TE*CF)*1e6.
262    let cf = crate::constants::GAMMA_BAR_HZ_PER_T * params.b0;
263    let ppm_to_rad = 2.0 * std::f64::consts::PI * params.te.seconds(&params.echo_times, "TFI") * cf * 1e-6;
264    let mut rdf = vec![0.0f64; n];
265    par_fill(&mut rdf, |i| total_field[i] * ppm_to_rad);
266
267    let mask: Vec<bool> = mask.iter().map(|&m| m != 0).collect();
268    // P = 1*Mask + P_B*(1-Mask) (Liu et al. 2017, eq. 3). P is real, so PH = P.
269    let mut p = vec![0.0f64; n];
270    par_fill(&mut p, |i| if mask[i] { 1.0 } else { params.precond });
271
272    // N_std = N_std.*Mask; tempn = N_std
273    let mut n_std: Vec<f64> = (0..n).map(|i| if mask[i] { n_std[i] } else { 0.0 }).collect();
274
275    // D = dipole_kernel(matrix_size, voxel_size, B0_dir): see TfiKernel
276    let dk = match params.kernel {
277        TfiKernel::Dft => dipole_kernel(grid, bdir),
278        TfiKernel::MediOddGrid => {
279            let n = (bdir.0 * bdir.0 + bdir.1 * bdir.1 + bdir.2 * bdir.2).sqrt();
280            dipole_kernel_medi(grid, (bdir.0 / n, bdir.1 / n, bdir.2 / n))
281        }
282    };
283    let mut m = vec![0.0f64; n];
284    dataterm_mask(params.data_weighting, &n_std, &mask, &mut m);
285    // opts.wG = gradient_mask(...); with a preconditioner, opts.wG = wG.*Mask
286    let mut wg = gradient_mask(magnitude, &mask, grid, params.percentage);
287    for c in wg.iter_mut() {
288        par_update(c, |i, v| {
289            if !mask[i] {
290                *v = 0.0;
291            }
292        });
293    }
294    if params.precision == TfiPrecision::Single {
295        return tfi_single(&rdf, &mask, &p, n_std, &dk, &wg, grid, ppm_to_rad, params, progress);
296    }
297    // m, and with it w2, is zero outside the mask (SNR weighting), so the Dconv(P dx) in A(dx)
298    // is only needed, and the Dconv after the weighting only has input, inside its bounding box.
299    let d: Vec<Complex64> = dk.iter().map(|&v| Complex64::new(v, 0.0)).collect();
300    let mbox = if params.data_weighting == 0 {
301        [0..grid.nx(), 0..grid.ny(), 0..grid.nz()]
302    } else {
303        bounding_box(&mask, dims)
304    };
305
306    let linear = params.linear;
307    // B = m.*exp(1i*RDF), or m.*RDF with 'linear'
308    let data = |m: &[f64], b0: &mut [Complex64]| {
309        par_fill(b0, |i| {
310            if linear { Complex64::new(m[i] * rdf[i], 0.0) } else { m[i] * Complex64::new(0.0, rdf[i]).exp() }
311        });
312    };
313    // W = m.*exp(1i*Dconv(x)), or m.*Dconv(x) with 'linear'
314    let model = |m: f64, dpx: f64| -> Complex64 {
315        if linear { Complex64::new(m * dpx, 0.0) } else { m * Complex64::new(0.0, dpx).exp() }
316    };
317    let mut b0 = vec![Complex64::new(0.0, 0.0); n];
318    data(&m, &mut b0);
319
320    // e = 0.000001, scaled with P: e*PH(P(ones(matrix_size)))
321    let e: Vec<f64> = p.iter().map(|&v| 1e-6 * v * v).collect();
322    let lambda2 = 2.0 * params.lambda;
323
324    let mut x = vec![0.0f64; n];
325    let mut px = vec![0.0f64; n];
326    let mut pq = vec![0.0f64; n];
327    // wG.*Vr.*wG per direction
328    let mut wvw = [vec![0.0f64; n], vec![0.0f64; n], vec![0.0f64; n]];
329    let mut g = [vec![0.0f64; n], vec![0.0f64; n], vec![0.0f64; n]];
330    let mut w = vec![Complex64::new(0.0, 0.0); n];
331    let mut w2 = vec![0.0f64; n];
332    let mut b = vec![0.0f64; n];
333    let mut t = vec![0.0f64; n];
334    let mut rv = vec![0.0f64; n];
335    let mut cg = CgBuffers::new(n);
336    let mask_idx: Vec<usize> = (0..n).filter(|&i| mask[i]).collect();
337    let total = params.max_iter * params.cg_max_iter;
338    let mut res_norm_ratio = f64::INFINITY;
339    let mut iter = 0;
340    while res_norm_ratio > params.tol && iter < params.max_iter {
341        iter += 1;
342        // w = W(opts.m, P(x))
343        par_fill(&mut px, |i| p[i] * x[i]);
344        dconv(&px, &d, &mut fft, &mut buf, &mut t);
345        par_fill(&mut w, |i| model(m[i], t[i]));
346        // reg_MEDI(opts, P(x), PHP): Vr = 1./sqrt(abs(wG.*grad(real(P(x)))).^2 + e)
347        fgrad(&px, dims, vs, &mut g);
348        for c in 0..3 {
349            let (wgc, gc) = (&wg[c], &g[c]);
350            par_fill(&mut wvw[c], |i| {
351                let u = wgc[i] * gc[i];
352                let vr = 1.0 / (u * u + e[i]).sqrt();
353                wgc[i] * vr * wgc[i]
354            });
355        }
356        // fidelity weight: conj(opts.m).*opts.m (linear) or conj(w).*w
357        if linear {
358            par_fill(&mut w2, |i| m[i] * m[i]);
359        } else {
360            par_fill(&mut w2, |i| w[i].norm_sqr());
361        }
362
363        // b = PH(reg(P(x))) + 2*lambda*PH(Dconv(real(conj(w).*conj(1i).*(w-b0))))
364        // or, with 'linear', b = PH(reg(P(x))) + 2*lambda*PH(Dconv(real(conj(opts.m).*(w-b0))))
365        par_fill(&mut rv, |i| {
366            if linear {
367                m[i] * (w[i] - b0[i]).re
368            } else {
369                (w[i].conj() * Complex64::new(0.0, -1.0) * (w[i] - b0[i])).re
370            }
371        });
372        dconv(&rv, &d, &mut fft, &mut buf, &mut t);
373        reg(&px, &wvw, dims, vs, &mut b);
374        par_update(&mut b, |i, v| *v = -(p[i] * *v + lambda2 * p[i] * t[i]));
375
376        // A = @(dx) PH(reg(P(dx))) + 2*lambda*PH(Dconv(w2.*Dconv(P(dx)))); dx = cgsolve(A, -b)
377        let base = (iter - 1) * params.cg_max_iter;
378        cgsolve(
379            |q: &[f64], out: &mut [f64]| {
380                par_fill(&mut pq, |i| p[i] * q[i]);
381                reg(&pq, &wvw, dims, vs, out);
382                dconv_weighted(&pq, &d, &w2, &mbox, &mut fft, &mut buf);
383                par_update(out, |i, v| *v = p[i] * *v + lambda2 * p[i] * buf[i].re);
384            },
385            &b, params.cg_tol, params.cg_max_iter, &mut cg,
386            |k| progress(base + k, total),
387        );
388        let dx = &cg.x;
389
390        // res_norm_ratio = norm(dx)/norm(x); x = x + dx
391        let ndx = dx.iter().map(|v| v * v).sum::<f64>().sqrt();
392        let nrm_x = x.iter().map(|v| v * v).sum::<f64>().sqrt();
393        res_norm_ratio = ndx / nrm_x;
394        par_update(&mut x, |i, v| *v += dx[i]);
395
396        if params.merit {
397            // wres = W(opts.m, P(x)) - b0, used inside the mask only
398            par_fill(&mut px, |i| p[i] * x[i]);
399            dconv(&px, &d, &mut fft, &mut buf, &mut t);
400            let wres: Vec<Complex64> = mask_idx.iter().map(|&i| model(m[i], t[i]) - b0[i]).collect();
401            let mean = wres.iter().copied().sum::<Complex64>() / mask_idx.len() as f64;
402            // factor = std(abs(a))*6, MATLAB's std (normalised by N-1)
403            let a: Vec<f64> = wres.iter().map(|&v| (v - mean).norm()).collect();
404            let am = a.iter().sum::<f64>() / a.len() as f64;
405            let sd = (a.iter().map(|v| (v - am).powi(2)).sum::<f64>() / (a.len() as f64 - 1.0)).sqrt();
406            let factor = sd * 6.0;
407            for (q, &i) in mask_idx.iter().enumerate() {
408                let r = (a[q] / factor).max(1.0);
409                n_std[i] *= r * r;
410            }
411            dataterm_mask(params.data_weighting, &n_std, &mask, &mut m);
412            data(&m, &mut b0);
413        }
414    }
415    progress(total, total);
416
417    // x = P(x)/(2*pi*delta_TE*CF)*1e6.*Mask
418    par_update(&mut x, |i, v| *v = if mask[i] { p[i] * *v / ppm_to_rad } else { 0.0 });
419    x
420}
421
422/// [`tfi_l1`] in MATLAB's single precision ([`TfiPrecision::Single`]): `MEDI_L1.m` line for line
423/// with MATLAB's types. `tempn = single(N_std)` makes the data weight `m`, and with it the data
424/// `b0 = m.*RDF`, single; from there every product with a single operand is single, so the
425/// operator, the right-hand side, `cgsolve.m`'s vectors and scalars, `dx` and the iterate `x`
426/// (`x + dx`) are single. Double operands (`P`, the edge mask, `ε`, the kernel, and after MERIT
427/// the data weight and data, which `dataterm_mask` remakes from `double(N_std)`) are rounded to
428/// single where they meet a single one. FFTs are single precision.
429#[allow(clippy::too_many_arguments)]
430fn tfi_single(
431    rdf: &[f64],
432    mask: &[bool],
433    p: &[f64],
434    mut n_std: Vec<f64>,
435    dk: &[f64],
436    wg: &[Vec<f64>; 3],
437    grid: &Grid,
438    ppm_to_rad: f64,
439    params: &TfiParams,
440    mut progress: impl FnMut(usize, usize),
441) -> Vec<f64> {
442    use num_complex::Complex32;
443    let n = grid.n_total();
444    let (nx, ny, nz) = grid.dims;
445    let vs = [grid.vsx() as f32, grid.vsy() as f32, grid.vsz() as f32];
446    let mut fft = crate::fft::Fft3dWorkspaceF32::new(nx, ny, nz);
447    let mut buf = vec![Complex32::new(0.0, 0.0); n];
448    let d: Vec<f32> = dk.iter().map(|&v| v as f32).collect();
449    let p32: Vec<f32> = p.iter().map(|&v| v as f32).collect();
450    let wg32: Vec<Vec<f32>> = wg.iter().map(|c| c.iter().map(|&v| v as f32).collect()).collect();
451    // e = 0.000001*PH(P(ones)) (reg_MEDI.m), double, rounded where it meets single
452    let e: Vec<f32> = p.iter().map(|&v| (1e-6 * v * v) as f32).collect();
453    let lambda2 = (2.0 * params.lambda) as f32;
454    let linear = params.linear;
455
456    // Dconv = @(dx) real(ifftn(D.*fftn(dx))), single
457    let mut dconv = |x: &[f32], out: &mut [f32]| {
458        for (c, &v) in buf.iter_mut().zip(x) {
459            *c = Complex32::new(v, 0.0);
460        }
461        fft.fft3d(&mut buf);
462        for (c, &dv) in buf.iter_mut().zip(&d) {
463            *c *= dv;
464        }
465        fft.ifft3d(&mut buf);
466        for (o, c) in out.iter_mut().zip(&buf) {
467            *o = c.re;
468        }
469    };
470    let nxy = nx * ny;
471    // fgrad.m, single
472    let fgrad = |x: &[f32], g: &mut [Vec<f32>; 3]| {
473        for k in 0..nz {
474            for j in 0..ny {
475                for i in 0..nx {
476                    let q = i + nx * j + nxy * k;
477                    g[0][q] = if i + 1 < nx { (x[q + 1] - x[q]) / vs[0] } else { 0.0 };
478                    g[1][q] = if j + 1 < ny { (x[q + nx] - x[q]) / vs[1] } else { 0.0 };
479                    g[2][q] = if k + 1 < nz { (x[q + nxy] - x[q]) / vs[2] } else { 0.0 };
480                }
481            }
482        }
483    };
484    // bdiv.m, single
485    let bdiv = |g: &[Vec<f32>; 3], out: &mut [f32]| {
486        for k in 0..nz {
487            for j in 0..ny {
488                for i in 0..nx {
489                    let q = i + nx * j + nxy * k;
490                    let tx = (if i + 1 < nx { g[0][q] } else { 0.0 }) - (if i > 0 { g[0][q - 1] } else { 0.0 });
491                    let ty = (if j + 1 < ny { g[1][q] } else { 0.0 }) - (if j > 0 { g[1][q - nx] } else { 0.0 });
492                    let tz = (if k + 1 < nz { g[2][q] } else { 0.0 }) - (if k > 0 { g[2][q - nxy] } else { 0.0 });
493                    out[q] = -(tx / vs[0] + ty / vs[1] + tz / vs[2]);
494                }
495            }
496        }
497    };
498    // inner products: accumulated in double, returned single
499    let dot = |a: &[f32], b: &[f32]| -> f32 { a.iter().zip(b).map(|(&x, &y)| x as f64 * y as f64).sum::<f64>() as f32 };
500
501    // m = dataterm_mask(mode, single(N_std), Mask): single
502    let dataterm_single = |n_std: &[f64]| -> Vec<f32> {
503        if params.data_weighting == 0 {
504            return vec![1.0; n];
505        }
506        let mut w: Vec<f32> = (0..n)
507            .map(|i| {
508                let v = (mask[i] as u8 as f32) / (n_std[i] as f32);
509                if mask[i] && v.is_finite() { v } else { 0.0 }
510            })
511            .collect();
512        let count = mask.iter().filter(|&&m| m).count() as f64;
513        let mean = (w.iter().zip(mask).filter(|(_, &m)| m).map(|(&v, _)| v as f64).sum::<f64>() / count) as f32;
514        w.iter_mut().for_each(|v| *v /= mean);
515        w
516    };
517    let mut m: Vec<f32> = dataterm_single(&n_std);
518    // b0 = B(m, RDF): m.*RDF (linear) or m.*exp(1i*RDF), single
519    let data = |m: &[f32]| -> Vec<Complex32> {
520        (0..n)
521            .map(|i| {
522                if linear { Complex32::new(m[i] * rdf[i] as f32, 0.0) } else { m[i] * Complex32::new(0.0, rdf[i] as f32).exp() }
523            })
524            .collect()
525    };
526    let mut b0 = data(&m);
527
528    let mut x = vec![0.0f32; n];
529    let mut px = vec![0.0f32; n];
530    let mut t = vec![0.0f32; n];
531    let mut t2 = vec![0.0f32; n];
532    let mut g = [vec![0.0f32; n], vec![0.0f32; n], vec![0.0f32; n]];
533    let mut wvw = [vec![0.0f32; n], vec![0.0f32; n], vec![0.0f32; n]];
534    let mut w = vec![Complex32::new(0.0, 0.0); n];
535    let mut w2 = vec![0.0f32; n];
536    let mut b = vec![0.0f32; n];
537    let mut rv = vec![0.0f32; n];
538    let (mut cx, mut cr, mut cd, mut cq) = (vec![0.0f32; n], vec![0.0f32; n], vec![0.0f32; n], vec![0.0f32; n]);
539    let mut pq = vec![0.0f32; n];
540    let mut regout = vec![0.0f32; n];
541    let model = |m: f32, dpx: f32| -> Complex32 {
542        if linear { Complex32::new(m * dpx, 0.0) } else { m * Complex32::new(0.0, dpx).exp() }
543    };
544    let mask_idx: Vec<usize> = (0..n).filter(|&i| mask[i]).collect();
545    let total = params.max_iter * params.cg_max_iter;
546    let mut res_norm_ratio = f32::INFINITY;
547    let mut iter = 0;
548    while res_norm_ratio > params.tol as f32 && iter < params.max_iter {
549        iter += 1;
550        for i in 0..n {
551            px[i] = p32[i] * x[i];
552        }
553        dconv(&px, &mut t);
554        for i in 0..n {
555            w[i] = model(m[i], t[i]);
556        }
557        // reg_MEDI: Vr = 1./sqrt(abs(wG.*grad(real(P(x)))).^2 + e)
558        fgrad(&px, &mut g);
559        for c in 0..3 {
560            for i in 0..n {
561                let u = wg32[c][i] * g[c][i];
562                let vr = 1.0 / (u * u + e[i]).sqrt();
563                wvw[c][i] = wg32[c][i] * vr * wg32[c][i];
564            }
565        }
566        if linear {
567            for i in 0..n {
568                w2[i] = m[i] * m[i];
569            }
570        } else {
571            for i in 0..n {
572                w2[i] = w[i].norm_sqr();
573            }
574        }
575        // reg(dx) = PH(div(wG.*Vr.*wG.*grad(real(P(dx)))))
576        let regop = |q: &[f32], out: &mut [f32], pq: &mut [f32], g: &mut [Vec<f32>; 3]| {
577            for i in 0..n {
578                pq[i] = p32[i] * q[i];
579            }
580            fgrad(pq, g);
581            for c in 0..3 {
582                for i in 0..n {
583                    g[c][i] *= wvw[c][i];
584                }
585            }
586            bdiv(g, out);
587            for i in 0..n {
588                out[i] *= p32[i];
589            }
590        };
591        // b = reg(x) + 2*lambda*PH(Dconv(real(conj(m).*(w-b0)))) (or the exponential term's)
592        for i in 0..n {
593            rv[i] = if linear {
594                m[i] * (w[i] - b0[i]).re
595            } else {
596                (w[i].conj() * Complex32::new(0.0, -1.0) * (w[i] - b0[i])).re
597            };
598        }
599        dconv(&rv, &mut t);
600        regop(&x, &mut b, &mut pq, &mut g);
601        for i in 0..n {
602            b[i] = -(b[i] + lambda2 * (p32[i] * t[i]));
603        }
604        // A = @(dx) reg(dx) + 2*lambda*PH(Dconv(w2.*Dconv(P(dx)))); dx = cgsolve(A, -b)
605        let mut apply = |q: &[f32], out: &mut [f32]| {
606            regop(q, &mut regout, &mut pq, &mut g);
607            for i in 0..n {
608                pq[i] = p32[i] * q[i];
609            }
610            dconv(&pq, &mut t);
611            for i in 0..n {
612                t[i] *= w2[i];
613            }
614            dconv(&t, &mut t2);
615            for i in 0..n {
616                out[i] = regout[i] + lambda2 * (p32[i] * t2[i]);
617            }
618        };
619        // cgsolve.m, single
620        let base = (iter - 1) * params.cg_max_iter;
621        let tol = params.cg_tol as f32;
622        cx.fill(0.0);
623        cr.copy_from_slice(&b);
624        cd.copy_from_slice(&b);
625        let mut delta = dot(&cr, &cr);
626        let delta0 = dot(&b, &b);
627        let mut numiter = 0;
628        while numiter < params.cg_max_iter && delta > tol * tol * delta0 {
629            apply(&cd, &mut cq);
630            let alpha = delta / dot(&cd, &cq);
631            for i in 0..n {
632                cx[i] += alpha * cd[i];
633            }
634            if (numiter + 1) % 50 == 0 {
635                apply(&cx, &mut cq);
636                for i in 0..n {
637                    cr[i] = b[i] - cq[i];
638                }
639            } else {
640                for i in 0..n {
641                    cr[i] -= alpha * cq[i];
642                }
643            }
644            let delta_old = delta;
645            delta = dot(&cr, &cr);
646            let beta = delta / delta_old;
647            for i in 0..n {
648                cd[i] = cr[i] + beta * cd[i];
649            }
650            numiter += 1;
651            progress(base + numiter, total);
652        }
653        // res_norm_ratio = norm(dx)/norm(x); x = x + dx
654        res_norm_ratio = dot(&cx, &cx).sqrt() / dot(&x, &x).sqrt();
655        for i in 0..n {
656            x[i] += cx[i];
657        }
658
659        if params.merit {
660            for i in 0..n {
661                px[i] = p32[i] * x[i];
662            }
663            dconv(&px, &mut t);
664            // wres = W(m, P(x)) - b0, single; mean and std over the mask in single
665            let wres: Vec<Complex32> = mask_idx.iter().map(|&i| model(m[i], t[i]) - b0[i]).collect();
666            let cnt = mask_idx.len() as f64;
667            let mean = {
668                let s = wres.iter().fold(num_complex::Complex64::new(0.0, 0.0), |a, v| a + num_complex::Complex64::new(v.re as f64, v.im as f64));
669                Complex32::new((s.re / cnt) as f32, (s.im / cnt) as f32)
670            };
671            let a: Vec<f32> = wres.iter().map(|&v| (v - mean).norm()).collect();
672            let am = (a.iter().map(|&v| v as f64).sum::<f64>() / cnt) as f32;
673            let sd = ((a.iter().map(|&v| ((v - am) as f64).powi(2)).sum::<f64>() / (cnt - 1.0)).sqrt()) as f32;
674            let factor = sd * 6.0;
675            // N_std(Mask==1) = N_std(Mask==1).*wres(Mask==1).^2: single product, stored double
676            for (q, &i) in mask_idx.iter().enumerate() {
677                let r = (a[q] / factor).max(1.0);
678                n_std[i] = ((n_std[i] as f32) * (r * r)) as f64;
679            }
680            // tempn = double(N_std); m = dataterm_mask(...) in double, b0 = B(m, RDF) in double,
681            // both rounded to single where they meet the single iterate
682            let mut md = vec![0.0f64; n];
683            dataterm_mask(params.data_weighting, &n_std, mask, &mut md);
684            m = md.iter().map(|&v| v as f32).collect();
685            b0 = (0..n)
686                .map(|i| {
687                    if linear {
688                        Complex32::new((md[i] * rdf[i]) as f32, 0.0)
689                    } else {
690                        let z = md[i] * num_complex::Complex64::new(0.0, rdf[i]).exp();
691                        Complex32::new(z.re as f32, z.im as f32)
692                    }
693                })
694                .collect();
695        }
696    }
697    progress(total, total);
698    let scale = ppm_to_rad as f32;
699    (0..n).map(|i| if mask[i] { (p32[i] * x[i] / scale) as f64 } else { 0.0 }).collect()
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    fn small() -> TfiParams {
707        TfiParams { cg_max_iter: 10, max_iter: 3, ..TfiParams::default() }
708    }
709
710    fn cube_mask(n: usize, lo: usize, hi: usize) -> Vec<u8> {
711        (0..n * n * n)
712            .map(|i| {
713                let (x, y, z) = (i % n, (i / n) % n, i / (n * n));
714                u8::from((lo..hi).contains(&x) && (lo..hi).contains(&y) && (lo..hi).contains(&z))
715            })
716            .collect()
717    }
718
719    #[test]
720    fn zero_field_gives_zero() {
721        let n = 8;
722        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
723        let mask = cube_mask(n, 2, 6);
724        let mag: Vec<f64> = (0..n * n * n).map(|i| 1.0 + (i % 5) as f64).collect();
725        let chi = tfi_l1(&vec![0.0; n * n * n], &mask, &mag, &vec![1.0; mag.len()], &grid, (0.0, 0.0, 1.0), &small(), |_, _| {});
726        assert!(chi.iter().all(|v| v.abs() < 1e-12));
727    }
728
729    #[test]
730    fn output_is_masked_and_finite() {
731        let n = 8;
732        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
733        let mask = cube_mask(n, 2, 6);
734        let field: Vec<f64> = (0..n * n * n).map(|i| 0.01 * ((i % 7) as f64 - 3.0)).collect();
735        let mag: Vec<f64> = (0..n * n * n).map(|i| 1.0 + (i % 5) as f64).collect();
736        for linear in [false, true] {
737            let p = TfiParams { linear, ..small() };
738            let chi = tfi_l1(&field, &mask, &mag, &vec![1.0; mag.len()], &grid, (0.0, 0.0, 1.0), &p, |_, _| {});
739            assert!(chi.iter().any(|&v| v != 0.0));
740            for i in 0..chi.len() {
741                assert!(chi[i].is_finite());
742                if mask[i] == 0 {
743                    assert_eq!(chi[i], 0.0);
744                }
745            }
746        }
747    }
748
749    /// The field scale matters (λ is fixed and, with the exponential term, the data term is
750    /// nonlinear), so `b0` and `te` change the result.
751    #[test]
752    fn units_follow_b0_and_te() {
753        let n = 8;
754        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
755        let mask = cube_mask(n, 2, 6);
756        let field: Vec<f64> = (0..n * n * n).map(|i| 0.05 * ((i % 7) as f64 - 3.0)).collect();
757        let mag: Vec<f64> = (0..n * n * n).map(|i| 1.0 + (i % 5) as f64).collect();
758        let a = tfi_l1(&field, &mask, &mag, &vec![1.0; mag.len()], &grid, (0.0, 0.0, 1.0), &small(), |_, _| {});
759        let p7 = TfiParams { b0: 7.0, ..small() };
760        let b = tfi_l1(&field, &mask, &mag, &vec![1.0; mag.len()], &grid, (0.0, 0.0, 1.0), &p7, |_, _| {});
761        assert_ne!(a, b);
762    }
763
764    /// Echo magnitudes give the RSS edge mask and the Fit_ppm_complex noise map.
765    #[test]
766    fn echo_magnitudes_give_rss_and_the_fit_noise() {
767        let n = 8;
768        let nt = n * n * n;
769        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
770        let mask = cube_mask(n, 2, 6);
771        let field: Vec<f64> = (0..nt).map(|i| 0.01 * ((i % 7) as f64 - 3.0)).collect();
772        let mag: Vec<f64> = (0..nt).map(|i| 1.0 + (i % 5) as f64).collect();
773        let echoes: Vec<f64> = mag.iter().copied().chain(mag.iter().map(|v| 0.5 * v + (v % 2.0))).collect();
774        let p = small();
775        let a = tfi(&field, &mask, EchoMagnitudes { data: &echoes, n_echoes: 2 }, &grid, (0.0, 0.0, 1.0), &p, |_, _| {});
776        let rss: Vec<f64> = (0..nt).map(|i| (echoes[i].powi(2) + echoes[i + nt].powi(2)).sqrt()).collect();
777        let n_std = crate::utils::fit_ppm_complex_noise(&echoes, 2);
778        let b = tfi_l1(&field, &mask, &rss, &n_std, &grid, (0.0, 0.0, 1.0), &p, |_, _| {});
779        assert_eq!(a, b);
780    }
781}