Skip to main content

qsm_core/bgremove/
pdf.rs

1//! Projection onto Dipole Fields (PDF) background field removal
2//!
3//! Projects the field onto dipole fields generated by sources outside
4//! the brain mask, separating background and local fields.
5//!
6//! Reference:
7//! Liu, T., Khalidov, I., de Rochefort, L., Spincemaille, P., Liu, J., Tsiouris, A.J.,
8//! Wang, Y. (2011). "A novel background field removal method for MRI using projection
9//! onto dipole fields." NMR in Biomedicine, 24(9):1129-1136. https://doi.org/10.1002/nbm.1670
10//!
11//! Reference implementation: https://github.com/kamesy/QSM.jl
12
13use num_complex::Complex64;
14use crate::Grid;
15use crate::fft::{fft3d, ifft3d};
16use crate::kernels::dipole::dipole_kernel;
17use crate::utils::vec_norm;
18
19/// PDF algorithm parameters
20#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
21#[derive(Clone, Debug)]
22pub struct PdfParams {
23    /// Convergence tolerance
24    pub tol: f64,
25    /// Maximum LSMR iterations. `None` uses an automatic default of
26    /// `sqrt(n_voxels)`.
27    pub max_iter: Option<usize>,
28}
29
30impl Default for PdfParams {
31    fn default() -> Self {
32        Self { tol: 1e-5, max_iter: None }
33    }
34}
35
36/// PDF background field removal
37///
38/// # Arguments
39/// * `field` - Total field (nx * ny * nz)
40/// * `mask` - Binary mask (nx * ny * nz), 1 = brain, 0 = background
41/// * `grid` - Volume dimensions and voxel sizes
42/// * `bdir` - B0 field direction
43/// * `params` - PDF parameters (tolerance, optional max iterations)
44/// * `progress` - Progress callback (iteration, max_iter)
45///
46/// # Returns
47/// Local field with background removed
48pub fn pdf(
49    field: &[f64],
50    mask: &[u8],
51    grid: &Grid,
52    bdir: (f64, f64, f64),
53    params: &PdfParams,
54    progress: impl FnMut(usize, usize),
55) -> Vec<f64> {
56    // Default max iterations to sqrt(n_voxels) when unspecified.
57    let max_iter = params.max_iter.unwrap_or_else(|| (grid.n_total() as f64).sqrt() as usize);
58    pdf_core(field, mask, grid, bdir, params.tol, max_iter, progress)
59}
60
61/// PDF with an explicit LSMR iteration count.
62///
63/// Internal entry point; the public [`pdf`] wrapper supplies the default
64/// iteration count from [`PdfParams`].
65pub(crate) fn pdf_core(
66    field: &[f64],
67    mask: &[u8],
68    grid: &Grid,
69    bdir: (f64, f64, f64),
70    tol: f64,
71    max_iter: usize,
72    mut progress: impl FnMut(usize, usize),
73) -> Vec<f64> {
74    let (nx, ny, nz) = grid.dims;
75    let n_total = nx * ny * nz;
76
77    // Generate dipole kernel
78    let d_kernel = dipole_kernel(grid, bdir);
79
80    // Create background mask (complement of brain mask)
81    let bg_mask: Vec<f64> = mask.iter()
82        .map(|&m| if m == 0 { 1.0 } else { 0.0 })
83        .collect();
84
85    // Brain mask as f64
86    let brain_mask: Vec<f64> = mask.iter()
87        .map(|&m| if m != 0 { 1.0 } else { 0.0 })
88        .collect();
89
90    // RHS: b = W * f where W is brain mask weights
91    let b: Vec<f64> = field.iter()
92        .zip(brain_mask.iter())
93        .map(|(&f, &w)| f * w)
94        .collect();
95
96    // Initialize solution
97    let mut x = vec![0.0; n_total];
98
99    let mut u = b.clone();
100    let mut beta = vec_norm(&u);
101
102    if beta < 1e-20 {
103        return vec![0.0; n_total];
104    }
105
106    for i in 0..n_total {
107        u[i] /= beta;
108    }
109
110    let mut v = apply_at(&u, &brain_mask, &d_kernel, &bg_mask, nx, ny, nz);
111    let mut alpha = vec_norm(&v);
112
113    if alpha < 1e-20 {
114        return vec![0.0; n_total];
115    }
116
117    for i in 0..n_total {
118        v[i] /= alpha;
119    }
120
121    // LSMR variables (following Fong & Saunders 2011 / QSM.jl implementation)
122    let norm_b = beta;
123    let mut w = v.clone();
124    let mut phi_bar = beta;
125    let mut rho_bar = alpha;
126
127    // Variables for ||r|| estimation (matching Julia's lsmr.jl)
128    let mut beta_dd = beta;
129    let mut beta_d = 0.0;
130    let mut rho_d_old = 1.0;
131    let mut tau_tilde_old = 0.0;
132    let mut theta_tilde = 0.0;
133    let mut zeta = 0.0;
134    let d_accum = 0.0; // beta_check^2 accumulator; always 0 when lambda=0
135
136    // Variables for ||A|| estimation
137    let mut norm_a2 = alpha * alpha;
138
139    // Variables for the QR factorization (needed for ||r|| recurrence)
140    let mut zeta_bar = alpha * beta;
141    let mut alpha_bar = alpha;
142    let mut c_bar = 1.0;
143    let mut s_bar = 0.0;
144    let mut rho_val = 1.0;
145    let mut rho_bar_lsmr = 1.0;
146
147    for iter in 0..max_iter {
148        // Report progress
149        progress(iter + 1, max_iter);
150
151        // Bidiagonalization
152        let av = apply_a(&v, &bg_mask, &d_kernel, &brain_mask, nx, ny, nz);
153        for i in 0..n_total {
154            u[i] = av[i] - alpha * u[i];
155        }
156        beta = vec_norm(&u);
157
158        if beta < 1e-20 {
159            progress(iter + 1, iter + 1);
160            break;
161        }
162
163        for i in 0..n_total {
164            u[i] /= beta;
165        }
166
167        let atu = apply_at(&u, &brain_mask, &d_kernel, &bg_mask, nx, ny, nz);
168        for i in 0..n_total {
169            v[i] = atu[i] - beta * v[i];
170        }
171        alpha = vec_norm(&v);
172
173        if alpha < 1e-20 {
174            progress(iter + 1, iter + 1);
175            break;
176        }
177
178        for i in 0..n_total {
179            v[i] /= alpha;
180        }
181
182        // Construct and apply rotation Q_i (for solution update)
183        let rho = (rho_bar * rho_bar + beta * beta).sqrt();
184        let c = rho_bar / rho;
185        let s = beta / rho;
186        let theta = s * alpha;
187        rho_bar = -c * alpha;
188        let phi = c * phi_bar;
189        phi_bar *= s;
190
191        // Update x and w
192        let phi_rho = phi / rho;
193        let theta_rho = theta / rho;
194
195        for i in 0..n_total {
196            x[i] += phi_rho * w[i];
197            w[i] = v[i] - theta_rho * w[i];
198        }
199
200        // LSMR QR factorization (matching Julia lsmr.jl, with lambda=0)
201        let _rho_old = rho_val;
202        rho_val = (alpha_bar * alpha_bar + beta * beta).sqrt();
203        let c_lsmr = alpha_bar / rho_val;
204        let s_lsmr = beta / rho_val;
205        let theta_new = s_lsmr * alpha;
206        alpha_bar = c_lsmr * alpha;
207
208        let _rho_bar_old = rho_bar_lsmr;
209        let zeta_old = zeta;
210        let theta_bar = s_bar * rho_val;
211        let rho_tmp = c_bar * rho_val;
212        rho_bar_lsmr = (rho_tmp * rho_tmp + theta_new * theta_new).sqrt();
213        c_bar = rho_tmp / rho_bar_lsmr;
214        s_bar = theta_new / rho_bar_lsmr;
215        zeta = c_bar * zeta_bar;
216        zeta_bar *= -s_bar;
217
218        // Estimate ||r|| (matching Julia lsmr.jl lines 264-287, with lambda=0)
219        let beta_hat = c_lsmr * beta_dd;
220        beta_dd *= -s_lsmr;
221
222        let theta_tilde_old = theta_tilde;
223        let rho_tilde_old = (rho_d_old * rho_d_old + theta_bar * theta_bar).sqrt();
224        let c_tilde_old = rho_d_old / rho_tilde_old;
225        let s_tilde_old = theta_bar / rho_tilde_old;
226        theta_tilde = s_tilde_old * rho_bar_lsmr;
227        rho_d_old = c_tilde_old * rho_bar_lsmr;
228        beta_d = -s_tilde_old * beta_d + c_tilde_old * beta_hat;
229
230        tau_tilde_old = (zeta_old - theta_tilde_old * tau_tilde_old) / rho_tilde_old;
231        let tau_d = (zeta - theta_tilde * tau_tilde_old) / rho_d_old;
232
233        let norm_r = (d_accum + (beta_d - tau_d).powi(2) + beta_dd.powi(2)).sqrt();
234
235        // Estimate ||A||
236        norm_a2 += beta * beta;
237        let norm_a = norm_a2.sqrt();
238        norm_a2 += alpha * alpha;
239
240        // ||A'r|| estimate
241        let norm_ar = zeta_bar.abs();
242
243        // Convergence tests (matching Julia lsmr.jl lines 308-318)
244        let norm_x = vec_norm(&x);
245        let test1 = norm_r / norm_b;
246        let test2 = if norm_a * norm_r > 0.0 { norm_ar / (norm_a * norm_r) } else { 0.0 };
247        let eps_r = tol + tol * norm_a * norm_x / norm_b;
248
249        if test1 <= eps_r || test2 <= tol {
250            progress(iter + 1, iter + 1);
251            break;
252        }
253    }
254
255    // Compute background field: b_field = D * (M_bg * x)
256    let mut bg_source: Vec<Complex64> = x.iter()
257        .zip(bg_mask.iter())
258        .map(|(&xi, &m)| Complex64::new(xi * m, 0.0))
259        .collect();
260
261    fft3d(&mut bg_source, nx, ny, nz);
262
263    for i in 0..n_total {
264        bg_source[i] *= d_kernel[i];
265    }
266
267    ifft3d(&mut bg_source, nx, ny, nz);
268
269    // Local field = total field - background field, masked
270    let mut local_field = vec![0.0; n_total];
271    for i in 0..n_total {
272        if mask[i] != 0 {
273            local_field[i] = field[i] - bg_source[i].re;
274        }
275    }
276
277    local_field
278}
279
280/// Apply A = W * D * M_bg
281fn apply_a(
282    x: &[f64],
283    bg_mask: &[f64],
284    d_kernel: &[f64],
285    brain_mask: &[f64],
286    nx: usize, ny: usize, nz: usize,
287) -> Vec<f64> {
288    let n_total = nx * ny * nz;
289
290    // Apply background mask
291    let mut temp: Vec<Complex64> = x.iter()
292        .zip(bg_mask.iter())
293        .map(|(&xi, &m)| Complex64::new(xi * m, 0.0))
294        .collect();
295
296    // FFT
297    fft3d(&mut temp, nx, ny, nz);
298
299    // Apply dipole kernel
300    for i in 0..n_total {
301        temp[i] *= d_kernel[i];
302    }
303
304    // IFFT
305    ifft3d(&mut temp, nx, ny, nz);
306
307    // Apply brain mask weights
308    temp.iter()
309        .zip(brain_mask.iter())
310        .map(|(t, &w)| t.re * w)
311        .collect()
312}
313
314/// Apply A^T = M_bg * D * W
315fn apply_at(
316    u: &[f64],
317    brain_mask: &[f64],
318    d_kernel: &[f64],
319    bg_mask: &[f64],
320    nx: usize, ny: usize, nz: usize,
321) -> Vec<f64> {
322    let n_total = nx * ny * nz;
323
324    // Apply brain mask weights
325    let mut temp: Vec<Complex64> = u.iter()
326        .zip(brain_mask.iter())
327        .map(|(&ui, &w)| Complex64::new(ui * w, 0.0))
328        .collect();
329
330    // FFT
331    fft3d(&mut temp, nx, ny, nz);
332
333    // Apply dipole kernel (D is real and symmetric)
334    for i in 0..n_total {
335        temp[i] *= d_kernel[i];
336    }
337
338    // IFFT
339    ifft3d(&mut temp, nx, ny, nz);
340
341    // Apply background mask
342    temp.iter()
343        .zip(bg_mask.iter())
344        .map(|(t, &m)| t.re * m)
345        .collect()
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_pdf_zero_field() {
354        let n = 8;
355        let field = vec![0.0; n * n * n];
356        let mask = vec![1u8; n * n * n];
357        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
358
359        let local = pdf_core(
360            &field, &mask, &grid,
361            (0.0, 0.0, 1.0), 1e-5, 10, |_, _| {}
362        );
363
364        for &val in local.iter() {
365            assert!(val.abs() < 1e-10, "Zero field should give zero local field");
366        }
367    }
368
369    #[test]
370    fn test_pdf_finite() {
371        let n = 8;
372        let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
373
374        // Create a spherical mask in the center
375        let mut mask = vec![0u8; n * n * n];
376        let center = n / 2;
377        let radius = n / 4;
378
379        for i in 0..n {
380            for j in 0..n {
381                for k in 0..n {
382                    let di = (i as i32) - (center as i32);
383                    let dj = (j as i32) - (center as i32);
384                    let dk = (k as i32) - (center as i32);
385                    if di*di + dj*dj + dk*dk <= (radius * radius) as i32 {
386                        mask[i * n * n + j * n + k] = 1;
387                    }
388                }
389            }
390        }
391
392        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
393        let local = pdf_core(
394            &field, &mask, &grid,
395            (0.0, 0.0, 1.0), 1e-5, 20, |_, _| {}
396        );
397
398        for (i, &val) in local.iter().enumerate() {
399            assert!(val.is_finite(), "Local field should be finite at index {}", i);
400        }
401    }
402
403    #[test]
404    fn test_pdf_mask() {
405        let n = 8;
406        let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
407        let mut mask = vec![1u8; n * n * n];
408        mask[0] = 0;
409        mask[10] = 0;
410        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
411
412        let local = pdf_core(
413            &field, &mask, &grid,
414            (0.0, 0.0, 1.0), 1e-5, 10, |_, _| {}
415        );
416
417        assert_eq!(local[0], 0.0, "Masked voxel should be zero");
418        assert_eq!(local[10], 0.0, "Masked voxel should be zero");
419    }
420
421    #[test]
422    fn test_pdf_nonuniform_voxels() {
423        let n = 8;
424        let field: Vec<f64> = (0..n*n*n).map(|i| (i as f64) * 0.001).collect();
425
426        // Create a spherical mask in the center
427        let mut mask = vec![0u8; n * n * n];
428        let center = n / 2;
429        let radius = n / 4;
430
431        for z in 0..n {
432            for y in 0..n {
433                for x in 0..n {
434                    let dx = (x as i32) - (center as i32);
435                    let dy = (y as i32) - (center as i32);
436                    let dz = (z as i32) - (center as i32);
437                    if dx*dx + dy*dy + dz*dz <= (radius * radius) as i32 {
438                        mask[x + y * n + z * n * n] = 1;
439                    }
440                }
441            }
442        }
443
444        // Anisotropic voxel sizes
445        let grid = Grid::new(n, n, n, 0.5, 1.0, 2.0);
446        let local = pdf_core(
447            &field, &mask, &grid,
448            (0.0, 0.0, 1.0), 1e-5, 20, |_, _| {}
449        );
450
451        for (i, &val) in local.iter().enumerate() {
452            assert!(val.is_finite(), "PDF with nonuniform voxels should be finite at index {}", i);
453        }
454
455        // Masked voxels should be zero
456        for i in 0..n*n*n {
457            if mask[i] == 0 {
458                assert_eq!(local[i], 0.0, "Outside mask should be zero");
459            }
460        }
461    }
462
463    #[test]
464    fn test_pdf_varying_field() {
465        let n = 8;
466
467        // Create a spatially varying field (quadratic in z)
468        let mut field = vec![0.0; n * n * n];
469        for z in 0..n {
470            for y in 0..n {
471                for x in 0..n {
472                    let idx = x + y * n + z * n * n;
473                    let zf = (z as f64) / (n as f64);
474                    field[idx] = zf * zf * 0.5;
475                }
476            }
477        }
478
479        // Spherical mask
480        let mut mask = vec![0u8; n * n * n];
481        let center = n / 2;
482        let radius = n / 4;
483        for z in 0..n {
484            for y in 0..n {
485                for x in 0..n {
486                    let dx = (x as i32) - (center as i32);
487                    let dy = (y as i32) - (center as i32);
488                    let dz = (z as i32) - (center as i32);
489                    if dx*dx + dy*dy + dz*dz <= (radius * radius) as i32 {
490                        mask[x + y * n + z * n * n] = 1;
491                    }
492                }
493            }
494        }
495
496        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
497        let local = pdf_core(
498            &field, &mask, &grid,
499            (0.0, 0.0, 1.0), 1e-5, 30, |_, _| {}
500        );
501
502        // All values should be finite
503        for (i, &val) in local.iter().enumerate() {
504            assert!(val.is_finite(), "Varying field should produce finite results at index {}", i);
505        }
506
507        // The local field inside the mask should differ from the total field
508        // (some background was removed)
509        let mut any_changed = false;
510        for i in 0..n*n*n {
511            if mask[i] != 0 && (local[i] - field[i]).abs() > 1e-10 {
512                any_changed = true;
513                break;
514            }
515        }
516        assert!(any_changed, "PDF should modify the field inside the mask for a varying field");
517    }
518
519    #[test]
520    fn test_pdf_larger_volume() {
521        // Use 16x16x16 to exercise more of the LSMR loop
522        let n = 16;
523
524        // Create a dipole-like field pattern
525        let mut field = vec![0.0; n * n * n];
526        let center = n / 2;
527        for z in 0..n {
528            for y in 0..n {
529                for x in 0..n {
530                    let dx = (x as f64) - (center as f64);
531                    let dy = (y as f64) - (center as f64);
532                    let dz = (z as f64) - (center as f64);
533                    let r2 = dx * dx + dy * dy + dz * dz;
534                    if r2 > 0.5 {
535                        // Dipole-like field: (3*cos^2(theta) - 1) / r^3
536                        let r = r2.sqrt();
537                        let cos_theta = dz / r;
538                        field[x + y * n + z * n * n] = (3.0 * cos_theta * cos_theta - 1.0) / (r * r * r) * 0.01;
539                    }
540                }
541            }
542        }
543
544        // Spherical mask
545        let mut mask = vec![0u8; n * n * n];
546        let radius = n / 3;
547        for z in 0..n {
548            for y in 0..n {
549                for x in 0..n {
550                    let dx = (x as i32) - (center as i32);
551                    let dy = (y as i32) - (center as i32);
552                    let dz = (z as i32) - (center as i32);
553                    if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
554                        mask[x + y * n + z * n * n] = 1;
555                    }
556                }
557            }
558        }
559
560        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
561        let local = pdf_core(
562            &field, &mask, &grid,
563            (0.0, 0.0, 1.0), 1e-4, 50, |_, _| {}
564        );
565
566        assert_eq!(local.len(), n * n * n);
567        for (i, &val) in local.iter().enumerate() {
568            assert!(val.is_finite(), "PDF larger volume: finite at index {}", i);
569        }
570
571        // Masked-out voxels must be zero
572        for i in 0..n * n * n {
573            if mask[i] == 0 {
574                assert_eq!(local[i], 0.0);
575            }
576        }
577    }
578
579    #[test]
580    fn test_pdf_more_iterations() {
581        // Test with more LSMR iterations to exercise convergence check
582        let n = 8;
583        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
584
585        let mut mask = vec![0u8; n * n * n];
586        let center = n / 2;
587        let radius = n / 4;
588        for z in 0..n {
589            for y in 0..n {
590                for x in 0..n {
591                    let dx = (x as i32) - (center as i32);
592                    let dy = (y as i32) - (center as i32);
593                    let dz = (z as i32) - (center as i32);
594                    if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
595                        mask[x + y * n + z * n * n] = 1;
596                    }
597                }
598            }
599        }
600
601        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
602
603        // With many iterations the result should converge
604        let local_many = pdf_core(
605            &field, &mask, &grid,
606            (0.0, 0.0, 1.0), 1e-8, 100, |_, _| {}
607        );
608
609        let local_few = pdf_core(
610            &field, &mask, &grid,
611            (0.0, 0.0, 1.0), 1e-8, 5, |_, _| {}
612        );
613
614        // Both should be finite
615        for &val in local_many.iter().chain(local_few.iter()) {
616            assert!(val.is_finite());
617        }
618    }
619
620    #[test]
621    fn test_pdf_different_bdir() {
622        // Test with non-standard B0 direction
623        let n = 8;
624        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
625
626        let mut mask = vec![0u8; n * n * n];
627        let center = n / 2;
628        let radius = n / 4;
629        for z in 0..n {
630            for y in 0..n {
631                for x in 0..n {
632                    let dx = (x as i32) - (center as i32);
633                    let dy = (y as i32) - (center as i32);
634                    let dz = (z as i32) - (center as i32);
635                    if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
636                        mask[x + y * n + z * n * n] = 1;
637                    }
638                }
639            }
640        }
641
642        // Tilted B0 direction
643        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
644        let local = pdf_core(
645            &field, &mask, &grid,
646            (0.1, 0.2, 0.97), 1e-5, 20, |_, _| {}
647        );
648
649        for &val in &local {
650            assert!(val.is_finite());
651        }
652    }
653
654    #[test]
655    fn test_pdf_with_progress() {
656        let n = 8;
657        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
658
659        let mut mask = vec![0u8; n * n * n];
660        let center = n / 2;
661        let radius = n / 4;
662        for z in 0..n {
663            for y in 0..n {
664                for x in 0..n {
665                    let dx = (x as i32) - (center as i32);
666                    let dy = (y as i32) - (center as i32);
667                    let dz = (z as i32) - (center as i32);
668                    if dx * dx + dy * dy + dz * dz <= (radius * radius) as i32 {
669                        mask[x + y * n + z * n * n] = 1;
670                    }
671                }
672            }
673        }
674
675        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
676        let mut progress_calls = Vec::new();
677        let local = pdf_core(
678            &field, &mask, &grid,
679            (0.0, 0.0, 1.0), 1e-5, 20,
680            |iter, max| { progress_calls.push((iter, max)); }
681        );
682
683        assert_eq!(local.len(), n * n * n);
684        assert!(!progress_calls.is_empty(), "Progress should be called at least once");
685        for &val in &local {
686            assert!(val.is_finite());
687        }
688    }
689
690    #[test]
691    fn test_pdf_all_mask() {
692        // All voxels masked (no background) - should still work
693        let n = 8;
694        let field: Vec<f64> = (0..n * n * n).map(|i| (i as f64) * 0.001).collect();
695        let mask = vec![1u8; n * n * n];
696        let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
697
698        let local = pdf_core(
699            &field, &mask, &grid,
700            (0.0, 0.0, 1.0), 1e-5, 10, |_, _| {}
701        );
702
703        // With all voxels as "brain", the background mask is empty
704        // so there's nothing to project onto => local should approximate field
705        for &val in &local {
706            assert!(val.is_finite());
707        }
708    }
709}