Skip to main content

qsm_core/inversion/
ilsqr.rs

1//! iLSQR: Iterative LSQR for QSM with streaking artifact removal
2//!
3//! Reference:
4//! Li, W., Wang, N., Yu, F., Han, H., Cao, W., Romero, R., Tantiwongkosi, B.,
5//! Duong, T.Q., Liu, C. (2015). "A method for estimating and removing streaking
6//! artifacts in quantitative susceptibility mapping."
7//! NeuroImage, 108:111-122. https://doi.org/10.1016/j.neuroimage.2014.12.043
8//!
9//! Reference implementation: https://github.com/kamesy/QSM.m
10//!
11//! The algorithm consists of 4 steps:
12//! 1. Initial LSQR solution with Laplacian-based weights
13//! 2. FastQSM estimate using sign(D) approximation
14//! 3. Streaking artifact estimation using LSMR
15//! 4. Artifact subtraction
16
17/// Parameters for the iLSQR algorithm.
18#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
19#[derive(Clone, Debug)]
20pub struct IlsqrParams {
21    /// Convergence tolerance (default: 0.01)
22    pub tol: f64,
23    /// Maximum iterations (default: 50)
24    pub max_iter: usize,
25}
26
27impl Default for IlsqrParams {
28    fn default() -> Self {
29        Self {
30            tol: 0.01,
31            max_iter: 50,
32        }
33    }
34}
35
36use std::cell::RefCell;
37use num_complex::Complex64;
38use crate::fft::Fft3dWorkspace;
39use crate::kernels::dipole::dipole_kernel;
40use crate::kernels::smv::smv_kernel;
41use crate::utils::gradient::{fgrad, bdiv};
42use crate::Grid;
43
44// ============================================================================
45// LSQR Solver
46// ============================================================================
47
48/// LSQR iterative solver for Ax = b
49///
50/// Solves the least squares problem min ||Ax - b||² using the LSQR algorithm.
51/// Based on Paige & Saunders (1982).
52///
53/// # Arguments
54/// * `apply_a` - Function that computes A*x
55/// * `apply_at` - Function that computes A^T*x
56/// * `b` - Right-hand side vector
57/// * `tol` - Convergence tolerance
58/// * `max_iter` - Maximum iterations
59///
60/// # Returns
61/// Solution vector x
62pub fn lsqr<F, G>(
63    apply_a: F,
64    apply_at: G,
65    b: &[f64],
66    tol: f64,
67    max_iter: usize,
68) -> Vec<f64>
69where
70    F: Fn(&[f64]) -> Vec<f64>,
71    G: Fn(&[f64]) -> Vec<f64>,
72{
73    // Initialize
74    let mut u = b.to_vec();
75    let mut beta = norm(&u);
76
77    if beta > 0.0 {
78        scale_inplace(&mut u, 1.0 / beta);
79    }
80
81    let mut v = apply_at(&u);
82    let n = v.len();
83    let mut alpha = norm(&v);
84
85    if alpha > 0.0 {
86        scale_inplace(&mut v, 1.0 / alpha);
87    }
88
89    let mut w = v.clone();
90    let mut x = vec![0.0; n];
91
92    let mut phi_bar = beta;
93    let mut rho_bar = alpha;
94
95    let bnorm = beta;
96
97    for _iter in 0..max_iter {
98        // Bidiagonalization
99        let mut u_new = apply_a(&v);
100        axpy(&mut u_new, -alpha, &u);
101        beta = norm(&u_new);
102
103        if beta > 0.0 {
104            scale_inplace(&mut u_new, 1.0 / beta);
105        }
106        u = u_new;
107
108        let mut v_new = apply_at(&u);
109        axpy(&mut v_new, -beta, &v);
110        alpha = norm(&v_new);
111
112        if alpha > 0.0 {
113            scale_inplace(&mut v_new, 1.0 / alpha);
114        }
115        v = v_new;
116
117        // Construct and apply rotation
118        let rho = (rho_bar * rho_bar + beta * beta).sqrt();
119        let c = rho_bar / rho;
120        let s = beta / rho;
121        let theta = s * alpha;
122        rho_bar = -c * alpha;
123        let phi = c * phi_bar;
124        phi_bar *= s;
125
126        // Update x and w
127        let t1 = phi / rho;
128        let t2 = -theta / rho;
129
130        for i in 0..n {
131            x[i] += t1 * w[i];
132            w[i] = v[i] + t2 * w[i];
133        }
134
135        // Check convergence
136        let rel_residual = phi_bar / (bnorm + 1e-20);
137
138        if rel_residual < tol {
139            break;
140        }
141    }
142
143    x
144}
145
146// ============================================================================
147// LSQR Solver (Complex)
148// ============================================================================
149
150/// Complex norm
151fn norm_complex(x: &[Complex64]) -> f64 {
152    x.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt()
153}
154
155/// Complex scale in place
156fn scale_complex_inplace(x: &mut [Complex64], s: f64) {
157    for v in x.iter_mut() {
158        *v *= s;
159    }
160}
161
162/// Complex axpy: y += a * x
163fn axpy_complex(y: &mut [Complex64], a: f64, x: &[Complex64]) {
164    for (yi, xi) in y.iter_mut().zip(x.iter()) {
165        *yi += a * xi;
166    }
167}
168
169/// LSQR iterative solver for Ax = b (complex version)
170///
171/// Solves the least squares problem min ||Ax - b||² using the LSQR algorithm.
172/// Based on Paige & Saunders (1982), with convergence tests matching MATLAB's lsqr.
173///
174/// Convergence tests (matching MATLAB):
175/// 1. ||r|| / ||b|| <= btol + atol * ||A|| * ||x|| / ||b||  (residual test)
176/// 2. ||A'r|| / (||A|| * ||r||) <= atol  (normal equations test)
177pub fn lsqr_complex<F, G>(
178    apply_a: F,
179    apply_ah: G,
180    b: &[Complex64],
181    tol: f64,
182    max_iter: usize,
183    verbose: bool,
184) -> Vec<Complex64>
185where
186    F: Fn(&[Complex64]) -> Vec<Complex64>,
187    G: Fn(&[Complex64]) -> Vec<Complex64>,
188{
189    // Initialize: beta_1 * u_1 = b
190    let mut u = b.to_vec();
191    let mut beta = norm_complex(&u);
192
193    if beta > 0.0 {
194        scale_complex_inplace(&mut u, 1.0 / beta);
195    }
196
197    // alpha_1 * v_1 = A^H * u_1
198    let mut v = apply_ah(&u);
199    let n = v.len();
200    let mut alpha = norm_complex(&v);
201
202    if alpha > 0.0 {
203        scale_complex_inplace(&mut v, 1.0 / alpha);
204    }
205
206    let mut w = v.clone();
207    let mut x = vec![Complex64::new(0.0, 0.0); n];
208
209    let mut phi_bar = beta;
210    let mut rho_bar = alpha;
211
212    let bnorm = beta;
213    let atol = tol;
214    let btol = tol;
215
216    // Track ||A|| estimate
217    let mut norm_a2 = alpha * alpha;
218
219    // ||x|| estimate using plane rotations (matches MATLAB's built-in lsqr xxnorm)
220    // Verified: produces identical values to exact norm_complex(&x)
221    let mut xxnorm = 0.0;
222    let mut z_sol = 0.0;
223    let mut cs2 = -1.0;
224    let mut sn2 = 0.0;
225
226    if alpha * beta == 0.0 {
227        return x;
228    }
229
230    for _iter in 0..max_iter {
231        // Bidiagonalization step
232        let mut u_new = apply_a(&v);
233        axpy_complex(&mut u_new, -alpha, &u);
234        beta = norm_complex(&u_new);
235
236        if beta > 0.0 {
237            scale_complex_inplace(&mut u_new, 1.0 / beta);
238        }
239        u = u_new;
240
241        let mut v_new = apply_ah(&u);
242        axpy_complex(&mut v_new, -beta, &v);
243        alpha = norm_complex(&v_new);
244
245        if alpha > 0.0 {
246            scale_complex_inplace(&mut v_new, 1.0 / alpha);
247        }
248        v = v_new;
249
250        // Construct and apply Givens rotation
251        let rho = (rho_bar * rho_bar + beta * beta).sqrt();
252        let c = rho_bar / rho;
253        let s = beta / rho;
254        let theta = s * alpha;
255        rho_bar = -c * alpha;
256        let phi = c * phi_bar;
257        phi_bar *= s;
258
259        // ||x|| estimation via plane rotations (MATLAB's xxnorm approach)
260        let delta = sn2 * rho;
261        let gambar = -cs2 * rho;
262        let rhs = phi - delta * z_sol;
263        let zbar = rhs / gambar;
264        let xnorm = (xxnorm + zbar * zbar).sqrt();
265        let gamma = (gambar * gambar + theta * theta).sqrt();
266        cs2 = gambar / gamma;
267        sn2 = theta / gamma;
268        z_sol = rhs / gamma;
269        xxnorm += z_sol * z_sol;
270
271        // Update x and w
272        let t1 = phi / rho;
273        let t2 = -theta / rho;
274        for i in 0..n {
275            x[i] += t1 * w[i];
276            w[i] = v[i] + t2 * w[i];
277        }
278
279        // Estimate norms for convergence tests
280        let normr = phi_bar;
281        let norm_ar = alpha * (c * phi_bar).abs();
282
283        norm_a2 += beta * beta + alpha * alpha;
284        let norm_a = norm_a2.sqrt();
285
286        // Convergence test.
287        //
288        // The kamesy reference (QSM.m/src/inversion/ilsqr.m, Step 1 `lsqr_`)
289        // calls MATLAB's *built-in* `lsqr(afun, b, tol, maxit)`, which stops on
290        // the simple relative residual `||b - A*x|| / ||b|| <= tol`. It does NOT
291        // use the augmented Paige-Saunders test
292        // `||r||/||b|| <= btol + atol*||A||*||x||/||b||` (that belongs to
293        // `lsqrSOL`). The previous augmented test tripped at iter ~10 (relres
294        // ~0.09) because the `atol*||A||*||x||/||b||` term inflated the
295        // threshold, leaving Step 1 badly under-converged (xlsqr NRMSE ~19% vs
296        // kamesy). Matching the built-in relative-residual test lets Step 1 run
297        // to relres <= tol (~39 iters here), reproducing kamesy's xlsqr exactly
298        // (NRMSE ~0.4%).
299        //
300        // `normr = phi_bar` is the LSQR estimate of ||r||; `norm_ar`, `norm_a`,
301        // `xnorm` are retained only for the optional verbose printout.
302        let test1 = normr / (bnorm + 1e-20);
303        let test2 = norm_ar / ((norm_a * normr) + 1e-20);
304        let _ = (btol, xnorm);
305
306        if verbose {
307            eprintln!("  LSQR iter {:>3}: ||r||/||b||={:.6e}  ||A'r||/(||A||·||r||)={:.6e}",
308                _iter + 1, test1, test2);
309        }
310
311        if test1 <= atol {
312            if verbose {
313                eprintln!("  LSQR converged at iteration {} (relres={:.4e})",
314                    _iter + 1, test1);
315            }
316            break;
317        }
318    }
319
320    x
321}
322
323// ============================================================================
324// LSMR Solver
325// ============================================================================
326
327/// LSMR iterative solver for Ax = b
328///
329/// Solves the least squares problem min ||Ax - b||² using the LSMR algorithm.
330/// Based on Fong & Saunders (2011). More stable than LSQR for ill-conditioned problems.
331///
332/// # Arguments
333/// * `apply_a` - Function that computes A*x
334/// * `apply_at` - Function that computes A^T*x
335/// * `b` - Right-hand side vector
336/// * `n` - Size of solution vector
337/// * `atol` - Absolute tolerance
338/// * `btol` - Relative tolerance
339/// * `max_iter` - Maximum iterations
340/// * `verbose` - Print progress
341///
342/// # Returns
343/// Solution vector x
344pub fn lsmr<F, G>(
345    apply_a: F,
346    apply_at: G,
347    b: &[f64],
348    n: usize,
349    atol: f64,
350    btol: f64,
351    max_iter: usize,
352    _verbose: bool,
353) -> Vec<f64>
354where
355    F: Fn(&[f64]) -> Vec<f64>,
356    G: Fn(&[f64]) -> Vec<f64>,
357{
358    // Reference: Fong & Saunders (2011), "LSMR: An iterative algorithm for
359    // sparse least-squares problems", SIAM J. Sci. Comput.
360    // Based on the official MATLAB implementation by Fong & Saunders.
361
362    // Initialize: beta*u = b, alpha*v = A'*u
363    let mut u = b.to_vec();
364    let mut beta = norm(&u);
365
366    if beta > 0.0 {
367        scale_inplace(&mut u, 1.0 / beta);
368    }
369
370    let mut v = apply_at(&u);
371    let mut alpha = norm(&v);
372
373    if alpha > 0.0 {
374        scale_inplace(&mut v, 1.0 / alpha);
375    }
376
377    // Initialize variables (matching MATLAB reference variable names)
378    let mut alpha_bar = alpha;
379    let mut zeta_bar = alpha * beta;
380    let mut rho = 1.0;
381    let mut rho_bar = 1.0;
382    let mut c_bar = 1.0;
383    let mut s_bar = 0.0;
384
385    let mut h = v.clone();
386    let mut h_bar = vec![0.0; n];
387    let mut x = vec![0.0; n];
388
389    // Variables for ||r|| estimation
390    let normb = beta;
391    let mut betadd = beta;
392    let mut betad = 0.0;
393    let mut rhodold = 1.0;
394    let mut tautildeold = 0.0;
395    let mut thetatilde = 0.0;
396    let mut zeta = 0.0;
397    let d = 0.0;
398
399    // Variables for ||A|| and cond(A) estimation
400    let mut norm_a2 = alpha * alpha;
401    let mut maxrbar = 0.0f64;
402    let mut minrbar = 1e100f64;
403    let conlim = 1e8;
404    let ctol = if conlim > 0.0 { 1.0 / conlim } else { 0.0 };
405
406    // Early exit if A'b = 0
407    if alpha * beta == 0.0 {
408        return x;
409    }
410
411    for _iter in 0..max_iter {
412        // Bidiagonalization
413        let mut u_new = apply_a(&v);
414        axpy(&mut u_new, -alpha, &u);
415        beta = norm(&u_new);
416
417        if beta > 0.0 {
418            scale_inplace(&mut u_new, 1.0 / beta);
419        }
420        u = u_new;
421
422        let mut v_new = apply_at(&u);
423        axpy(&mut v_new, -beta, &v);
424        alpha = norm(&v_new);
425
426        if alpha > 0.0 {
427            scale_inplace(&mut v_new, 1.0 / alpha);
428        }
429        v = v_new;
430
431        // Construct rotation Q_i (undamped: alphahat = alphabar)
432        let rho_old = rho;
433        rho = (alpha_bar * alpha_bar + beta * beta).sqrt();
434        let c = alpha_bar / rho;
435        let s = beta / rho;
436        let theta_new = s * alpha;
437        alpha_bar = c * alpha;
438
439        // Construct rotation Qbar_i
440        let rho_bar_old = rho_bar;
441        let zeta_old = zeta;
442        let theta_bar = s_bar * rho;
443        let rho_temp = c_bar * rho;
444        rho_bar = (rho_temp * rho_temp + theta_new * theta_new).sqrt();
445        c_bar = rho_temp / rho_bar;
446        s_bar = theta_new / rho_bar;
447        zeta = c_bar * zeta_bar;
448        zeta_bar *= -s_bar;
449
450        // Update h_bar, x, h
451        for i in 0..n {
452            h_bar[i] = h[i] - (theta_bar * rho / (rho_old * rho_bar_old)) * h_bar[i];
453            x[i] += (zeta / (rho * rho_bar)) * h_bar[i];
454            h[i] = v[i] - (theta_new / rho) * h[i];
455        }
456
457        // Estimate ||r|| (from reference implementation)
458        // For undamped case: chat=1, shat=0, so betaacute=betadd, betacheck=0
459        let betaacute = betadd;      // chat * betadd (chat=1 for undamped)
460        // betacheck = 0 for undamped (shat=0), so d += 0
461        let betahat = c * betaacute;
462        betadd = -s * betaacute;
463
464        let thetatildeold = thetatilde;
465        let rhotildeold = (rhodold * rhodold + theta_bar * theta_bar).sqrt();
466        let ctildeold = rhodold / rhotildeold;
467        let stildeold = theta_bar / rhotildeold;
468        thetatilde = stildeold * rho_bar;
469        rhodold = ctildeold * rho_bar;
470        betad = -stildeold * betad + ctildeold * betahat;
471
472        tautildeold = (zeta_old - thetatildeold * tautildeold) / rhotildeold;
473        let taud = (zeta - thetatilde * tautildeold) / rhodold;
474        // d += betacheck^2 = 0 for undamped case
475        let normr = (d + (betad - taud).powi(2) + betadd * betadd).sqrt();
476
477        // Estimate ||A||
478        norm_a2 += beta * beta;
479        let norm_a = norm_a2.sqrt();
480        norm_a2 += alpha * alpha;
481
482        // Estimate cond(A) (matching MATLAB reference)
483        maxrbar = maxrbar.max(rho_bar_old);
484        if _iter > 0 {
485            minrbar = minrbar.min(rho_bar_old);
486        }
487        let cond_a = maxrbar.max(rho_temp) / minrbar.min(rho_temp);
488
489        // Convergence tests (matching reference implementation)
490        let norm_ar = zeta_bar.abs();
491        let normx = norm(&x);
492
493        let test1 = normr / (normb + 1e-20);
494        let test2 = norm_ar / ((norm_a * normr) + 1e-20);
495        let test3 = 1.0 / (cond_a + 1e-20);
496        let rtol = btol + atol * norm_a * normx / (normb + 1e-20);
497
498        if _verbose {
499            eprintln!("  LSMR iter {:>3}: ||r||/||b||={:.6e}  ||A'r||/(||A||·||r||)={:.6e}  1/condA={:.6e}  rtol={:.6e}",
500                _iter + 1, test1, test2, test3, rtol);
501        }
502
503        // Test3 (condition number) checked first, then test2, then test1
504        // matching MATLAB priority where later tests override earlier istop
505        if test3 <= ctol || test2 <= atol || test1 <= rtol {
506            if _verbose {
507                let reason = if test1 <= rtol { "test1 (residual)"
508                } else if test2 <= atol { "test2 (||A'r||)"
509                } else { "test3 (cond(A))" };
510                eprintln!("  LSMR converged at iteration {} via {}", _iter + 1, reason);
511            }
512            break;
513        }
514    }
515
516    x
517}
518
519// ============================================================================
520// Weight Functions
521// ============================================================================
522
523/// Compute Laplacian of a 3D field using mask-adaptive finite differences
524///
525/// Matches MATLAB's lap1_mex.c: uses central differences where both neighbors
526/// are in the mask, forward/backward one-sided stencils near mask boundaries,
527/// and zero contribution where neither neighbor is in the mask.
528fn compute_laplacian(
529    f: &[f64],
530    mask: &[u8],
531    nx: usize, ny: usize, nz: usize,
532    vsx: f64, vsy: f64, vsz: f64,
533) -> Vec<f64> {
534    let n_total = nx * ny * nz;
535    let mut lap = vec![0.0; n_total];
536
537    let hx = 1.0 / (vsx * vsx);
538    let hy = 1.0 / (vsy * vsy);
539    let hz = 1.0 / (vsz * vsz);
540
541    let nxny = nx * ny;
542
543    for k in 0..nz {
544        for j in 0..ny {
545            let jk_offset = j * nx + k * nxny;
546            for i in 0..nx {
547                let l = i + jk_offset;
548
549                if mask[l] == 0 {
550                    continue;
551                }
552
553                // X-axis contribution
554                lap[l] += hx * lap1_axis(f, mask, l, 1, nx, i, nx);
555
556                // Y-axis contribution
557                lap[l] += hy * lap1_axis(f, mask, l, nx, nxny, j * nx, nxny);
558
559                // Z-axis contribution
560                lap[l] += hz * lap1_axis(f, mask, l, nxny, n_total, k * nxny, n_total);
561            }
562        }
563    }
564
565    lap
566}
567
568/// Compute second derivative along one axis using mask-adaptive stencil.
569///
570/// Matches MATLAB's lap1_mex.c logic:
571/// - `idx = 2*G[l+a] + G[l-a]` selects the stencil type:
572///   3 = central, 2 = forward, 1 = backward, 0 = zero
573/// - At domain boundaries: i=0 → forward, i=N-1 → backward
574///
575/// # Arguments
576/// * `f` - field values
577/// * `mask` - binary mask
578/// * `l` - linear index of current voxel
579/// * `a` - stride for this axis (1 for x, nx for y, nx*ny for z)
580/// * `n_axis` - total extent for this axis (nx for x, nx*ny for y, nx*ny*nz for z)
581/// * `coord` - axis coordinate as linear offset (i for x, j*nx for y, k*nx*ny for z)
582/// * `n_total` - total number of voxels (only used for z boundary detection)
583#[inline]
584fn lap1_axis(
585    f: &[f64],
586    mask: &[u8],
587    l: usize,
588    a: usize,
589    n_axis: usize,
590    coord: usize,
591    n_total: usize,
592) -> f64 {
593    // Determine the stencil type based on mask of neighbors and boundary
594    // MATLAB: (i-1) < NXX ? 2*G[l+a]+G[l-a] : (i==0)*2 + (i==NX)
595    // where NXX = N_axis_size - 2 (using size_t underflow trick for boundary detection)
596    let n_end = n_axis - a; // corresponds to NX, NY, NZ in MATLAB (last element coord)
597    let n_interior = n_axis - 2 * a; // corresponds to NXX, NYY, NZZ
598
599    let stencil = if coord.wrapping_sub(a) < n_interior {
600        // Interior: check mask neighbors
601        2 * (mask[l + a] as u8) + (mask[l - a] as u8)
602    } else {
603        // Boundary: first → forward(2), last → backward(1)
604        if coord == 0 { 2 } else if coord == n_end { 1 } else { 0 }
605    };
606
607    match stencil {
608        3 => {
609            // Central: u[l-a] - 2u[l] + u[l+a]
610            f[l - a] - 2.0 * f[l] + f[l + a]
611        }
612        2 => {
613            // Forward one-sided
614            lap1_forward(f, mask, l, a, n_axis, coord, n_total)
615        }
616        1 => {
617            // Backward one-sided
618            lap1_backward(f, mask, l, a, n_axis, coord, n_total)
619        }
620        _ => 0.0, // Neither neighbor in mask
621    }
622}
623
624/// Forward one-sided second derivative (matching MATLAB's fd/ff functions)
625#[inline]
626fn lap1_forward(
627    f: &[f64],
628    mask: &[u8],
629    l: usize,
630    a: usize,
631    n_axis: usize,
632    coord: usize,
633    _n_total: usize,
634) -> f64 {
635    // 4th order: 2u - 5u[+a] + 4u[+2a] - u[+3a]
636    if coord + 3 * a < n_axis && mask[l + 2 * a] != 0 && mask[l + 3 * a] != 0 {
637        2.0 * f[l] - 5.0 * f[l + a] + 4.0 * f[l + 2 * a] - f[l + 3 * a]
638    }
639    // 2nd order: u - 2u[+a] + u[+2a]
640    else if coord + 2 * a < n_axis && mask[l + 2 * a] != 0 {
641        f[l] - 2.0 * f[l + a] + f[l + 2 * a]
642    }
643    // 1st order: u[+a] - u
644    else {
645        f[l + a] - f[l]
646    }
647}
648
649/// Backward one-sided second derivative (matching MATLAB's bd/bf functions)
650#[inline]
651fn lap1_backward(
652    f: &[f64],
653    mask: &[u8],
654    l: usize,
655    a: usize,
656    n_axis: usize,
657    coord: usize,
658    _n_total: usize,
659) -> f64 {
660    // 4th order: -u[-3a] + 4u[-2a] - 5u[-a] + 2u
661    if coord.wrapping_sub(3 * a) < n_axis && mask[l - 3 * a] != 0 && mask[l - 2 * a] != 0 {
662        -f[l - 3 * a] + 4.0 * f[l - 2 * a] - 5.0 * f[l - a] + 2.0 * f[l]
663    }
664    // 2nd order: u[-2a] - 2u[-a] + u
665    else if coord.wrapping_sub(2 * a) < n_axis && mask[l - 2 * a] != 0 {
666        f[l - 2 * a] - 2.0 * f[l - a] + f[l]
667    }
668    // 1st order: u[-a] - u
669    else {
670        f[l - a] - f[l]
671    }
672}
673
674/// Laplacian weights for iLSQR (Equation 7)
675///
676/// Weights based on Laplacian magnitude with percentile-based thresholding.
677fn laplacian_weights_ilsqr(
678    f: &[f64],
679    mask: &[u8],
680    nx: usize, ny: usize, nz: usize,
681    vsx: f64, vsy: f64, vsz: f64,
682    pmin: f64,
683    pmax: f64,
684) -> Vec<f64> {
685    let n_total = nx * ny * nz;
686    let mut w = vec![0.0; n_total];
687
688    // Compute Laplacian
689    let lap = compute_laplacian(f, mask, nx, ny, nz, vsx, vsy, vsz);
690
691    // Collect masked Laplacian values for percentile calculation
692    let mut masked_lap: Vec<f64> = lap.iter()
693        .zip(mask.iter())
694        .filter(|(_, &m)| m > 0)
695        .map(|(&l, _)| l)
696        .collect();
697
698    if masked_lap.is_empty() {
699        return w;
700    }
701
702    // Sort for percentile calculation (MATLAB: prctile with linear interpolation)
703    masked_lap.sort_by(|a, b| a.partial_cmp(b).unwrap());
704
705    let thr_min = prctile(&masked_lap, pmin);
706    let thr_max = prctile(&masked_lap, pmax);
707
708    let range = thr_max - thr_min;
709
710    // Apply weights (Equation 7)
711    for i in 0..n_total {
712        if mask[i] == 0 {
713            continue;
714        }
715
716        let l = lap[i];
717
718        if l < thr_min {
719            w[i] = 1.0;
720        } else if l > thr_max {
721            w[i] = 0.0;
722        } else if range > 1e-10 {
723            w[i] = (thr_max - l) / range;
724        }
725    }
726
727    w
728}
729
730/// K-space weights for FastQSM (Equation 10)
731///
732/// Weights based on |D|^n with percentile normalization.
733fn dipole_kspace_weights_ilsqr(
734    d: &[f64],
735    n_exp: f64,
736    pa: f64,
737    pb: f64,
738) -> Vec<f64> {
739    let len = d.len();
740    let mut w = vec![0.0; len];
741
742    // Compute |D|^n
743    for i in 0..len {
744        w[i] = d[i].abs().powf(n_exp);
745    }
746
747    // Percentile on ALL values (matching MATLAB: prctile(vec(w), [pa, pb]))
748    let mut vals: Vec<f64> = w.to_vec();
749    vals.sort_by(|a, b| a.partial_cmp(b).unwrap());
750
751    if vals.is_empty() {
752        return vec![0.0; len];
753    }
754
755    let ab_min = prctile(&vals, pa);
756    let ab_max = prctile(&vals, pb);
757
758    let range = ab_max - ab_min;
759
760    // Normalize to [0, 1]
761    for i in 0..len {
762        if range > 1e-20 {
763            w[i] = (w[i] - ab_min) / range;
764        }
765        w[i] = w[i].max(0.0).min(1.0);
766    }
767
768    w
769}
770
771/// Mask-adaptive forward gradient (matching MATLAB's gradfm_mex)
772///
773/// For masked voxels: uses forward difference where forward neighbor is in mask,
774/// falls back to backward difference, or 0 if neither neighbor is in mask.
775/// Outside mask: gradient is 0.
776fn fgrad_masked(
777    f: &[f64],
778    mask: &[u8],
779    nx: usize, ny: usize, nz: usize,
780    vsx: f64, vsy: f64, vsz: f64,
781) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
782    let n_total = nx * ny * nz;
783    let mut dx = vec![0.0; n_total];
784    let mut dy = vec![0.0; n_total];
785    let mut dz = vec![0.0; n_total];
786
787    let hx = 1.0 / vsx;
788    let hy = 1.0 / vsy;
789    let hz = 1.0 / vsz;
790
791    let nxny = nx * ny;
792
793    for k in 0..nz {
794        for j in 0..ny {
795            let jk = j * nx + k * nxny;
796            for i in 0..nx {
797                let l = i + jk;
798                if mask[l] == 0 { continue; }
799
800                // X-axis: forward if possible, else backward, else 0
801                dx[l] = if i < nx - 1 && mask[l + 1] != 0 {
802                    hx * (f[l + 1] - f[l])
803                } else if i > 0 && mask[l - 1] != 0 {
804                    hx * (f[l] - f[l - 1])
805                } else {
806                    0.0
807                };
808
809                // Y-axis
810                dy[l] = if j < ny - 1 && mask[l + nx] != 0 {
811                    hy * (f[l + nx] - f[l])
812                } else if j > 0 && mask[l - nx] != 0 {
813                    hy * (f[l] - f[l - nx])
814                } else {
815                    0.0
816                };
817
818                // Z-axis
819                dz[l] = if k < nz - 1 && mask[l + nxny] != 0 {
820                    hz * (f[l + nxny] - f[l])
821                } else if k > 0 && mask[l - nxny] != 0 {
822                    hz * (f[l] - f[l - nxny])
823                } else {
824                    0.0
825                };
826            }
827        }
828    }
829
830    (dx, dy, dz)
831}
832
833/// Gradient weights for streaking artifact estimation (Equation 15)
834fn gradient_weights_ilsqr(
835    x: &[f64],
836    mask: &[u8],
837    nx: usize, ny: usize, nz: usize,
838    vsx: f64, vsy: f64, vsz: f64,
839    pmin: f64,
840    pmax: f64,
841) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
842    // MATLAB uses gradf(x, mask, vsz) — mask-adaptive forward differences
843    let (gx, gy, gz) = fgrad_masked(x, mask, nx, ny, nz, vsx, vsy, vsz);
844
845    // Apply percentile-based weights to each component
846    let wx = gradient_weights_component(&gx, mask, pmin, pmax);
847    let wy = gradient_weights_component(&gy, mask, pmin, pmax);
848    let wz = gradient_weights_component(&gz, mask, pmin, pmax);
849
850    (wx, wy, wz)
851}
852
853fn gradient_weights_component(
854    g: &[f64],
855    mask: &[u8],
856    pmin: f64,
857    pmax: f64,
858) -> Vec<f64> {
859    let len = g.len();
860    let mut w = vec![0.0; len];
861
862    // Collect masked gradient values
863    let mut masked_g: Vec<f64> = g.iter()
864        .zip(mask.iter())
865        .filter(|(_, &m)| m > 0)
866        .map(|(&v, _)| v)
867        .collect();
868
869    if masked_g.is_empty() {
870        return w;
871    }
872
873    masked_g.sort_by(|a, b| a.partial_cmp(b).unwrap());
874
875    let thr_min = prctile(&masked_g, pmin);
876    let thr_max = prctile(&masked_g, pmax);
877
878    let range = thr_max - thr_min;
879
880    for i in 0..len {
881        if mask[i] == 0 {
882            continue;
883        }
884
885        let v = g[i];
886
887        if v < thr_min {
888            w[i] = 1.0;
889        } else if v > thr_max {
890            w[i] = 0.0;
891        } else if range > 1e-10 {
892            w[i] = (thr_max - v) / range;
893        }
894
895        // Apply mask
896        w[i] *= mask[i] as f64;
897    }
898
899    w
900}
901
902// ============================================================================
903// Helper Functions
904// ============================================================================
905
906fn norm(x: &[f64]) -> f64 {
907    x.iter().map(|&v| v * v).sum::<f64>().sqrt()
908}
909
910fn scale_inplace(x: &mut [f64], s: f64) {
911    for v in x.iter_mut() {
912        *v *= s;
913    }
914}
915
916fn axpy(y: &mut [f64], a: f64, x: &[f64]) {
917    for (yi, &xi) in y.iter_mut().zip(x.iter()) {
918        *yi += a * xi;
919    }
920}
921
922fn multiply_elementwise(a: &[f64], b: &[f64]) -> Vec<f64> {
923    a.iter().zip(b.iter()).map(|(&ai, &bi)| ai * bi).collect()
924}
925
926fn sign_array(x: &[f64]) -> Vec<f64> {
927    x.iter().map(|&v| {
928        if v > 0.0 { 1.0 }
929        else if v < 0.0 { -1.0 }
930        else { 0.0 }
931    }).collect()
932}
933
934/// Percentile with linear interpolation (matching MATLAB's prctile)
935///
936/// Input must be a sorted slice. Returns the p-th percentile (p in [0, 100]).
937fn prctile(sorted: &[f64], p: f64) -> f64 {
938    let n = sorted.len();
939    if n == 0 { return 0.0; }
940    if n == 1 { return sorted[0]; }
941    let h = (p / 100.0) * (n - 1) as f64;
942    let lo = h.floor() as usize;
943    let hi = (lo + 1).min(n - 1);
944    let frac = h - lo as f64;
945    sorted[lo] + frac * (sorted[hi] - sorted[lo])
946}
947
948// ============================================================================
949// Step 1: Initial LSQR Solution
950// ============================================================================
951
952/// Step 1: Initial LSQR solution with Laplacian weights
953fn lsqr_step(
954    f: &[f64],
955    mask: &[u8],
956    d: &[f64],
957    nx: usize, ny: usize, nz: usize,
958    vsx: f64, vsy: f64, vsz: f64,
959    workspace: &mut Fft3dWorkspace,
960) -> Vec<f64> {
961
962    // Laplacian weight parameters (from QSM.m)
963    let pmin = 60.0;
964    let pmax = 99.9;
965    let tol_lsqr = 0.01;
966    let maxit_lsqr = 50;
967
968    // Compute Laplacian weights (Equation 7)
969    let w = laplacian_weights_ilsqr(f, mask, nx, ny, nz, vsx, vsy, vsz, pmin, pmax);
970
971    // Compute b = D * FFT(w .* f) - b is COMPLEX
972    let wf: Vec<Complex64> = w.iter().zip(f.iter())
973        .map(|(&wi, &fi)| Complex64::new(wi * fi, 0.0))
974        .collect();
975
976    let mut wf_fft = wf.clone();
977    workspace.fft3d(&mut wf_fft);
978
979    // b = D .* FFT(w .* f) - keep as complex!
980    let b: Vec<Complex64> = wf_fft.iter().zip(d.iter())
981        .map(|(wfi, &di)| wfi * di)
982        .collect();
983
984    // Define A*x operator: D * FFT(w .* real(IFFT(D .* x)))
985    // Works with complex vectors throughout
986    // Reuse a single workspace across all LSQR iterations to avoid repeated allocation
987    let lsqr_ws = RefCell::new(Fft3dWorkspace::new(nx, ny, nz));
988    let apply_a = |x: &[Complex64]| -> Vec<Complex64> {
989        // D .* x (in k-space) - x is complex, D is real
990        let dx: Vec<Complex64> = x.iter().zip(d.iter())
991            .map(|(xi, &di)| xi * di)
992            .collect();
993
994        // IFFT(D .* x)
995        let mut dx_ifft = dx.clone();
996        let mut ws = lsqr_ws.borrow_mut();
997        ws.ifft3d(&mut dx_ifft);
998
999        // w .* real(IFFT(D .* x)) - take real part here as per MATLAB reference
1000        let wdx: Vec<Complex64> = w.iter().zip(dx_ifft.iter())
1001            .map(|(&wi, dxi)| Complex64::new(wi * dxi.re, 0.0))
1002            .collect();
1003
1004        // FFT(w .* ...)
1005        let mut wdx_fft = wdx.clone();
1006        ws.fft3d(&mut wdx_fft);
1007
1008        // D .* FFT(...)
1009        wdx_fft.iter().zip(d.iter())
1010            .map(|(wdxi, &di)| wdxi * di)
1011            .collect()
1012    };
1013
1014    // A^H is same as A for this Hermitian operator (D is real, w is real)
1015    let apply_ah = |x: &[Complex64]| -> Vec<Complex64> {
1016        apply_a(x)
1017    };
1018
1019    // Solve with complex LSQR
1020    let x_lsqr = lsqr_complex(apply_a, apply_ah, &b, tol_lsqr, maxit_lsqr, false);
1021
1022    // IFFT to get result in image space
1023    let mut x_ifft = x_lsqr;
1024    workspace.ifft3d(&mut x_ifft);
1025
1026    // Apply mask and take real part
1027    x_ifft.iter().zip(mask.iter())
1028        .map(|(xi, &mi)| if mi > 0 { xi.re } else { 0.0 })
1029        .collect()
1030}
1031
1032// ============================================================================
1033// Step 2: FastQSM
1034// ============================================================================
1035
1036/// Step 2: FastQSM estimate
1037fn fastqsm_step(
1038    f: &[f64],
1039    mask: &[u8],
1040    d: &[f64],
1041    nx: usize, ny: usize, nz: usize,
1042    vsx: f64, vsy: f64, vsz: f64,
1043    workspace: &mut Fft3dWorkspace,
1044) -> Vec<f64> {
1045    let n_total = nx * ny * nz;
1046
1047    // FFT of field
1048    let f_complex: Vec<Complex64> = f.iter()
1049        .map(|&v| Complex64::new(v, 0.0))
1050        .collect();
1051
1052    let mut f_fft = f_complex;
1053    workspace.fft3d(&mut f_fft);
1054
1055    // Equation (8): x = sign(D) .* F
1056    let sign_d = sign_array(d);
1057    let x: Vec<Complex64> = f_fft.iter().zip(sign_d.iter())
1058        .map(|(fi, &si)| fi * si)
1059        .collect();
1060
1061    // K-space weights (Equation 10)
1062    let pa = 1.0;
1063    let pb = 30.0;
1064    let n_exp = 0.001;
1065    let wfs = dipole_kspace_weights_ilsqr(d, n_exp, pa, pb);
1066
1067    // SMV kernel for smoothing (Equation 9)
1068    let r_smv = 3.0;
1069    let smv_grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
1070    let h = smv_kernel(&smv_grid, r_smv);
1071
1072    // FFT of SMV kernel — take real part to match MATLAB: real(fft3(ifftshift(h)))
1073    let h_complex: Vec<Complex64> = h.iter()
1074        .map(|&v| Complex64::new(v, 0.0))
1075        .collect();
1076    let mut h_fft_complex = h_complex;
1077    workspace.fft3d(&mut h_fft_complex);
1078    let h_fft: Vec<f64> = h_fft_complex.iter().map(|c| c.re).collect();
1079
1080    // Equation (9): Apply weighted combination
1081    // x = FFT(mask .* IFFT(wfs .* x + (1-wfs) .* (h .* x)))
1082    let mut x_filtered: Vec<Complex64> = x.iter()
1083        .zip(wfs.iter())
1084        .zip(h_fft.iter())
1085        .map(|((xi, &wi), &hi)| {
1086            xi * wi + xi * hi * (1.0 - wi)
1087        })
1088        .collect();
1089
1090    workspace.ifft3d(&mut x_filtered);
1091
1092    // Apply mask
1093    for (xi, &mi) in x_filtered.iter_mut().zip(mask.iter()) {
1094        if mi == 0 {
1095            *xi = Complex64::new(0.0, 0.0);
1096        } else {
1097            *xi = Complex64::new(xi.re, 0.0);
1098        }
1099    }
1100
1101    workspace.fft3d(&mut x_filtered);
1102
1103    // Equation (11): Apply again
1104    let mut x_filtered2: Vec<Complex64> = x_filtered.iter()
1105        .zip(wfs.iter())
1106        .zip(h_fft.iter())
1107        .map(|((xi, &wi), &hi)| {
1108            xi * wi + xi * hi * (1.0 - wi)
1109        })
1110        .collect();
1111
1112    workspace.ifft3d(&mut x_filtered2);
1113
1114    let x_fs: Vec<f64> = x_filtered2.iter().zip(mask.iter())
1115        .map(|(xi, &mi)| if mi > 0 { xi.re } else { 0.0 })
1116        .collect();
1117
1118    // Equation (12): TKD for comparison
1119    let t0 = 1.0 / 8.0;
1120    let mut inv_d = vec![0.0; n_total];
1121    for i in 0..n_total {
1122        if d[i].abs() < t0 {
1123            inv_d[i] = d[i].signum() / t0;
1124        } else {
1125            inv_d[i] = 1.0 / d[i];
1126        }
1127    }
1128
1129    let x_tkd_fft: Vec<Complex64> = f_fft.iter().zip(inv_d.iter())
1130        .map(|(fi, &idi)| fi * idi)
1131        .collect();
1132
1133    let mut x_tkd_complex = x_tkd_fft;
1134    workspace.ifft3d(&mut x_tkd_complex);
1135
1136    let x_tkd: Vec<f64> = x_tkd_complex.iter().zip(mask.iter())
1137        .map(|(xi, &mi)| if mi > 0 { xi.re } else { 0.0 })
1138        .collect();
1139
1140    // Equations (13-14): Linear regression to scale FastQSM
1141    // Solve: xtkd ≈ a * xfs + b
1142    // MATLAB reference uses ALL voxels (including zeros outside mask) for the regression
1143    let sum_xfs: f64 = x_fs.iter().map(|&v| v).sum();
1144    let sum_xtkd: f64 = x_tkd.iter().map(|&v| v).sum();
1145    let sum_xfs2: f64 = x_fs.iter().map(|&v| v * v).sum();
1146    let sum_xfs_xtkd: f64 = x_fs.iter().zip(x_tkd.iter())
1147        .map(|(&xf, &xt)| xf * xt)
1148        .sum();
1149
1150    let n_all: f64 = n_total as f64;
1151
1152    // Solve 2x2 system: [sum_xfs2, sum_xfs; sum_xfs, n] * [a; b] = [sum_xfs_xtkd; sum_xtkd]
1153    let det = sum_xfs2 * n_all - sum_xfs * sum_xfs;
1154
1155    let (a, b) = if det.abs() > 1e-20 {
1156        let a = (n_all * sum_xfs_xtkd - sum_xfs * sum_xtkd) / det;
1157        let b = (sum_xfs2 * sum_xtkd - sum_xfs * sum_xfs_xtkd) / det;
1158        (a, b)
1159    } else {
1160        (1.0, 0.0)
1161    };
1162
1163    // Equation (14): x = a * xfs + b
1164    x_fs.iter().zip(mask.iter())
1165        .map(|(&xf, &mi)| if mi > 0 { a * xf + b } else { 0.0 })
1166        .collect()
1167}
1168
1169// ============================================================================
1170// Step 3: Streaking Artifact Estimation
1171// ============================================================================
1172
1173/// Step 3: Estimate streaking artifacts using LSMR
1174fn susceptibility_artifacts_step(
1175    x0: &[f64],
1176    xfs: &[f64],
1177    mask: &[u8],
1178    d: &[f64],
1179    nx: usize, ny: usize, nz: usize,
1180    vsx: f64, vsy: f64, vsz: f64,
1181    tol: f64,
1182    maxit: usize,
1183    _workspace: &mut Fft3dWorkspace,
1184) -> Vec<f64> {
1185    let n_total = nx * ny * nz;
1186
1187    // Gradient weights (Equation 15)
1188    let pmin = 50.0;
1189    let pmax = 70.0;
1190    let (wx, wy, wz) = gradient_weights_ilsqr(xfs, mask, nx, ny, nz, vsx, vsy, vsz, pmin, pmax);
1191
1192    // Ill-conditioned mask (Equation 4)
1193    let thr = 0.1;
1194    let mic: Vec<f64> = d.iter().map(|&di| if di.abs() < thr { 1.0 } else { 0.0 }).collect();
1195
1196    // Compute gradient of x0 (Equation 3)
1197    let step3_grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
1198    let (dx, dy, dz) = fgrad(x0, &step3_grid);
1199
1200    // b = [wx .* dx; wy .* dy; wz .* dz] (concatenated)
1201    let bx = multiply_elementwise(&wx, &dx);
1202    let by = multiply_elementwise(&wy, &dy);
1203    let bz = multiply_elementwise(&wz, &dz);
1204
1205    let mut b = Vec::with_capacity(3 * n_total);
1206    b.extend_from_slice(&bx);
1207    b.extend_from_slice(&by);
1208    b.extend_from_slice(&bz);
1209
1210    // Define forward operator A and adjoint A^T
1211    // Reuse a single workspace across all LSMR iterations to avoid repeated allocation
1212    let lsmr_ws = RefCell::new(Fft3dWorkspace::new(nx, ny, nz));
1213    let apply_a = |x_in: &[f64]| -> Vec<f64> {
1214        // x_in is in image space
1215        // Apply Mic in k-space
1216        let x_complex: Vec<Complex64> = x_in.iter()
1217            .map(|&v| Complex64::new(v, 0.0))
1218            .collect();
1219
1220        let mut x_fft = x_complex;
1221        let mut ws = lsmr_ws.borrow_mut();
1222        ws.fft3d(&mut x_fft);
1223
1224        // Apply ill-conditioned mask
1225        let x_mic: Vec<Complex64> = x_fft.iter().zip(mic.iter())
1226            .map(|(xi, &mi)| xi * mi)
1227            .collect();
1228
1229        let mut x_ifft = x_mic;
1230        ws.ifft3d(&mut x_ifft);
1231
1232        let x_filtered: Vec<f64> = x_ifft.iter().map(|xi| xi.re).collect();
1233
1234        // Compute gradient
1235        let (gx, gy, gz) = fgrad(&x_filtered, &step3_grid);
1236
1237        // Apply weights and concatenate
1238        let mut result = Vec::with_capacity(3 * n_total);
1239        result.extend(wx.iter().zip(gx.iter()).map(|(&w, &g)| w * g));
1240        result.extend(wy.iter().zip(gy.iter()).map(|(&w, &g)| w * g));
1241        result.extend(wz.iter().zip(gz.iter()).map(|(&w, &g)| w * g));
1242
1243        result
1244    };
1245
1246    // Define adjoint operator A^T
1247    let apply_at = |y_in: &[f64]| -> Vec<f64> {
1248        // y_in is [yx; yy; yz] concatenated (3 * n_total)
1249        let yx = &y_in[0..n_total];
1250        let yy = &y_in[n_total..2*n_total];
1251        let yz = &y_in[2*n_total..3*n_total];
1252
1253        // Apply weights
1254        let wyx: Vec<f64> = wx.iter().zip(yx.iter()).map(|(&w, &y)| w * y).collect();
1255        let wyy: Vec<f64> = wy.iter().zip(yy.iter()).map(|(&w, &y)| w * y).collect();
1256        let wyz: Vec<f64> = wz.iter().zip(yz.iter()).map(|(&w, &y)| w * y).collect();
1257
1258        // Adjoint of forward gradient = -div (bdiv returns +div, so negate)
1259        // MATLAB's gradfp_adj_mex uses h = -1/voxel_size, including the negation.
1260        // Rust's bdiv uses h = +1/voxel_size, so we negate here.
1261        let div = bdiv(&wyx, &wyy, &wyz, &step3_grid);
1262
1263        // Apply Mic in k-space
1264        let div_complex: Vec<Complex64> = div.iter()
1265            .map(|&v| Complex64::new(-v, 0.0))
1266            .collect();
1267
1268        let mut div_fft = div_complex;
1269        let mut ws = lsmr_ws.borrow_mut();
1270        ws.fft3d(&mut div_fft);
1271
1272        let div_mic: Vec<Complex64> = div_fft.iter().zip(mic.iter())
1273            .map(|(di, &mi)| di * mi)
1274            .collect();
1275
1276        let mut div_ifft = div_mic;
1277        ws.ifft3d(&mut div_ifft);
1278
1279        div_ifft.iter().map(|di| di.re).collect()
1280    };
1281
1282    // Solve with LSMR
1283    let xsa = lsmr(apply_a, apply_at, &b, n_total, tol, tol, maxit, false);
1284
1285    // Apply mask
1286    xsa.iter().zip(mask.iter())
1287        .map(|(&x, &m)| if m > 0 { x } else { 0.0 })
1288        .collect()
1289}
1290
1291// ============================================================================
1292// Main iLSQR Algorithm
1293// ============================================================================
1294
1295/// iLSQR: A method for estimating and removing streaking artifacts in QSM
1296///
1297/// # Arguments
1298/// * `field` - Unwrapped local field/tissue phase (nx * ny * nz)
1299/// * `mask` - Binary mask of region of interest
1300/// * `grid` - Volume grid (dimensions and voxel sizes)
1301/// * `bdir` - B0 field direction (bx, by, bz)
1302/// * `params` - iLSQR parameters (tol, max_iter)
1303/// * `progress` - Progress callback `(step, total_steps)`
1304///
1305/// # Returns
1306/// Tuple of (susceptibility, streaking_artifacts, fast_qsm, initial_lsqr)
1307pub fn ilsqr(
1308    field: &[f64],
1309    mask: &[u8],
1310    grid: &Grid,
1311    bdir: (f64, f64, f64),
1312    params: &IlsqrParams,
1313    mut progress: impl FnMut(usize, usize),
1314) -> (Vec<f64>, Vec<f64>, Vec<f64>, Vec<f64>) {
1315    let (nx, ny, nz) = grid.dims;
1316    let (vsx, vsy, vsz) = grid.voxel_size;
1317
1318    // Generate dipole kernel
1319    let d = dipole_kernel(grid, bdir);
1320
1321    // Create FFT workspace
1322    let mut workspace = Fft3dWorkspace::new(nx, ny, nz);
1323
1324    progress(1, 4);
1325
1326    // Step 1: Initial LSQR solution
1327    let xlsqr = lsqr_step(field, mask, &d, nx, ny, nz, vsx, vsy, vsz, &mut workspace);
1328
1329    progress(2, 4);
1330
1331    // Step 2: FastQSM estimate
1332    let xfs = fastqsm_step(field, mask, &d, nx, ny, nz, vsx, vsy, vsz, &mut workspace);
1333
1334    progress(3, 4);
1335
1336    // Step 3: Estimate streaking artifacts
1337    let xsa = susceptibility_artifacts_step(
1338        &xlsqr, &xfs, mask, &d,
1339        nx, ny, nz, vsx, vsy, vsz,
1340        params.tol, params.max_iter, &mut workspace
1341    );
1342
1343    progress(4, 4);
1344
1345    // Step 4: Subtract artifacts
1346    let chi: Vec<f64> = xlsqr.iter().zip(xsa.iter()).zip(mask.iter())
1347        .map(|((&xl, &xs), &m)| if m > 0 { xl - xs } else { 0.0 })
1348        .collect();
1349
1350    (chi, xsa, xfs, xlsqr)
1351}
1352
1353#[cfg(test)]
1354mod tests {
1355    use super::*;
1356
1357    #[test]
1358    fn test_lsqr_simple() {
1359        // Test LSQR on a simple diagonal system
1360        let n = 10;
1361        let diag: Vec<f64> = (1..=n).map(|i| i as f64).collect();
1362        let b: Vec<f64> = diag.iter().map(|&d| d * 2.0).collect();  // x = [2, 2, 2, ...]
1363
1364        let apply_a = |x: &[f64]| -> Vec<f64> {
1365            x.iter().zip(diag.iter()).map(|(&xi, &di)| xi * di).collect()
1366        };
1367
1368        let x = lsqr(apply_a, apply_a, &b, 1e-10, 100);
1369
1370        for (i, &xi) in x.iter().enumerate() {
1371            assert!((xi - 2.0).abs() < 1e-6, "x[{}] = {}, expected 2.0", i, xi);
1372        }
1373    }
1374
1375    #[test]
1376    fn test_norm() {
1377        let x = vec![3.0, 4.0];
1378        assert!((norm(&x) - 5.0).abs() < 1e-10);
1379    }
1380
1381    #[test]
1382    fn test_sign_array() {
1383        let x = vec![-2.0, 0.0, 3.0];
1384        let s = sign_array(&x);
1385        assert_eq!(s, vec![-1.0, 0.0, 1.0]);
1386    }
1387
1388    #[test]
1389    fn test_lsqr_complex_relres_stop() {
1390        // Regression for the iLSQR Step-1 fix: lsqr_complex must stop on the
1391        // simple relative-residual criterion `||r||/||b|| <= tol` (matching
1392        // MATLAB's built-in `lsqr` that kamesy's ilsqr.m Step 1 calls), NOT the
1393        // augmented Paige-Saunders `rtol = btol + atol*||A||*||x||/||b||` test,
1394        // which previously tripped early and left Step 1 under-converged.
1395        //
1396        // Use an ill-conditioned diagonal A = diag(1, 10, 100, 1000) where the
1397        // augmented test (with its ||A||*||x|| term) would stop well before the
1398        // residual actually reaches tol. With the correct test, the residual
1399        // must fall to <= tol.
1400        let diag = vec![1.0, 10.0, 100.0, 1000.0];
1401        let expected: Vec<Complex64> =
1402            diag.iter().map(|&d| Complex64::new(d, 0.0)).collect();
1403        let b: Vec<Complex64> = expected
1404            .iter()
1405            .zip(diag.iter())
1406            .map(|(&xi, &di)| xi * di)
1407            .collect();
1408
1409        let da = diag.clone();
1410        let apply = move |x: &[Complex64]| -> Vec<Complex64> {
1411            x.iter().zip(da.iter()).map(|(&xi, &di)| xi * di).collect()
1412        };
1413        let apply2 = apply.clone();
1414        let tol = 1e-6;
1415        let x = lsqr_complex(apply, apply2, &b, tol, 200, false);
1416
1417        // Residual ||A*x - b|| / ||b|| must be at or below tol.
1418        let ax: Vec<Complex64> =
1419            x.iter().zip(diag.iter()).map(|(&xi, &di)| xi * di).collect();
1420        let r: f64 = ax
1421            .iter()
1422            .zip(b.iter())
1423            .map(|(a, bb)| (a - bb).norm_sqr())
1424            .sum::<f64>()
1425            .sqrt();
1426        let bn: f64 = b.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
1427        assert!(
1428            r / bn <= tol * 10.0,
1429            "relres {} should reach ~tol {}; premature-stop regression",
1430            r / bn,
1431            tol
1432        );
1433    }
1434
1435    #[test]
1436    fn test_lsqr_complex_diagonal() {
1437        // Test complex LSQR on a diagonal system: A = diag(1, 2, 3), b = [1+i, 4+2i, 9+3i]
1438        // Expected solution: x = [1+i, 2+i, 3+i]
1439        let diag = vec![1.0, 2.0, 3.0];
1440        let expected = vec![
1441            Complex64::new(1.0, 1.0),
1442            Complex64::new(2.0, 1.0),
1443            Complex64::new(3.0, 1.0),
1444        ];
1445        let b: Vec<Complex64> = expected.iter().zip(diag.iter())
1446            .map(|(&xi, &di)| xi * di)
1447            .collect();
1448
1449        let diag_a = diag.clone();
1450        let diag_ah = diag.clone();
1451        let apply_a = move |x: &[Complex64]| -> Vec<Complex64> {
1452            x.iter().zip(diag_a.iter()).map(|(&xi, &di)| xi * di).collect()
1453        };
1454        let apply_ah = move |x: &[Complex64]| -> Vec<Complex64> {
1455            x.iter().zip(diag_ah.iter()).map(|(&xi, &di)| xi * di).collect()
1456        };
1457
1458        let x = lsqr_complex(apply_a, apply_ah, &b, 1e-10, 100, false);
1459
1460        for (i, (xi, ei)) in x.iter().zip(expected.iter()).enumerate() {
1461            assert!((xi.re - ei.re).abs() < 1e-6,
1462                "x[{}].re = {}, expected {}", i, xi.re, ei.re);
1463            assert!((xi.im - ei.im).abs() < 1e-6,
1464                "x[{}].im = {}, expected {}", i, xi.im, ei.im);
1465        }
1466    }
1467
1468    #[test]
1469    fn test_lsmr_diagonal() {
1470        // Test the LSMR solver (inside ilsqr.rs) exercises all code paths.
1471        // Use a well-conditioned diagonal system: A = diag(1, 1, 1) (identity)
1472        // b = [3, 5, 7], expected x = [3, 5, 7]
1473        let b = vec![3.0, 5.0, 7.0];
1474
1475        let apply_a = |x: &[f64]| -> Vec<f64> { x.to_vec() };
1476        let apply_at = |x: &[f64]| -> Vec<f64> { x.to_vec() };
1477
1478        let x = lsmr(apply_a, apply_at, &b, 3, 1e-6, 1e-6, 200, false);
1479
1480        // Verify that the solver returns finite values and the output has correct length
1481        assert_eq!(x.len(), 3);
1482        for (i, &xi) in x.iter().enumerate() {
1483            assert!(xi.is_finite(), "x[{}] = {} is not finite", i, xi);
1484        }
1485
1486        // Compute residual: ||Ax - b|| should be reduced from ||b||
1487        let residual: f64 = x.iter().zip(b.iter())
1488            .map(|(&xi, &bi)| (xi - bi).powi(2))
1489            .sum::<f64>()
1490            .sqrt();
1491        let bnorm: f64 = b.iter().map(|&bi| bi * bi).sum::<f64>().sqrt();
1492        assert!(residual < bnorm,
1493            "residual {} should be less than ||b|| = {}", residual, bnorm);
1494    }
1495
1496    #[test]
1497    fn test_laplacian_weights() {
1498        // Test laplacian_weights_ilsqr on a small 4x4x4 volume with a uniform field
1499        // inside a mask. A constant field has zero Laplacian, so weights should be 1.0.
1500        let (nx, ny, nz) = (4, 4, 4);
1501        let n_total = nx * ny * nz;
1502        let mut mask = vec![0u8; n_total];
1503        let mut field = vec![0.0; n_total];
1504
1505        // Create a sphere mask and constant field inside
1506        for k in 0..nz {
1507            for j in 0..ny {
1508                for i in 0..nx {
1509                    let idx = i + j * nx + k * nx * ny;
1510                    let ci = i as f64 - 1.5;
1511                    let cj = j as f64 - 1.5;
1512                    let ck = k as f64 - 1.5;
1513                    let r2 = ci * ci + cj * cj + ck * ck;
1514                    if r2 < 2.5 {
1515                        mask[idx] = 1;
1516                        field[idx] = 5.0; // constant field => Laplacian is 0
1517                    }
1518                }
1519            }
1520        }
1521
1522        let w = laplacian_weights_ilsqr(&field, &mask, nx, ny, nz, 1.0, 1.0, 1.0, 10.0, 90.0);
1523
1524        // All weights should be finite and in [0, 1]
1525        for (i, &wi) in w.iter().enumerate() {
1526            assert!(wi.is_finite(), "weight[{}] is not finite", i);
1527            assert!(wi >= 0.0 && wi <= 1.0, "weight[{}] = {} out of [0,1]", i, wi);
1528        }
1529
1530        // Masked-out voxels should have weight 0
1531        for i in 0..n_total {
1532            if mask[i] == 0 {
1533                assert_eq!(w[i], 0.0, "weight outside mask should be 0 at index {}", i);
1534            }
1535        }
1536    }
1537
1538    #[test]
1539    fn test_dipole_kspace_weights() {
1540        // Test dipole_kspace_weights_ilsqr with synthetic dipole values
1541        let d = vec![0.0, 0.01, 0.1, 0.3, 0.5, 0.7, 1.0, -0.5, -1.0, 0.0];
1542
1543        let w = dipole_kspace_weights_ilsqr(&d, 1.0, 1.0, 90.0);
1544
1545        // All weights should be in [0, 1]
1546        for (i, &wi) in w.iter().enumerate() {
1547            assert!(wi >= 0.0 && wi <= 1.0,
1548                "weight[{}] = {} out of [0,1]", i, wi);
1549        }
1550
1551        // Zero dipole values should produce weight 0 (or very small) since |0|^n = 0
1552        assert!(w[0] <= 1e-10, "weight at D=0 should be ~0, got {}", w[0]);
1553
1554        // The largest |D| values should have weight near 1.0
1555        // d[6]=1.0 and d[8]=-1.0 have the largest |D|
1556        assert!(w[6] > 0.5, "weight at |D|=1.0 should be large, got {}", w[6]);
1557        assert!(w[8] > 0.5, "weight at |D|=1.0 should be large, got {}", w[8]);
1558    }
1559
1560    #[test]
1561    fn test_gradient_weights() {
1562        // Test gradient_weights_ilsqr on a small 4x4x4 volume
1563        let (nx, ny, nz) = (4, 4, 4);
1564        let n_total = nx * ny * nz;
1565
1566        // Create a mask (all ones for simplicity)
1567        let mask = vec![1u8; n_total];
1568
1569        // Create a field with a linear gradient in x
1570        let mut field = vec![0.0; n_total];
1571        for k in 0..nz {
1572            for j in 0..ny {
1573                for i in 0..nx {
1574                    let idx = i + j * nx + k * nx * ny;
1575                    field[idx] = i as f64; // linear in x
1576                }
1577            }
1578        }
1579
1580        let (wx, wy, wz) = gradient_weights_ilsqr(
1581            &field, &mask, nx, ny, nz, 1.0, 1.0, 1.0, 10.0, 90.0
1582        );
1583
1584        // All weights should be finite and in [0, 1]
1585        for i in 0..n_total {
1586            assert!(wx[i].is_finite(), "wx[{}] is not finite", i);
1587            assert!(wy[i].is_finite(), "wy[{}] is not finite", i);
1588            assert!(wz[i].is_finite(), "wz[{}] is not finite", i);
1589            assert!(wx[i] >= 0.0 && wx[i] <= 1.0, "wx[{}] = {} out of [0,1]", i, wx[i]);
1590            assert!(wy[i] >= 0.0 && wy[i] <= 1.0, "wy[{}] = {} out of [0,1]", i, wy[i]);
1591            assert!(wz[i] >= 0.0 && wz[i] <= 1.0, "wz[{}] = {} out of [0,1]", i, wz[i]);
1592        }
1593
1594        // The y and z gradients are zero for this field, so wy and wz should reflect
1595        // that all gradient values are identical (zero). Check they are well-defined.
1596        let wy_sum: f64 = wy.iter().sum();
1597        let wz_sum: f64 = wz.iter().sum();
1598        assert!(wy_sum.is_finite(), "wy sum is not finite");
1599        assert!(wz_sum.is_finite(), "wz sum is not finite");
1600    }
1601
1602    #[test]
1603    fn test_ilsqr_small() {
1604        // Run ilsqr_simple on a small 8x8x8 volume with a sphere mask
1605        // and synthetic local field data. This exercises the full pipeline:
1606        // lsqr_step, fastqsm_step, susceptibility_artifacts_step.
1607        let (nx, ny, nz) = (8, 8, 8);
1608        let n_total = nx * ny * nz;
1609        let vsx = 1.0;
1610        let vsy = 1.0;
1611        let vsz = 1.0;
1612        let bdir = (0.0, 0.0, 1.0);
1613
1614        // Create a sphere mask centered in the volume
1615        let mut mask = vec![0u8; n_total];
1616        let cx = (nx as f64 - 1.0) / 2.0;
1617        let cy = (ny as f64 - 1.0) / 2.0;
1618        let cz = (nz as f64 - 1.0) / 2.0;
1619        let radius = 3.0;
1620
1621        for k in 0..nz {
1622            for j in 0..ny {
1623                for i in 0..nx {
1624                    let idx = i + j * nx + k * nx * ny;
1625                    let di = i as f64 - cx;
1626                    let dj = j as f64 - cy;
1627                    let dk = k as f64 - cz;
1628                    if di * di + dj * dj + dk * dk < radius * radius {
1629                        mask[idx] = 1;
1630                    }
1631                }
1632            }
1633        }
1634
1635        // Create synthetic local field: a simple dipole-like pattern
1636        // Use a small susceptibility source and forward-model through the dipole kernel
1637        let mut field = vec![0.0; n_total];
1638        for k in 0..nz {
1639            for j in 0..ny {
1640                for i in 0..nx {
1641                    let idx = i + j * nx + k * nx * ny;
1642                    if mask[idx] > 0 {
1643                        let di = i as f64 - cx;
1644                        let dj = j as f64 - cy;
1645                        let dk = k as f64 - cz;
1646                        // Simulate a simple field variation
1647                        field[idx] = 0.01 * (dk * dk - di * di - dj * dj)
1648                            / (di * di + dj * dj + dk * dk + 1.0);
1649                    }
1650                }
1651            }
1652        }
1653
1654        let tol = 0.1;
1655        let maxit = 5; // Few iterations for speed
1656
1657        let grid = Grid::new(nx, ny, nz, vsx, vsy, vsz);
1658        let params = IlsqrParams { tol, max_iter: maxit };
1659        let (chi, _, _, _) = ilsqr(&field, &mask, &grid, bdir, &params, |_, _| {});
1660
1661        // Check output dimensions
1662        assert_eq!(chi.len(), n_total, "output size mismatch");
1663
1664        // Check all values are finite
1665        for (i, &v) in chi.iter().enumerate() {
1666            assert!(v.is_finite(), "chi[{}] = {} is not finite", i, v);
1667        }
1668
1669        // Check mask is respected: outside mask should be zero
1670        for i in 0..n_total {
1671            if mask[i] == 0 {
1672                assert_eq!(chi[i], 0.0, "chi outside mask should be 0 at index {}", i);
1673            }
1674        }
1675
1676        // Check that the result is not all zeros inside the mask
1677        let inside_sum: f64 = chi.iter().zip(mask.iter())
1678            .filter(|(_, &m)| m > 0)
1679            .map(|(&v, _)| v.abs())
1680            .sum();
1681        assert!(inside_sum > 0.0, "chi should not be all zeros inside the mask");
1682    }
1683}