Skip to main content

qsm_core/inversion/
tgv.rs

1//! TGV-QSM: Total Generalized Variation for Quantitative Susceptibility Mapping
2//!
3//! Single-step QSM reconstruction from wrapped phase data using TGV regularization.
4//!
5//! References:
6//! Langkammer, C., Bredies, K., Poser, B.A., et al. (2015).
7//! "Fast quantitative susceptibility mapping using 3D EPI and total generalized variation."
8//! NeuroImage, 111:622-630. https://doi.org/10.1016/j.neuroimage.2015.02.041
9//!
10//! Chatnuntawech, I., McDaniel, P., et al. (2017).
11//! "Single-step quantitative susceptibility mapping with variational penalties."
12//! NMR in Biomedicine, 30(4):e3570. https://doi.org/10.1002/nbm.3570
13//!
14//! Reference implementation: https://github.com/korbinian90/QuantitativeSusceptibilityMappingTGV.jl
15//!
16//! The algorithm solves:
17//!   min_χ ||∇²(phase) - D*χ||₂² + α₁||∇χ - w||₁ + α₀||ε(w)||₁
18//!
19//! where:
20//! - χ is the susceptibility map
21//! - w is an auxiliary vector field (velocity)
22//! - ε(w) is the symmetric gradient of w
23//! - D is the dipole kernel
24//! - α₀, α₁ are TGV regularization parameters
25//!
26//! Optimizations:
27//! - Bounding box reduction: only process the region containing the mask
28//! - Pre-allocated buffers: all temporary arrays allocated once outside the loop
29//! - Early termination: convergence check every 100 iterations
30
31use std::f32::consts::PI;
32use crate::utils::mask::erode_mask;
33#[cfg(feature = "parallel")]
34use rayon::prelude::*;
35
36/// TGV parameters
37#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
38#[derive(Clone, Debug)]
39pub struct TgvParams {
40    /// First-order TGV weight (gradient term)
41    pub alpha1: f32,
42    /// Second-order TGV weight (symmetric gradient term)
43    pub alpha0: f32,
44    /// Number of primal-dual iterations
45    pub iterations: usize,
46    /// Number of mask erosions
47    pub erosions: usize,
48    /// Primal step size multiplier (larger = faster but less stable)
49    pub step_size: f32,
50    /// Field strength in Tesla
51    pub fieldstrength: f32,
52    /// Echo time in seconds
53    pub te: f32,
54    /// Convergence tolerance (relative change in chi)
55    pub tol: f32,
56}
57
58impl Default for TgvParams {
59    fn default() -> Self {
60        Self {
61            // Langkammer et al. TGV-QSM reference regularization (alpha1 = gradient/first-order
62            // weight, alpha0 = second-order weight). The previous 0.001/0.001 roughly doubled the
63            // gradient penalty vs the reference and visibly over-smoothed the susceptibility map
64            // (recovered gradient magnitude ~47% of ground truth on a simulated 7T volume; lowering
65            // alpha1 to the reference restored contrast — xsim 0.33 -> 0.47).
66            alpha1: 0.0005,
67            alpha0: 0.0015,
68            iterations: 1000,
69            erosions: 3,
70            step_size: 3.0,
71            fieldstrength: 3.0,
72            te: 0.020, // 20ms
73            tol: 1e-5,
74        }
75    }
76}
77
78/// Get default alpha values based on regularization level (1-4)
79///
80/// Level 2 (default) gives α₀=0.001, α₁=0.001
81pub fn get_default_alpha(regularization: u8) -> (f32, f32) {
82    match regularization.clamp(1, 4) {
83        1 => (0.0005, 0.0005),
84        2 => (0.001, 0.001),
85        3 => (0.002, 0.003),
86        _ => (0.003, 0.005),
87    }
88}
89
90/// Get default number of iterations based on voxel size and step size.
91///
92/// Matches the Julia reference: `max(1000, 3200 / prod(res)^0.42) / step_size^0.6`
93pub fn get_default_iterations(res: (f32, f32, f32), step_size: f32) -> usize {
94    let prod_res = res.0 * res.1 * res.2;
95    let it = (1000.0_f32).max(3200.0 / prod_res.powf(0.42)) / step_size.powf(0.6);
96    it.round() as usize
97}
98
99/// Bounding box for mask region
100#[derive(Clone, Debug)]
101struct BoundingBox {
102    i_min: usize,
103    i_max: usize,
104    j_min: usize,
105    j_max: usize,
106    k_min: usize,
107    k_max: usize,
108}
109
110impl BoundingBox {
111    /// Find minimal bounding box containing the mask with padding
112    fn from_mask(mask: &[u8], nx: usize, ny: usize, nz: usize, padding: usize) -> Self {
113        let mut i_min = nx;
114        let mut i_max = 0;
115        let mut j_min = ny;
116        let mut j_max = 0;
117        let mut k_min = nz;
118        let mut k_max = 0;
119
120        for k in 0..nz {
121            for j in 0..ny {
122                for i in 0..nx {
123                    if mask[i + j * nx + k * nx * ny] != 0 {
124                        i_min = i_min.min(i);
125                        i_max = i_max.max(i);
126                        j_min = j_min.min(j);
127                        j_max = j_max.max(j);
128                        k_min = k_min.min(k);
129                        k_max = k_max.max(k);
130                    }
131                }
132            }
133        }
134
135        // Add padding and clamp to bounds
136        let i_min = i_min.saturating_sub(padding);
137        let j_min = j_min.saturating_sub(padding);
138        let k_min = k_min.saturating_sub(padding);
139        let i_max = (i_max + padding + 1).min(nx);
140        let j_max = (j_max + padding + 1).min(ny);
141        let k_max = (k_max + padding + 1).min(nz);
142
143        Self { i_min, i_max, j_min, j_max, k_min, k_max }
144    }
145
146    fn dims(&self) -> (usize, usize, usize) {
147        (self.i_max - self.i_min, self.j_max - self.j_min, self.k_max - self.k_min)
148    }
149
150    fn total(&self) -> usize {
151        let (bx, by, bz) = self.dims();
152        bx * by * bz
153    }
154}
155
156/// Extract sub-volume from full volume
157fn extract_subvolume<T: Copy + Default>(
158    full: &[T],
159    bbox: &BoundingBox,
160    nx: usize, ny: usize, _nz: usize,
161) -> Vec<T> {
162    let (bx, by, bz) = bbox.dims();
163    let mut sub = vec![T::default(); bx * by * bz];
164
165    for k in 0..bz {
166        for j in 0..by {
167            for i in 0..bx {
168                let full_idx = (bbox.i_min + i) + (bbox.j_min + j) * nx + (bbox.k_min + k) * nx * ny;
169                let sub_idx = i + j * bx + k * bx * by;
170                sub[sub_idx] = full[full_idx];
171            }
172        }
173    }
174    sub
175}
176
177/// Insert sub-volume back into full volume
178fn insert_subvolume<T: Copy>(
179    full: &mut [T],
180    sub: &[T],
181    bbox: &BoundingBox,
182    nx: usize, ny: usize, _nz: usize,
183) {
184    let (bx, by, bz) = bbox.dims();
185
186    for k in 0..bz {
187        for j in 0..by {
188            for i in 0..bx {
189                let full_idx = (bbox.i_min + i) + (bbox.j_min + j) * nx + (bbox.k_min + k) * nx * ny;
190                let sub_idx = i + j * bx + k * bx * by;
191                full[full_idx] = sub[sub_idx];
192            }
193        }
194    }
195}
196
197/// Compute dipole stencil (27-point spatial kernel)
198pub fn compute_dipole_stencil(
199    res: (f32, f32, f32),
200    b0_dir: (f32, f32, f32),
201) -> [[[f32; 3]; 3]; 3] {
202    let (dx, dy, dz) = res;
203    let (bx, by, bz) = b0_dir;
204
205    // Normalize B0 direction
206    let b_norm = (bx * bx + by * by + bz * bz).sqrt();
207    let bx = bx / b_norm;
208    let by = by / b_norm;
209    let bz = bz / b_norm;
210
211    let mut stencil = [[[0.0f32; 3]; 3]; 3];
212
213    let hx2 = 1.0 / (dx * dx);
214    let hy2 = 1.0 / (dy * dy);
215    let hz2 = 1.0 / (dz * dz);
216    let factor = 1.0 / 3.0;
217
218    // Center (i=1, j=1, k=1)
219    stencil[1][1][1] = -2.0 * (hx2 + hy2 + hz2) * factor
220                     + 2.0 * (bx * bx * hx2 + by * by * hy2 + bz * bz * hz2);
221
222    // X neighbors
223    stencil[0][1][1] = hx2 * factor - bx * bx * hx2;
224    stencil[2][1][1] = hx2 * factor - bx * bx * hx2;
225
226    // Y neighbors
227    stencil[1][0][1] = hy2 * factor - by * by * hy2;
228    stencil[1][2][1] = hy2 * factor - by * by * hy2;
229
230    // Z neighbors
231    stencil[1][1][0] = hz2 * factor - bz * bz * hz2;
232    stencil[1][1][2] = hz2 * factor - bz * bz * hz2;
233
234    // Cross terms for oblique B0
235    let hxy = 1.0 / (dx * dy);
236    let hxz = 1.0 / (dx * dz);
237    let hyz = 1.0 / (dy * dz);
238
239    let xy_factor = -bx * by * hxy;
240    stencil[0][0][1] = xy_factor;
241    stencil[2][2][1] = xy_factor;
242    stencil[0][2][1] = -xy_factor;
243    stencil[2][0][1] = -xy_factor;
244
245    let xz_factor = -bx * bz * hxz;
246    stencil[0][1][0] = xz_factor;
247    stencil[2][1][2] = xz_factor;
248    stencil[0][1][2] = -xz_factor;
249    stencil[2][1][0] = -xz_factor;
250
251    let yz_factor = -by * bz * hyz;
252    stencil[1][0][0] = yz_factor;
253    stencil[1][2][2] = yz_factor;
254    stencil[1][0][2] = -yz_factor;
255    stencil[1][2][0] = -yz_factor;
256
257    stencil
258}
259
260/// Compute SVD-fitted oblique 27-point dipole stencil via DST-based Poisson solves.
261///
262/// Matches the Julia reference implementation from QuantitativeSusceptibilityMappingTGV.jl.
263/// For each unique centrosymmetric pair of the 26 off-center stencil positions, a Poisson
264/// equation is solved using DST-I to obtain Green's function values. These are then fitted
265/// to the analytical dipole field via least squares, and resolution-weighted to produce
266/// the final stencil.
267///
268/// Note: The delta function for offsets (di,dj,dk) and (-di,-dj,-dk) are identical
269/// (both place +1 at mid±I and -2 at mid), so we only compute 13 unique Poisson
270/// solutions and solve a full-rank 13×13 system. This avoids the rank deficiency
271/// that would occur with all 26 positions.
272pub fn compute_oblique_stencil(
273    res: (f32, f32, f32),
274    b0_dir: (f32, f32, f32),
275) -> [[[f32; 3]; 3]; 3] {
276    let n: usize = 64;
277    let singularity_cutout = 4.0_f64;
278    let mid = n / 2; // 32 (0-indexed center)
279    let n2 = n * n;
280    let n3 = n * n * n;
281
282    let (dx, dy, dz) = (res.0 as f64, res.1 as f64, res.2 as f64);
283
284    // Normalize B0 direction
285    let b_norm = ((b0_dir.0 * b0_dir.0 + b0_dir.1 * b0_dir.1 + b0_dir.2 * b0_dir.2) as f64).sqrt();
286    let bdir = (b0_dir.0 as f64 / b_norm, b0_dir.1 as f64 / b_norm, b0_dir.2 as f64 / b_norm);
287
288    // Compute dipole field on 64³ grid
289    let mut d = vec![f64::NAN; n3];
290    let mut d_mask = vec![false; n3];
291
292    for k in 0..n {
293        for j in 0..n {
294            for i in 0..n {
295                let x = i as f64 - mid as f64;
296                let y = j as f64 - mid as f64;
297                let z = k as f64 - mid as f64;
298                let r = (x * x + y * y + z * z).sqrt();
299
300                let idx = i + j * n + k * n2;
301                if r < singularity_cutout {
302                    // d[idx] remains NAN, d_mask[idx] remains false
303                } else {
304                    let xz = (bdir.0 * x + bdir.1 * y + bdir.2 * z) / r;
305                    let kappa = (3.0 * xz * xz - 1.0) / (4.0 * std::f64::consts::PI * r * r * r);
306                    d[idx] = kappa;
307                    d_mask[idx] = true;
308                }
309            }
310        }
311    }
312
313    // DST-I eigenvalue components: coord2[k] = 2 * sin(π*(k+1) / (2*(N+1)))
314    let coord2_sq: Vec<f64> = (0..n)
315        .map(|k| {
316            let v = 2.0 * (std::f64::consts::PI * (k as f64 + 1.0) / (2.0 * (n as f64 + 1.0))).sin();
317            v * v
318        })
319        .collect();
320
321    // 3D eigenvalue grid: coord2_grid[i,j,k] = coord2[i]² + coord2[j]² + coord2[k]²
322    let mut coord2_grid = vec![0.0_f64; n3];
323    for k in 0..n {
324        for j in 0..n {
325            for i in 0..n {
326                coord2_grid[i + j * n + k * n2] = coord2_sq[i] + coord2_sq[j] + coord2_sq[k];
327            }
328        }
329    }
330
331    // Pre-compute DST-I sin table
332    let sin_table = dst_sin_table(n);
333    // Inverse DST-I scale: our DST computes Σ x[n]*sin(...) (no factor of 2),
334    // so the inverse is (2/(N+1))^3 for 3D (unlike FFTW which uses 1/(2*(N+1))^3).
335    let idst_scale = (2.0 / (n as f64 + 1.0)).powi(3);
336
337    // Enumerate all 26 stencil positions (excluding center), column-major order
338    let mut stencil_positions: Vec<(i32, i32, i32)> = Vec::with_capacity(26);
339    for dk in -1..=1_i32 {
340        for dj in -1..=1_i32 {
341            for di in -1..=1_i32 {
342                if di == 0 && dj == 0 && dk == 0 {
343                    continue;
344                }
345                stencil_positions.push((di, dj, dk));
346            }
347        }
348    }
349
350    // Collect valid dipole point indices
351    let valid_indices: Vec<usize> = d_mask
352        .iter()
353        .enumerate()
354        .filter(|(_, &m)| m)
355        .map(|(idx, _)| idx)
356        .collect();
357
358    // Exploit centrosymmetry: delta for (di,dj,dk) is identical to (-di,-dj,-dk),
359    // so only compute 13 unique Poisson solutions (one per centrosymmetric pair).
360    // Pair p corresponds to stencil_positions[p] and stencil_positions[25-p].
361    let num_pairs = 13;
362    let mut a_rows: Vec<Vec<f64>> = Vec::with_capacity(num_pairs);
363
364    for p in 0..num_pairs {
365        let (di, dj, dk) = stencil_positions[p];
366
367        // Create delta function: delta[mid+I] = 1, delta[mid-I] = 1, delta[mid] = -2
368        let mut delta = vec![0.0_f64; n3];
369        let pi = (mid as i32 + di) as usize;
370        let pj = (mid as i32 + dj) as usize;
371        let pk = (mid as i32 + dk) as usize;
372        let mi = (mid as i32 - di) as usize;
373        let mj = (mid as i32 - dj) as usize;
374        let mk = (mid as i32 - dk) as usize;
375
376        delta[pi + pj * n + pk * n2] = 1.0;
377        delta[mi + mj * n + mk * n2] += 1.0; // += handles coincident positions
378        delta[mid + mid * n + mid * n2] = -2.0;
379
380        // Forward 3D DST-I
381        let mut fdelta = dst3d(&delta, n, &sin_table);
382
383        // Poisson solve: divide by -coord2_grid
384        for idx in 0..n3 {
385            fdelta[idx] /= -coord2_grid[idx];
386        }
387
388        // Inverse 3D DST-I (= forward DST-I * scale)
389        let vdelta = dst3d(&fdelta, n, &sin_table);
390
391        // Extract values at valid dipole points, applying inverse scale
392        let row: Vec<f64> = valid_indices
393            .iter()
394            .map(|&idx| vdelta[idx] * idst_scale)
395            .collect();
396        a_rows.push(row);
397    }
398
399    // Extract dipole values at valid points
400    let d_valid: Vec<f64> = valid_indices.iter().map(|&idx| d[idx]).collect();
401
402    // Solve least squares: B^T * y ≈ d_valid via normal equations (13 unknowns, full rank)
403    // G = B * B^T (13×13), h = B * d_valid (13×1)
404    let mut g = vec![vec![0.0_f64; num_pairs]; num_pairs];
405    for i in 0..num_pairs {
406        for j in 0..=i {
407            let dot: f64 = a_rows[i]
408                .iter()
409                .zip(a_rows[j].iter())
410                .map(|(&a, &b)| a * b)
411                .sum();
412            g[i][j] = dot;
413            g[j][i] = dot;
414        }
415    }
416
417    let mut h = vec![0.0_f64; num_pairs];
418    for i in 0..num_pairs {
419        h[i] = a_rows[i]
420            .iter()
421            .zip(d_valid.iter())
422            .map(|(&a, &b)| a * b)
423            .sum();
424    }
425
426    // Solve G * y = h via eigendecomposition with thresholding (handles rank deficiency
427    // from centrosymmetry and other spatial symmetries like x-y equivalence)
428    let y = solve_symmetric_pseudoinverse(&g, &h);
429
430    // Assemble stencil: assign each pair's coefficient to both positions
431    // y[p] = x[p] + x[25-p] = 2*x[p] (by symmetry), so stencil[I] = 2*x[I] = y[p]
432    let mut result = [[[0.0f32; 3]; 3]; 3];
433    for p in 0..num_pairs {
434        let coeff = y[p] as f32;
435
436        // First member of pair
437        let (di, dj, dk) = stencil_positions[p];
438        result[(di + 1) as usize][(dj + 1) as usize][(dk + 1) as usize] = coeff;
439
440        // Second member (centrosymmetric partner)
441        let (di2, dj2, dk2) = stencil_positions[25 - p];
442        result[(di2 + 1) as usize][(dj2 + 1) as usize][(dk2 + 1) as usize] = coeff;
443    }
444
445    // Apply resolution weights: w = (i²/dx² + j²/dy² + k²/dz²) / (i² + j² + k²)
446    for dk in -1..=1_i32 {
447        for dj in -1..=1_i32 {
448            for di in -1..=1_i32 {
449                if di == 0 && dj == 0 && dk == 0 {
450                    continue;
451                }
452                let si = (di + 1) as usize;
453                let sj = (dj + 1) as usize;
454                let sk = (dk + 1) as usize;
455
456                let i2 = (di * di) as f64;
457                let j2 = (dj * dj) as f64;
458                let k2 = (dk * dk) as f64;
459                let weight = (i2 / (dx * dx) + j2 / (dy * dy) + k2 / (dz * dz)) / (i2 + j2 + k2);
460                result[si][sj][sk] *= weight as f32;
461            }
462        }
463    }
464
465    // Center = -sum(all others)
466    let mut total = 0.0f32;
467    for dk in 0..3 {
468        for dj in 0..3 {
469            for di in 0..3 {
470                if !(di == 1 && dj == 1 && dk == 1) {
471                    total += result[di][dj][dk];
472                }
473            }
474        }
475    }
476    result[1][1][1] = -total;
477
478    result
479}
480
481/// Build sin table for DST-I of given length.
482/// sin_table[n][k] = sin(π*(n+1)*(k+1)/(N+1))
483fn dst_sin_table(n: usize) -> Vec<Vec<f64>> {
484    let scale = std::f64::consts::PI / (n as f64 + 1.0);
485    let mut table = vec![vec![0.0_f64; n]; n];
486    for j in 0..n {
487        for k in 0..n {
488            table[j][k] = ((j as f64 + 1.0) * (k as f64 + 1.0) * scale).sin();
489        }
490    }
491    table
492}
493
494/// 3D separable DST-I (Type I Discrete Sine Transform).
495fn dst3d(input: &[f64], n: usize, sin_table: &[Vec<f64>]) -> Vec<f64> {
496    let n2 = n * n;
497    let mut data = input.to_vec();
498    let mut buf_in = vec![0.0_f64; n];
499    let mut buf_out = vec![0.0_f64; n];
500
501    // Transform along x (contiguous dimension)
502    for k in 0..n {
503        for j in 0..n {
504            let base = j * n + k * n2;
505            buf_in.copy_from_slice(&data[base..base + n]);
506            dst1(&buf_in, sin_table, &mut buf_out);
507            data[base..base + n].copy_from_slice(&buf_out);
508        }
509    }
510
511    // Transform along y (stride = n)
512    for k in 0..n {
513        for i in 0..n {
514            for j in 0..n {
515                buf_in[j] = data[i + j * n + k * n2];
516            }
517            dst1(&buf_in, sin_table, &mut buf_out);
518            for j in 0..n {
519                data[i + j * n + k * n2] = buf_out[j];
520            }
521        }
522    }
523
524    // Transform along z (stride = n*n)
525    for j in 0..n {
526        for i in 0..n {
527            for k in 0..n {
528                buf_in[k] = data[i + j * n + k * n2];
529            }
530            dst1(&buf_in, sin_table, &mut buf_out);
531            for k in 0..n {
532                data[i + j * n + k * n2] = buf_out[k];
533            }
534        }
535    }
536
537    data
538}
539
540/// 1D DST-I: X[k] = Σ x[n] * sin(π*(n+1)*(k+1)/(N+1))
541fn dst1(input: &[f64], sin_table: &[Vec<f64>], output: &mut [f64]) {
542    let n = input.len();
543    for k in 0..n {
544        let mut sum = 0.0_f64;
545        for j in 0..n {
546            sum += input[j] * sin_table[j][k];
547        }
548        output[k] = sum;
549    }
550}
551
552/// Solve a symmetric positive semi-definite system via eigendecomposition with thresholding.
553///
554/// Computes the pseudo-inverse solution: x = V * diag(1/λ, thresholded) * V^T * h
555/// where G = V Λ V^T is the eigendecomposition. Small eigenvalues (< threshold * max_eigenvalue)
556/// are zeroed, matching Julia's SVD-based approach for handling rank-deficient systems.
557fn solve_symmetric_pseudoinverse(g: &[Vec<f64>], h: &[f64]) -> Vec<f64> {
558    let n = h.len();
559
560    // Copy g for eigendecomposition (Jacobi method modifies in place)
561    let mut a: Vec<Vec<f64>> = g.to_vec();
562    let mut v = vec![vec![0.0_f64; n]; n];
563    for i in 0..n {
564        v[i][i] = 1.0;
565    }
566
567    // Jacobi eigendecomposition for symmetric matrices
568    let max_sweeps = 100;
569    let tol = 1e-15;
570
571    for _ in 0..max_sweeps {
572        // Find largest off-diagonal element
573        let mut max_off = 0.0_f64;
574        for i in 0..n {
575            for j in (i + 1)..n {
576                max_off = max_off.max(a[i][j].abs());
577            }
578        }
579        if max_off < tol {
580            break;
581        }
582
583        // Sweep all off-diagonal pairs
584        for p in 0..n {
585            for q in (p + 1)..n {
586                if a[p][q].abs() < tol {
587                    continue;
588                }
589
590                // Compute Givens rotation angle to zero a[p][q]
591                let app = a[p][p];
592                let aqq = a[q][q];
593                let apq = a[p][q];
594                let tau = (aqq - app) / (2.0 * apq);
595                let t = if tau >= 0.0 {
596                    1.0 / (tau + (1.0 + tau * tau).sqrt())
597                } else {
598                    -1.0 / (-tau + (1.0 + tau * tau).sqrt())
599                };
600                let c = 1.0 / (1.0 + t * t).sqrt();
601                let s = t * c;
602
603                // Update matrix A' = G^T A G (only rows/cols p, q change)
604                // First update off-diagonal rows
605                for i in 0..n {
606                    if i == p || i == q {
607                        continue;
608                    }
609                    let aip = a[i][p];
610                    let aiq = a[i][q];
611                    a[i][p] = c * aip - s * aiq;
612                    a[p][i] = a[i][p];
613                    a[i][q] = s * aip + c * aiq;
614                    a[q][i] = a[i][q];
615                }
616
617                // Update 2×2 block
618                a[p][p] = c * c * app - 2.0 * c * s * apq + s * s * aqq;
619                a[q][q] = s * s * app + 2.0 * c * s * apq + c * c * aqq;
620                a[p][q] = 0.0;
621                a[q][p] = 0.0;
622
623                // Accumulate eigenvectors: V' = V * G
624                for i in 0..n {
625                    let vip = v[i][p];
626                    let viq = v[i][q];
627                    v[i][p] = c * vip - s * viq;
628                    v[i][q] = s * vip + c * viq;
629                }
630            }
631        }
632    }
633
634    // Eigenvalues are on diagonal
635    let eigenvalues: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
636    let max_eigen = eigenvalues.iter().cloned().fold(0.0_f64, |a, b| a.max(b.abs()));
637    let threshold = 1e-10 * max_eigen;
638
639    // Solve: x = V * diag(1/λ_thresholded) * V^T * h
640    let mut vt_h = vec![0.0_f64; n];
641    for i in 0..n {
642        for j in 0..n {
643            vt_h[i] += v[j][i] * h[j];
644        }
645    }
646
647    for i in 0..n {
648        if eigenvalues[i].abs() > threshold {
649            vt_h[i] /= eigenvalues[i];
650        } else {
651            vt_h[i] = 0.0;
652        }
653    }
654
655    let mut x = vec![0.0_f64; n];
656    for i in 0..n {
657        for j in 0..n {
658            x[i] += v[i][j] * vt_h[j];
659        }
660    }
661
662    x
663}
664
665/// Apply dipole stencil to a 3D volume
666/// Uses Neumann BC at boundaries (matching Julia's wave_local)
667fn apply_stencil(
668    output: &mut [f32],
669    input: &[f32],
670    stencil: &[[[f32; 3]; 3]; 3],
671    mask: &[u8],
672    nx: usize, ny: usize, nz: usize,
673) {
674    let nxy = nx * ny;
675    // One rayon task per z-slab (disjoint output chunk); reads input by global index.
676    maybe_par_chunks_mut!(output, nxy).enumerate().for_each(|(k, out_slab)| {
677        for j in 0..ny {
678            for i in 0..nx {
679                let local = i + j * nx;
680                let idx = local + k * nxy;
681
682                if mask[idx] == 0 {
683                    out_slab[local] = 0.0;
684                    continue;
685                }
686
687                // Julia's wave_local only computes if not at boundary
688                // If at boundary, result is 0
689                if i == 0 || j == 0 || k == 0 || i + 1 >= nx || j + 1 >= ny || k + 1 >= nz {
690                    out_slab[local] = 0.0;
691                    continue;
692                }
693
694                let mut sum = 0.0f32;
695
696                for dk in 0..3i32 {
697                    for dj in 0..3i32 {
698                        for di in 0..3i32 {
699                            let ni = (i as i32 + di - 1) as usize;
700                            let nj = (j as i32 + dj - 1) as usize;
701                            let nk = (k as i32 + dk - 1) as usize;
702
703                            let nidx = ni + nj * nx + nk * nxy;
704                            sum += stencil[di as usize][dj as usize][dk as usize] * input[nidx];
705                        }
706                    }
707                }
708
709                out_slab[local] = sum;
710            }
711        }
712    });
713}
714
715/// Compute Laplacian of wrapped phase using the DEL method
716pub fn compute_phase_laplacian(
717    phase: &[f32],
718    mask: &[u8],
719    nx: usize, ny: usize, nz: usize,
720    vsx: f32, vsy: f32, vsz: f32,
721) -> Vec<f32> {
722    let n_total = nx * ny * nz;
723
724    let sin_phase: Vec<f32> = phase.iter().map(|&p| p.sin()).collect();
725    let cos_phase: Vec<f32> = phase.iter().map(|&p| p.cos()).collect();
726
727    let lap_sin = compute_laplacian(&sin_phase, nx, ny, nz, vsx, vsy, vsz);
728    let lap_cos = compute_laplacian(&cos_phase, nx, ny, nz, vsx, vsy, vsz);
729
730    let mut laplacian = vec![0.0f32; n_total];
731    for i in 0..n_total {
732        if mask[i] != 0 {
733            laplacian[i] = lap_sin[i] * cos_phase[i] - lap_cos[i] * sin_phase[i];
734        }
735    }
736
737    laplacian
738}
739
740/// Compute discrete Laplacian of a 3D array
741fn compute_laplacian(
742    input: &[f32],
743    nx: usize, ny: usize, nz: usize,
744    vsx: f32, vsy: f32, vsz: f32,
745) -> Vec<f32> {
746    let n_total = nx * ny * nz;
747    let mut output = vec![0.0f32; n_total];
748
749    let hx2 = 1.0 / (vsx * vsx);
750    let hy2 = 1.0 / (vsy * vsy);
751    let hz2 = 1.0 / (vsz * vsz);
752    let center = -2.0 * (hx2 + hy2 + hz2);
753
754    for k in 0..nz {
755        let km1 = if k == 0 { 0 } else { k - 1 };
756        let kp1 = if k + 1 >= nz { nz - 1 } else { k + 1 };
757
758        for j in 0..ny {
759            let jm1 = if j == 0 { 0 } else { j - 1 };
760            let jp1 = if j + 1 >= ny { ny - 1 } else { j + 1 };
761
762            for i in 0..nx {
763                let im1 = if i == 0 { 0 } else { i - 1 };
764                let ip1 = if i + 1 >= nx { nx - 1 } else { i + 1 };
765
766                let idx = i + j * nx + k * nx * ny;
767
768                output[idx] = center * input[idx]
769                    + hx2 * (input[im1 + j * nx + k * nx * ny] + input[ip1 + j * nx + k * nx * ny])
770                    + hy2 * (input[i + jm1 * nx + k * nx * ny] + input[i + jp1 * nx + k * nx * ny])
771                    + hz2 * (input[i + j * nx + km1 * nx * ny] + input[i + j * nx + kp1 * nx * ny]);
772            }
773        }
774    }
775
776    output
777}
778
779/// Apply Laplacian with mask
780fn apply_laplacian_inplace(
781    output: &mut [f32],
782    input: &[f32],
783    mask: &[u8],
784    nx: usize, ny: usize, nz: usize,
785    vsx: f32, vsy: f32, vsz: f32,
786) {
787    let hx2 = 1.0 / (vsx * vsx);
788    let hy2 = 1.0 / (vsy * vsy);
789    let hz2 = 1.0 / (vsz * vsz);
790    let nxy = nx * ny;
791
792    maybe_par_chunks_mut!(output, nxy).enumerate().for_each(|(k, out_slab)| {
793        let k_offset = k * nxy;
794
795        for j in 0..ny {
796            let j_offset = j * nx;
797
798            for i in 0..nx {
799                let local = i + j_offset;
800                let idx = local + k_offset;
801
802                if mask[idx] == 0 {
803                    out_slab[local] = 0.0;
804                    continue;
805                }
806
807                let a0 = input[idx];
808
809                // Neumann BC: use center value at boundary (matching Julia)
810                let a_xm = if i > 0 { input[(i - 1) + j_offset + k_offset] } else { a0 };
811                let a_xp = if i + 1 < nx { input[(i + 1) + j_offset + k_offset] } else { a0 };
812                let a_ym = if j > 0 { input[i + (j - 1) * nx + k_offset] } else { a0 };
813                let a_yp = if j + 1 < ny { input[i + (j + 1) * nx + k_offset] } else { a0 };
814                let a_zm = if k > 0 { input[i + j_offset + (k - 1) * nxy] } else { a0 };
815                let a_zp = if k + 1 < nz { input[i + j_offset + (k + 1) * nxy] } else { a0 };
816
817                // Laplacian: sum of second derivatives
818                out_slab[local] = hx2 * (a_xm - 2.0 * a0 + a_xp)
819                            + hy2 * (a_ym - 2.0 * a0 + a_yp)
820                            + hz2 * (a_zm - 2.0 * a0 + a_zp);
821            }
822        }
823    });
824}
825
826
827/// Compute gradient norm squared
828#[inline]
829fn grad_norm_sq(res: (f32, f32, f32)) -> f32 {
830    let (dx, dy, dz) = res;
831    4.0 * (1.0 / (dx * dx) + 1.0 / (dy * dy) + 1.0 / (dz * dz))
832}
833
834/// Compute the squared spectral norm of the TGV operator matrix via power iteration
835///
836/// The operator matrix M is:
837/// [0,   g,  1 ]
838/// [0,   0,  g ]
839/// [g², w,  0 ]
840///
841/// We compute the largest eigenvalue of M^T * M using power iteration.
842fn compute_operator_norm_sqr(g: f32, g2: f32, w: f32) -> f32 {
843    // M^T * M = [g⁴,   g²w,    0    ]
844    //           [g²w,  g²+w²,  g    ]
845    //           [0,    g,      g²+1 ]
846    let g4 = g2 * g2;
847    let g2w = g2 * w;
848    let g2_w2 = g2 + w * w;
849    let g2_1 = g2 + 1.0;
850
851    // Power iteration to find largest eigenvalue
852    let mut v = [1.0f32, 1.0, 1.0];
853
854    for _ in 0..20 {
855        // Matrix-vector multiply: y = (M^T * M) * v
856        let y0 = g4 * v[0] + g2w * v[1];
857        let y1 = g2w * v[0] + g2_w2 * v[1] + g * v[2];
858        let y2 = g * v[1] + g2_1 * v[2];
859
860        // Compute norm
861        let norm = (y0 * y0 + y1 * y1 + y2 * y2).sqrt();
862        if norm < 1e-10 {
863            break;
864        }
865
866        // Normalize
867        v[0] = y0 / norm;
868        v[1] = y1 / norm;
869        v[2] = y2 / norm;
870    }
871
872    // Rayleigh quotient: eigenvalue = v^T * (M^T * M) * v
873    let y0 = g4 * v[0] + g2w * v[1];
874    let y1 = g2w * v[0] + g2_w2 * v[1] + g * v[2];
875    let y2 = g * v[1] + g2_1 * v[2];
876
877    v[0] * y0 + v[1] * y1 + v[2] * y2
878}
879
880/// L2 norm of 3-component vector
881#[inline]
882fn norm3(x: f32, y: f32, z: f32) -> f32 {
883    (x * x + y * y + z * z).sqrt()
884}
885
886/// Frobenius norm of symmetric 3x3 tensor (6 components)
887#[inline]
888fn frobenius_norm(sxx: f32, sxy: f32, sxz: f32, syy: f32, syz: f32, szz: f32) -> f32 {
889    (sxx * sxx + syy * syy + szz * szz + 2.0 * (sxy * sxy + sxz * sxz + syz * syz)).sqrt()
890}
891
892/// L-infinity projection for 3-component vector
893#[inline]
894fn project_linf3(px: &mut f32, py: &mut f32, pz: &mut f32, threshold: f32) {
895    let norm = norm3(*px, *py, *pz);
896    if norm > threshold {
897        let scale = threshold / norm;
898        *px *= scale;
899        *py *= scale;
900        *pz *= scale;
901    }
902}
903
904/// L-infinity projection for 6-component symmetric tensor
905#[inline]
906fn project_linf6(
907    qxx: &mut f32, qxy: &mut f32, qxz: &mut f32,
908    qyy: &mut f32, qyz: &mut f32, qzz: &mut f32,
909    threshold: f32,
910) {
911    let norm = frobenius_norm(*qxx, *qxy, *qxz, *qyy, *qyz, *qzz);
912    if norm > threshold {
913        let scale = threshold / norm;
914        *qxx *= scale;
915        *qxy *= scale;
916        *qxz *= scale;
917        *qyy *= scale;
918        *qyz *= scale;
919        *qzz *= scale;
920    }
921}
922
923/// Compute relative change for convergence check
924fn compute_relative_change(chi: &[f32], chi_prev: &[f32], mask: &[u8]) -> f32 {
925    let mut diff_sq = 0.0f32;
926    let mut norm_sq = 0.0f32;
927
928    for i in 0..chi.len() {
929        if mask[i] != 0 {
930            let d = chi[i] - chi_prev[i];
931            diff_sq += d * d;
932            norm_sq += chi[i] * chi[i];
933        }
934    }
935
936    if norm_sq > 1e-10 {
937        (diff_sq / norm_sq).sqrt()
938    } else {
939        1.0
940    }
941}
942
943/// Pre-allocated workspace for TGV iteration
944struct TgvWorkspace {
945    // Primal variables
946    chi: Vec<f32>,
947    chi_: Vec<f32>,
948    chi_prev: Vec<f32>,  // For convergence check
949    phi: Vec<f32>,
950    phi_: Vec<f32>,
951    wx: Vec<f32>,
952    wy: Vec<f32>,
953    wz: Vec<f32>,
954    wx_: Vec<f32>,
955    wy_: Vec<f32>,
956    wz_: Vec<f32>,
957
958    // Dual variables
959    eta: Vec<f32>,
960    px: Vec<f32>,
961    py: Vec<f32>,
962    pz: Vec<f32>,
963    qxx: Vec<f32>,
964    qxy: Vec<f32>,
965    qxz: Vec<f32>,
966    qyy: Vec<f32>,
967    qyz: Vec<f32>,
968    qzz: Vec<f32>,
969
970    // Temporary buffers
971    temp1: Vec<f32>,
972    temp2: Vec<f32>,
973    gx: Vec<f32>,
974    gy: Vec<f32>,
975    gz: Vec<f32>,
976
977    // Symmetric gradient buffers (reused)
978    sxx: Vec<f32>,
979    sxy: Vec<f32>,
980    sxz: Vec<f32>,
981    syy: Vec<f32>,
982    syz: Vec<f32>,
983    szz: Vec<f32>,
984
985    // Divergence buffers (reused)
986    divqx: Vec<f32>,
987    divqy: Vec<f32>,
988    divqz: Vec<f32>,
989}
990
991impl TgvWorkspace {
992    fn new(n: usize) -> Self {
993        Self {
994            chi: vec![0.0; n],
995            chi_: vec![0.0; n],
996            chi_prev: vec![0.0; n],
997            phi: vec![0.0; n],
998            phi_: vec![0.0; n],
999            wx: vec![0.0; n],
1000            wy: vec![0.0; n],
1001            wz: vec![0.0; n],
1002            wx_: vec![0.0; n],
1003            wy_: vec![0.0; n],
1004            wz_: vec![0.0; n],
1005            eta: vec![0.0; n],
1006            px: vec![0.0; n],
1007            py: vec![0.0; n],
1008            pz: vec![0.0; n],
1009            qxx: vec![0.0; n],
1010            qxy: vec![0.0; n],
1011            qxz: vec![0.0; n],
1012            qyy: vec![0.0; n],
1013            qyz: vec![0.0; n],
1014            qzz: vec![0.0; n],
1015            temp1: vec![0.0; n],
1016            temp2: vec![0.0; n],
1017            gx: vec![0.0; n],
1018            gy: vec![0.0; n],
1019            gz: vec![0.0; n],
1020            sxx: vec![0.0; n],
1021            sxy: vec![0.0; n],
1022            sxz: vec![0.0; n],
1023            syy: vec![0.0; n],
1024            syz: vec![0.0; n],
1025            szz: vec![0.0; n],
1026            divqx: vec![0.0; n],
1027            divqy: vec![0.0; n],
1028            divqz: vec![0.0; n],
1029        }
1030    }
1031}
1032
1033/// Main TGV-QSM reconstruction
1034///
1035/// # Arguments
1036/// * `phase` - Wrapped phase data
1037/// * `mask` - Binary mask
1038/// * `grid` - Volume grid (dimensions and voxel sizes)
1039/// * `params` - TGV parameters
1040/// * `b0_dir` - B0 field direction
1041/// * `progress` - Progress callback `(iteration, total_iterations)`
1042///
1043/// The reconstruction runs in single precision internally; inputs and
1044/// outputs use `f64` to match the rest of the crate's API.
1045pub fn tgv_qsm(
1046    phase: &[f64],
1047    mask: &[u8],
1048    grid: &crate::Grid,
1049    params: &TgvParams,
1050    b0_dir: (f64, f64, f64),
1051    mut progress: impl FnMut(usize, usize),
1052) -> Vec<f64> {
1053    // The algorithm runs in single precision; convert the f64 boundary inputs.
1054    let phase_f32: Vec<f32> = phase.iter().map(|&v| v as f32).collect();
1055    let phase: &[f32] = &phase_f32;
1056    let b0_dir = (b0_dir.0 as f32, b0_dir.1 as f32, b0_dir.2 as f32);
1057
1058    let (nx, ny, nz) = grid.dims;
1059    let n_total = grid.n_total();
1060    let vsx = grid.vsx() as f32;
1061    let vsy = grid.vsy() as f32;
1062    let vsz = grid.vsz() as f32;
1063    let res = (vsx, vsy, vsz);
1064
1065    // Erode mask
1066    let full_grid = crate::Grid::new(nx, ny, nz, vsx as f64, vsy as f64, vsz as f64);
1067    let mut mask_eroded = mask.to_vec();
1068    for _ in 0..params.erosions {
1069        mask_eroded = erode_mask(&mask_eroded, &full_grid, 1);
1070    }
1071
1072    // Create mask0 (one more erosion for internal computations)
1073    let mask0 = erode_mask(&mask_eroded, &full_grid, 1);
1074
1075    // Find bounding box (with padding of 2 voxels)
1076    let bbox = BoundingBox::from_mask(&mask0, nx, ny, nz, 2);
1077    let (bx, by, bz) = bbox.dims();
1078    let b_total = bbox.total();
1079    let sub_grid = crate::Grid::new(bx, by, bz, vsx as f64, vsy as f64, vsz as f64);
1080
1081    // Extract sub-volumes for the bounding box region
1082    let phase_sub = extract_subvolume(phase, &bbox, nx, ny, nz);
1083    let mask0_sub = extract_subvolume(&mask0, &bbox, nx, ny, nz);
1084    let mask_eroded_sub = extract_subvolume(&mask_eroded, &bbox, nx, ny, nz);
1085
1086    // Compute phase Laplacian on sub-volume
1087    let mut laplace_phi0 = compute_phase_laplacian(&phase_sub, &mask0_sub, bx, by, bz, vsx, vsy, vsz);
1088
1089    // Subtract mean within mask
1090    let (sum, count): (f32, usize) = laplace_phi0.iter().zip(mask0_sub.iter())
1091        .filter(|(_, &m)| m != 0)
1092        .fold((0.0, 0), |(s, c), (&v, _)| (s + v, c + 1));
1093    if count > 0 {
1094        let mean = sum / count as f32;
1095        for (v, &m) in laplace_phi0.iter_mut().zip(mask0_sub.iter()) {
1096            if m != 0 {
1097                *v -= mean;
1098            }
1099        }
1100    }
1101
1102    // Compute SVD-fitted oblique dipole stencil (matching Julia reference)
1103    let stencil = compute_oblique_stencil(res, b0_dir);
1104
1105    // Compute step sizes for convergence (matching Julia implementation)
1106    let grad_norm_squared = grad_norm_sq(res);
1107    let grad_norm = grad_norm_squared.sqrt();
1108    let wave_norm: f32 = stencil.iter().flatten().flatten().map(|x| x.abs()).sum();
1109    let norm_sqr = compute_operator_norm_sqr(grad_norm, grad_norm_squared, wave_norm);
1110
1111    let tau = 1.0 / norm_sqr.sqrt();
1112    let sigma = tau;
1113
1114    // step_size is applied selectively in the updates:
1115    // - eta, phi: use base sigma/tau
1116    // - p, q, chi, w: use sigma * step_size / tau * step_size
1117    let sigma_step = sigma * params.step_size;
1118    let tau_step = tau * params.step_size;
1119
1120    // Projection thresholds are alpha values (NOT 1/alpha!)
1121    // Julia: projects p to ||p|| <= alpha1, q to ||q|| <= alpha0
1122    let alpha = (params.alpha0, params.alpha1);
1123
1124    // Pre-allocate all workspace buffers
1125    let mut ws = TgvWorkspace::new(b_total);
1126
1127    let mut _converged = false;
1128    let mut final_iter = params.iterations;
1129
1130    // Main iteration loop
1131    for iter in 0..params.iterations {
1132        progress(iter, params.iterations);
1133
1134        // Convergence check every 100 iterations
1135        if iter > 0 && iter % 100 == 0 {
1136            let rel_change = compute_relative_change(&ws.chi, &ws.chi_prev, &mask0_sub);
1137            if rel_change < params.tol {
1138                _converged = true;
1139                final_iter = iter;
1140                break;
1141            }
1142            // Save current chi for next convergence check
1143            ws.chi_prev.copy_from_slice(&ws.chi);
1144        }
1145
1146        // === DUAL UPDATE ===
1147
1148        // 1. Update eta (data term dual)
1149        apply_laplacian_inplace(&mut ws.temp1, &ws.phi_, &mask0_sub, bx, by, bz, vsx, vsy, vsz);
1150        apply_stencil(&mut ws.temp2, &ws.chi_, &stencil, &mask0_sub, bx, by, bz);
1151
1152        for i in 0..b_total {
1153            if mask0_sub[i] != 0 {
1154                ws.eta[i] += sigma * (-ws.temp1[i] + ws.temp2[i] - laplace_phi0[i]);
1155            }
1156        }
1157
1158        // 2. Update p (gradient dual)
1159        // Julia: p += mask0 * sigma * grad(chi) - mask * sigma * w
1160        // Compute unmasked gradient first
1161        crate::utils::gradient::fgrad_inplace_f32(
1162            &mut ws.gx, &mut ws.gy, &mut ws.gz, &ws.chi_, &sub_grid
1163        );
1164
1165        for i in 0..b_total {
1166            let in_mask0 = mask0_sub[i] != 0;
1167            let in_mask = mask_eroded_sub[i] != 0;
1168
1169            if in_mask0 || in_mask {
1170                // gradient term scaled by mask0, w term scaled by mask
1171                let sigmaw0 = if in_mask0 { sigma_step } else { 0.0 };
1172                let sigmaw = if in_mask { sigma_step } else { 0.0 };
1173
1174                ws.px[i] += sigmaw0 * ws.gx[i] - sigmaw * ws.wx_[i];
1175                ws.py[i] += sigmaw0 * ws.gy[i] - sigmaw * ws.wy_[i];
1176                ws.pz[i] += sigmaw0 * ws.gz[i] - sigmaw * ws.wz_[i];
1177
1178                project_linf3(&mut ws.px[i], &mut ws.py[i], &mut ws.pz[i], alpha.1);
1179            }
1180        }
1181
1182        // 3. Update q (symmetric gradient dual)
1183        crate::utils::gradient::symgrad_inplace_f32(
1184            &mut ws.sxx, &mut ws.sxy, &mut ws.sxz, &mut ws.syy, &mut ws.syz, &mut ws.szz,
1185            &ws.wx_, &ws.wy_, &ws.wz_, &sub_grid
1186        );
1187
1188        for i in 0..b_total {
1189            if mask0_sub[i] != 0 {
1190                ws.qxx[i] += sigma_step * ws.sxx[i];
1191                ws.qxy[i] += sigma_step * ws.sxy[i];
1192                ws.qxz[i] += sigma_step * ws.sxz[i];
1193                ws.qyy[i] += sigma_step * ws.syy[i];
1194                ws.qyz[i] += sigma_step * ws.syz[i];
1195                ws.qzz[i] += sigma_step * ws.szz[i];
1196
1197                project_linf6(
1198                    &mut ws.qxx[i], &mut ws.qxy[i], &mut ws.qxz[i],
1199                    &mut ws.qyy[i], &mut ws.qyz[i], &mut ws.qzz[i],
1200                    alpha.0
1201                );
1202            }
1203        }
1204
1205        // === VARIABLE SWAP ===
1206        std::mem::swap(&mut ws.phi, &mut ws.phi_);
1207        std::mem::swap(&mut ws.chi, &mut ws.chi_);
1208        std::mem::swap(&mut ws.wx, &mut ws.wx_);
1209        std::mem::swap(&mut ws.wy, &mut ws.wy_);
1210        std::mem::swap(&mut ws.wz, &mut ws.wz_);
1211
1212        // === PRIMAL UPDATE ===
1213
1214        // 1. Update phi
1215        for i in 0..b_total {
1216            ws.temp1[i] = if mask0_sub[i] != 0 { ws.eta[i] } else { 0.0 };
1217        }
1218        apply_laplacian_inplace(&mut ws.temp2, &ws.temp1, &mask0_sub, bx, by, bz, vsx, vsy, vsz);
1219
1220        for i in 0..b_total {
1221            let denom = 1.0 + if mask_eroded_sub[i] != 0 { tau } else { 0.0 };
1222            ws.phi[i] = (ws.phi_[i] + tau * ws.temp2[i]) / denom;
1223        }
1224
1225        // 2. Update chi
1226        crate::utils::gradient::bdiv_masked_inplace_f32(
1227            &mut ws.temp1, &ws.px, &ws.py, &ws.pz, &mask0_sub, &sub_grid
1228        );
1229
1230        for i in 0..b_total {
1231            ws.gx[i] = if mask0_sub[i] != 0 { ws.eta[i] } else { 0.0 };
1232        }
1233        apply_stencil(&mut ws.temp2, &ws.gx, &stencil, &mask0_sub, bx, by, bz);
1234
1235        for i in 0..b_total {
1236            ws.chi[i] = ws.chi_[i] + tau_step * (ws.temp1[i] - ws.temp2[i]);
1237        }
1238
1239        // 3. Update w
1240        for i in 0..b_total {
1241            let m = if mask0_sub[i] != 0 { 1.0 } else { 0.0 };
1242            ws.sxx[i] = ws.qxx[i] * m;
1243            ws.sxy[i] = ws.qxy[i] * m;
1244            ws.sxz[i] = ws.qxz[i] * m;
1245            ws.syy[i] = ws.qyy[i] * m;
1246            ws.syz[i] = ws.qyz[i] * m;
1247            ws.szz[i] = ws.qzz[i] * m;
1248        }
1249
1250        crate::utils::gradient::symdiv_inplace_f32(
1251            &mut ws.divqx, &mut ws.divqy, &mut ws.divqz,
1252            &ws.sxx, &ws.sxy, &ws.sxz, &ws.syy, &ws.syz, &ws.szz,
1253            &sub_grid
1254        );
1255
1256        // Julia: w_dest = w; if mask: w_dest += tau*(p + div(mask0*q))
1257        for i in 0..b_total {
1258            ws.wx[i] = ws.wx_[i];
1259            ws.wy[i] = ws.wy_[i];
1260            ws.wz[i] = ws.wz_[i];
1261            if mask_eroded_sub[i] != 0 {
1262                ws.wx[i] += tau_step * (ws.px[i] + ws.divqx[i]);
1263                ws.wy[i] += tau_step * (ws.py[i] + ws.divqy[i]);
1264                ws.wz[i] += tau_step * (ws.pz[i] + ws.divqz[i]);
1265            }
1266        }
1267
1268        // === EXTRAGRADIENT UPDATE ===
1269        for i in 0..b_total {
1270            ws.phi_[i] = 2.0 * ws.phi[i] - ws.phi_[i];
1271            ws.chi_[i] = 2.0 * ws.chi[i] - ws.chi_[i];
1272            ws.wx_[i] = 2.0 * ws.wx[i] - ws.wx_[i];
1273            ws.wy_[i] = 2.0 * ws.wy[i] - ws.wy_[i];
1274            ws.wz_[i] = 2.0 * ws.wz[i] - ws.wz_[i];
1275        }
1276    }
1277
1278    progress(final_iter, params.iterations);
1279
1280    // Scale to susceptibility (ppm)
1281    let gamma = 42.5781f32;  // Hz/T
1282    let scale = 1.0 / (2.0 * PI * params.te * params.fieldstrength * gamma);
1283
1284    // Create full-size result and insert sub-volume
1285    let mut result = vec![0.0f32; n_total];
1286
1287    // Scale chi in sub-volume and apply mask
1288    let mut chi_scaled = vec![0.0f32; b_total];
1289    for i in 0..b_total {
1290        if mask_eroded_sub[i] != 0 {
1291            chi_scaled[i] = ws.chi[i] * scale;
1292        }
1293    }
1294
1295    // Insert back into full volume
1296    insert_subvolume(&mut result, &chi_scaled, &bbox, nx, ny, nz);
1297
1298    result.iter().map(|&v| v as f64).collect()
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use super::*;
1304
1305    #[test]
1306    fn test_dipole_stencil() {
1307        let stencil = compute_dipole_stencil((1.0, 1.0, 1.0), (0.0, 0.0, 1.0));
1308
1309        let mut sum = 0.0f32;
1310        for k in 0..3 {
1311            for j in 0..3 {
1312                for i in 0..3 {
1313                    sum += stencil[i][j][k];
1314                }
1315            }
1316        }
1317        assert!(sum.abs() < 1e-6, "Stencil sum should be ~0, got {}", sum);
1318    }
1319
1320    #[test]
1321    fn test_phase_laplacian() {
1322        let nx = 4;
1323        let ny = 4;
1324        let nz = 4;
1325        let n = nx * ny * nz;
1326
1327        let phase = vec![1.0f32; n];
1328        let mask = vec![1u8; n];
1329
1330        let lap = compute_phase_laplacian(&phase, &mask, nx, ny, nz, 1.0, 1.0, 1.0);
1331
1332        let max_val = lap.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
1333        assert!(max_val < 1e-5, "Laplacian of constant should be ~0, got max {}", max_val);
1334    }
1335
1336    #[test]
1337    fn test_erode_mask() {
1338        let nx = 5;
1339        let ny = 5;
1340        let nz = 5;
1341
1342        let mask = vec![1u8; nx * ny * nz];
1343        let grid5 = crate::Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
1344        let eroded = erode_mask(&mask, &grid5, 1);
1345
1346        let center = 2 + 2 * nx + 2 * nx * ny;
1347        assert_eq!(eroded[center], 1);
1348        assert_eq!(eroded[0], 0);
1349    }
1350
1351    #[test]
1352    fn test_default_alpha() {
1353        let (a0, a1) = get_default_alpha(2);
1354        assert!((a0 - 0.001).abs() < 1e-6);
1355        assert!((a1 - 0.001).abs() < 1e-6);
1356    }
1357
1358    #[test]
1359    fn test_bounding_box() {
1360        let nx = 10;
1361        let ny = 10;
1362        let nz = 10;
1363        let mut mask = vec![0u8; nx * ny * nz];
1364
1365        // Set a small region in the center
1366        for k in 3..7 {
1367            for j in 3..7 {
1368                for i in 3..7 {
1369                    mask[i + j * nx + k * nx * ny] = 1;
1370                }
1371            }
1372        }
1373
1374        let bbox = BoundingBox::from_mask(&mask, nx, ny, nz, 1);
1375
1376        // Should be 3-1=2 to 6+1+1=8 (with padding 1)
1377        assert_eq!(bbox.i_min, 2);
1378        assert_eq!(bbox.i_max, 8);
1379        assert_eq!(bbox.j_min, 2);
1380        assert_eq!(bbox.j_max, 8);
1381    }
1382
1383    #[test]
1384    fn test_tgv_qsm_small() {
1385        let n = 12;
1386        let n_total = n * n * n;
1387        let center = 6.0_f32;
1388        let radius = 4.0_f32;
1389
1390        // Build a sphere mask
1391        let mut mask = vec![0u8; n_total];
1392        for k in 0..n {
1393            for j in 0..n {
1394                for i in 0..n {
1395                    let dx = i as f32 - center;
1396                    let dy = j as f32 - center;
1397                    let dz = k as f32 - center;
1398                    if (dx * dx + dy * dy + dz * dz).sqrt() < radius {
1399                        mask[i + j * n + k * n * n] = 1;
1400                    }
1401                }
1402            }
1403        }
1404
1405        // Fill phase with a linear ramp (z direction) masked
1406        let mut phase = vec![0.0f64; n_total];
1407        for k in 0..n {
1408            for j in 0..n {
1409                for i in 0..n {
1410                    let idx = i + j * n + k * n * n;
1411                    if mask[idx] != 0 {
1412                        phase[idx] = 0.1 * k as f64;
1413                    }
1414                }
1415            }
1416        }
1417
1418        let params = TgvParams {
1419            iterations: 10,
1420            erosions: 1,
1421            ..TgvParams::default()
1422        };
1423
1424        let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
1425        let result = tgv_qsm(&phase, &mask, &grid, &params, (0.0, 0.0, 1.0), |_, _| {});
1426
1427        assert_eq!(result.len(), n_total);
1428
1429        // All values should be finite
1430        for &v in &result {
1431            assert!(v.is_finite(), "Result contains non-finite value: {}", v);
1432        }
1433
1434        // There should be at least some non-zero values within the (eroded) mask
1435        let has_nonzero = result.iter().any(|&v| v.abs() > 1e-20);
1436        assert!(has_nonzero, "Result is entirely zero; expected non-zero values within mask");
1437    }
1438
1439    #[test]
1440    fn test_oblique_stencil() {
1441        let stencil = compute_oblique_stencil((1.0, 1.0, 1.0), (0.0, 0.0, 1.0));
1442
1443        // Sum of all elements should be ~0 (Laplacian-like operator)
1444        let mut sum = 0.0f32;
1445        for k in 0..3 {
1446            for j in 0..3 {
1447                for i in 0..3 {
1448                    sum += stencil[i][j][k];
1449                }
1450            }
1451        }
1452        assert!(
1453            sum.abs() < 1e-4,
1454            "Oblique stencil sum should be ~0, got {}",
1455            sum
1456        );
1457
1458        // Center element should be approximately -sum(others), so verify it matches
1459        let mut off_sum = 0.0f32;
1460        for k in 0..3 {
1461            for j in 0..3 {
1462                for i in 0..3 {
1463                    if !(i == 1 && j == 1 && k == 1) {
1464                        off_sum += stencil[i][j][k];
1465                    }
1466                }
1467            }
1468        }
1469        assert!(
1470            (stencil[1][1][1] + off_sum).abs() < 1e-6,
1471            "Center should be -sum(others): center={}, off_sum={}",
1472            stencil[1][1][1], off_sum
1473        );
1474    }
1475
1476    #[test]
1477    fn test_oblique_stencil_aniso() {
1478        let stencil = compute_oblique_stencil((1.0, 1.0, 2.0), (0.2, 0.3, 0.9));
1479
1480        // Sum of all elements should be ~0
1481        let mut sum = 0.0f32;
1482        for k in 0..3 {
1483            for j in 0..3 {
1484                for i in 0..3 {
1485                    sum += stencil[i][j][k];
1486                }
1487            }
1488        }
1489        assert!(
1490            sum.abs() < 1e-4,
1491            "Anisotropic oblique stencil sum should be ~0, got {}",
1492            sum
1493        );
1494    }
1495
1496    #[test]
1497    fn test_get_default_iterations() {
1498        // With isotropic 1mm voxels and step_size=1.0
1499        let it = get_default_iterations((1.0, 1.0, 1.0), 1.0);
1500        assert!(it >= 1000, "Iterations should be >= 1000 for 1mm iso, got {}", it);
1501
1502        // With larger voxels the count should decrease (prod_res is larger)
1503        let it_large = get_default_iterations((2.0, 2.0, 2.0), 1.0);
1504        assert!(it_large >= 1000, "Iterations should still be >= 1000 for 2mm iso");
1505
1506        // With very small voxels the count should be larger than 1mm iso
1507        let it_small = get_default_iterations((0.5, 0.5, 0.5), 1.0);
1508        assert!(it_small > it, "Smaller voxels should need more iterations: {} vs {}", it_small, it);
1509
1510        // Higher step_size should reduce iterations
1511        let it_fast = get_default_iterations((1.0, 1.0, 1.0), 3.0);
1512        assert!(it_fast < it, "Higher step_size should give fewer iterations: {} vs {}", it_fast, it);
1513    }
1514
1515    #[test]
1516    fn test_compute_relative_change() {
1517        // Two identical arrays -> relative change = 0
1518        let chi = vec![1.0f32, 2.0, 3.0, 4.0];
1519        let chi_prev = vec![1.0f32, 2.0, 3.0, 4.0];
1520        let mask = vec![1u8, 1, 1, 1];
1521        let rc = compute_relative_change(&chi, &chi_prev, &mask);
1522        assert!(rc.abs() < 1e-10, "Identical arrays should give 0 change, got {}", rc);
1523
1524        // One element changed
1525        let chi2 = vec![1.1f32, 2.0, 3.0, 4.0];
1526        let rc2 = compute_relative_change(&chi2, &chi_prev, &mask);
1527        assert!(rc2 > 0.0, "Different arrays should give positive change");
1528        // Expected: sqrt(0.01 / (1.21 + 4 + 9 + 16)) = sqrt(0.01 / 30.21)
1529        let expected = (0.01f32 / 30.21).sqrt();
1530        assert!(
1531            (rc2 - expected).abs() < 1e-5,
1532            "Expected relative change ~{}, got {}",
1533            expected,
1534            rc2
1535        );
1536
1537        // Masked elements should be ignored
1538        let mask_partial = vec![1u8, 0, 0, 0];
1539        let chi3 = vec![2.0f32, 999.0, 999.0, 999.0];
1540        let chi_prev3 = vec![1.0f32, 0.0, 0.0, 0.0];
1541        let rc3 = compute_relative_change(&chi3, &chi_prev3, &mask_partial);
1542        // diff_sq = (2-1)^2 = 1, norm_sq = 4 => sqrt(1/4) = 0.5
1543        let expected3 = (1.0f32 / 4.0).sqrt();
1544        assert!(
1545            (rc3 - expected3).abs() < 1e-6,
1546            "Masked relative change expected {}, got {}",
1547            expected3,
1548            rc3
1549        );
1550
1551        // All zeros -> should return 1.0 (norm_sq < 1e-10)
1552        let zeros = vec![0.0f32; 4];
1553        let rc4 = compute_relative_change(&zeros, &zeros, &mask);
1554        assert!(
1555            (rc4 - 1.0).abs() < 1e-6,
1556            "Zero norm should return 1.0, got {}",
1557            rc4
1558        );
1559    }
1560
1561    #[test]
1562    fn test_tgv_convergence() {
1563        // Zero phase => chi should be ~0. Set tol=1.0 so convergence triggers at iter 100.
1564        let n = 12;
1565        let n_total = n * n * n;
1566        let center = 6.0_f32;
1567        let radius = 4.0_f32;
1568
1569        let mut mask = vec![0u8; n_total];
1570        for k in 0..n {
1571            for j in 0..n {
1572                for i in 0..n {
1573                    let dx = i as f32 - center;
1574                    let dy = j as f32 - center;
1575                    let dz = k as f32 - center;
1576                    if (dx * dx + dy * dy + dz * dz).sqrt() < radius {
1577                        mask[i + j * n + k * n * n] = 1;
1578                    }
1579                }
1580            }
1581        }
1582
1583        let phase = vec![0.0f64; n_total];
1584
1585        let params = TgvParams {
1586            iterations: 1000,
1587            erosions: 1,
1588            tol: 1.1, // Very loose tolerance so convergence triggers at first check (iter 100)
1589            ..TgvParams::default()
1590        };
1591
1592        let grid = crate::Grid::new(n, n, n, 1.0, 1.0, 1.0);
1593        let progress_iters = std::cell::RefCell::new(Vec::new());
1594        let result = tgv_qsm(
1595            &phase, &mask, &grid, &params, (0.0, 0.0, 1.0),
1596            |iter, _total| { progress_iters.borrow_mut().push(iter); }
1597        );
1598
1599        assert_eq!(result.len(), n_total);
1600
1601        // With zero phase, all output values should be very close to zero
1602        let max_abs = result.iter().map(|v| v.abs()).fold(0.0f64, f64::max);
1603        assert!(
1604            max_abs < 1e-3,
1605            "Zero-phase TGV result should be ~0, got max abs {}",
1606            max_abs
1607        );
1608
1609        // Early convergence: the last progress call should report iter <= 100
1610        // (it converges at the iter-100 check since chi and chi_prev are both ~0)
1611        let iters = progress_iters.borrow();
1612        let &last_iter = iters.last().unwrap();
1613        assert!(
1614            last_iter <= 100,
1615            "Expected early convergence by iter 100, but last progress was at iter {}",
1616            last_iter
1617        );
1618    }
1619}