Skip to main content

qsm_core/kernels/
dipole.rs

1//! Dipole kernel for QSM
2//!
3//! The dipole kernel describes the relationship between magnetic susceptibility
4//! and the induced magnetic field in MRI. In k-space:
5//!
6//! D(k) = 1/3 - (k·B)² / |k|²
7//!
8//! where B is the B0 field direction (typically [0, 0, 1]).
9
10use crate::fft::fftfreq;
11
12/// Generate dipole kernel in k-space
13///
14/// Creates the dipole kernel D(k) = 1/3 - (kz)²/|k|² (for B = [0,0,1])
15/// centered at index (0, 0, 0) (not shifted).
16///
17/// # Arguments
18/// * `nx`, `ny`, `nz` - Array dimensions
19/// * `vsx`, `vsy`, `vsz` - Voxel sizes in mm
20/// * `bdir` - B0 field direction as (bx, by, bz), default (0, 0, 1)
21///
22/// # Returns
23/// Flattened dipole kernel array of size nx*ny*nz in C order
24pub fn dipole_kernel(
25    nx: usize, ny: usize, nz: usize,
26    vsx: f64, vsy: f64, vsz: f64,
27    bdir: (f64, f64, f64),
28) -> Vec<f64> {
29    let n_total = nx * ny * nz;
30    let mut d = vec![0.0; n_total];
31
32    // Generate frequency grids
33    let kx = fftfreq(nx, vsx);
34    let ky = fftfreq(ny, vsy);
35    let kz = fftfreq(nz, vsz);
36
37    // Normalize B direction
38    let (bx, by, bz) = bdir;
39    let bnorm = (bx * bx + by * by + bz * bz).sqrt();
40    let bx = bx / bnorm;
41    let by = by / bnorm;
42    let bz = bz / bnorm;
43
44    let one_third = 1.0 / 3.0;
45
46    // Compute dipole kernel: D(k) = 1/3 - (k·B)² / |k|²
47    // Fortran order: index = i + j*nx + k*nx*ny
48    for k in 0..nz {
49        let kz_val = kz[k];
50        for j in 0..ny {
51            let ky_val = ky[j];
52            for i in 0..nx {
53                let kx_val = kx[i];
54
55                let k_dot_b = kx_val * bx + ky_val * by + kz_val * bz;
56                let k_squared = kx_val * kx_val + ky_val * ky_val + kz_val * kz_val;
57
58                let idx = i + j * nx + k * nx * ny;
59
60                if k_squared > 1e-20 {
61                    d[idx] = one_third - (k_dot_b * k_dot_b) / k_squared;
62                } else {
63                    // DC term (k = 0)
64                    d[idx] = 0.0;
65                }
66            }
67        }
68    }
69
70    d
71}
72
73/// Generate dipole kernel with default B direction (0, 0, 1)
74pub fn dipole_kernel_default(
75    nx: usize, ny: usize, nz: usize,
76    vsx: f64, vsy: f64, vsz: f64,
77) -> Vec<f64> {
78    dipole_kernel(nx, ny, nz, vsx, vsy, vsz, (0.0, 0.0, 1.0))
79}
80
81// ============================================================================
82// F32 (Single Precision) Dipole Kernel Functions
83// ============================================================================
84
85use crate::fft::fftfreq_f32;
86
87/// Generate dipole kernel in k-space (f32 version for WASM performance)
88///
89/// Creates the dipole kernel D(k) = 1/3 - (kz)²/|k|² (for B = [0,0,1])
90/// centered at index (0, 0, 0) (not shifted).
91pub fn dipole_kernel_f32(
92    nx: usize, ny: usize, nz: usize,
93    vsx: f32, vsy: f32, vsz: f32,
94    bdir: (f32, f32, f32),
95) -> Vec<f32> {
96    let n_total = nx * ny * nz;
97    let mut d = vec![0.0f32; n_total];
98
99    // Generate frequency grids
100    let kx = fftfreq_f32(nx, vsx);
101    let ky = fftfreq_f32(ny, vsy);
102    let kz = fftfreq_f32(nz, vsz);
103
104    // Normalize B direction
105    let (bx, by, bz) = bdir;
106    let bnorm = (bx * bx + by * by + bz * bz).sqrt();
107    let bx = bx / bnorm;
108    let by = by / bnorm;
109    let bz = bz / bnorm;
110
111    let one_third = 1.0f32 / 3.0;
112
113    // Compute dipole kernel: D(k) = 1/3 - (k·B)² / |k|²
114    // Fortran order: index = i + j*nx + k*nx*ny
115    for k in 0..nz {
116        let kz_val = kz[k];
117        for j in 0..ny {
118            let ky_val = ky[j];
119            for i in 0..nx {
120                let kx_val = kx[i];
121
122                let k_dot_b = kx_val * bx + ky_val * by + kz_val * bz;
123                let k_squared = kx_val * kx_val + ky_val * ky_val + kz_val * kz_val;
124
125                let idx = i + j * nx + k * nx * ny;
126
127                if k_squared > 1e-10 {
128                    d[idx] = one_third - (k_dot_b * k_dot_b) / k_squared;
129                } else {
130                    // DC term (k = 0)
131                    d[idx] = 0.0;
132                }
133            }
134        }
135    }
136
137    d
138}
139
140/// Generate f32 dipole kernel with default B direction (0, 0, 1)
141pub fn dipole_kernel_default_f32(
142    nx: usize, ny: usize, nz: usize,
143    vsx: f32, vsy: f32, vsz: f32,
144) -> Vec<f32> {
145    dipole_kernel_f32(nx, ny, nz, vsx, vsy, vsz, (0.0, 0.0, 1.0))
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_dipole_kernel_dc() {
154        let d = dipole_kernel_default(4, 4, 4, 1.0, 1.0, 1.0);
155        // DC component should be 0
156        assert!(d[0].abs() < 1e-10, "DC component should be 0, got {}", d[0]);
157    }
158
159    #[test]
160    fn test_dipole_kernel_range() {
161        let d = dipole_kernel_default(8, 8, 8, 1.0, 1.0, 1.0);
162
163        // Dipole kernel should be in range [-2/3, 1/3]
164        for (i, &val) in d.iter().enumerate() {
165            if i > 0 {  // Skip DC
166                assert!(val >= -2.0/3.0 - 1e-10 && val <= 1.0/3.0 + 1e-10,
167                    "Dipole value {} out of range at index {}", val, i);
168            }
169        }
170    }
171
172    #[test]
173    fn test_dipole_kernel_symmetry() {
174        // Dipole kernel should be symmetric
175        let n = 8;
176        let d = dipole_kernel_default(n, n, n, 1.0, 1.0, 1.0);
177
178        // D(kx, ky, kz) should equal D(-kx, -ky, -kz)
179        for i in 1..n/2 {
180            let i_neg = n - i;
181            for j in 1..n/2 {
182                let j_neg = n - j;
183                for k in 1..n/2 {
184                    let k_neg = n - k;
185
186                    let idx1 = i * n * n + j * n + k;
187                    let idx2 = i_neg * n * n + j_neg * n + k_neg;
188
189                    let diff = (d[idx1] - d[idx2]).abs();
190                    assert!(diff < 1e-10,
191                        "Symmetry broken at ({},{},{}) vs ({},{},{}): {} vs {}",
192                        i, j, k, i_neg, j_neg, k_neg, d[idx1], d[idx2]);
193                }
194            }
195        }
196    }
197}