1use num_complex::Complex64;
18
19use crate::fft::{fft3d_real, ifft3d_real};
20use crate::grid::Grid;
21use crate::models::onnx::{OnnxModel, OnnxError, Tensor};
22
23pub const NEXTQSM_LAMBDAS: [f64; 6] =
26 [15.141516, 15.005543, 16.733437, 14.574243, 7.072418, 0.7588475];
27
28pub fn nextqsm(
37 total_field: &[f64],
38 mask: &[u8],
39 grid: &Grid,
40 bdir: (f64, f64, f64),
41 bf_onnx: &[u8],
42 vjp_onnx: &[u8],
43) -> Result<Vec<f64>, OnnxError> {
44 let (nx, ny, nz) = grid.dims;
45 let n = nx * ny * nz;
46 assert_eq!(total_field.len(), n);
47 assert_eq!(mask.len(), n);
48
49 let pad = |s: usize| -> (usize, usize) {
51 let total = (64 - s % 64) % 64;
52 (total / 2, s + total)
53 };
54 let (bx, px) = pad(nx);
55 let (by, py) = pad(ny);
56 let (bz, pz) = pad(nz);
57 let pgrid = Grid { dims: (px, py, pz), voxel_size: grid.voxel_size };
58 let np = px * py * pz;
59
60 let mut field_p = vec![0.0f64; np];
61 let mut mask_p = vec![0u8; np];
62 for z in 0..nz {
63 for y in 0..ny {
64 for x in 0..nx {
65 let s = x + nx * (y + ny * z);
66 let d = (x + bx) + px * ((y + by) + py * (z + bz));
67 field_p[d] = total_field[s];
68 mask_p[d] = mask[s];
69 }
70 }
71 }
72
73 let chi_p = nextqsm_padded(&field_p, &mask_p, &pgrid, bdir, bf_onnx, vjp_onnx, &NEXTQSM_LAMBDAS)?;
74
75 let mut chi = vec![0.0f64; n];
77 for z in 0..nz {
78 for y in 0..ny {
79 for x in 0..nx {
80 chi[x + nx * (y + ny * z)] = chi_p[(x + bx) + px * ((y + by) + py * (z + bz))];
81 }
82 }
83 }
84 Ok(chi)
85}
86
87#[allow(clippy::too_many_arguments)]
90pub fn nextqsm_padded(
91 field: &[f64],
92 mask: &[u8],
93 grid: &Grid,
94 bdir: (f64, f64, f64),
95 bf_onnx: &[u8],
96 vjp_onnx: &[u8],
97 lambdas: &[f64],
98) -> Result<Vec<f64>, OnnxError> {
99 let (nx, ny, nz) = grid.dims;
100 let n = nx * ny * nz;
101 let maskf: Vec<f64> = mask.iter().map(|&m| m as f64).collect();
102
103 let kernel = dipole_kernel_shifted(grid, bdir);
105
106 let bf = OnnxModel::load(bf_onnx)?;
108 let masked_field: Vec<f64> = field.iter().zip(&maskf).map(|(&f, &m)| f * m).collect();
109 let bf_out = run_unet(&bf, &masked_field, grid)?;
110 let bf_logits: Vec<f64> = bf_out.iter().zip(&maskf).map(|(&v, &m)| v * m).collect();
111 let norm_bf = l2(&bf_logits);
112
113 let vjp = OnnxModel::load(vjp_onnx)?;
114 let mut x = bf_logits.clone();
115 for &lam in lambdas.iter() {
116 let dx = dipole_forward(&x, &kernel, grid);
117 let u: Vec<f64> = bf_logits.iter().zip(&dx).map(|(&b, &d)| b - d).collect();
119 let norm_u = l2(&u);
120 let resid: Vec<f64> = dx.iter().zip(&bf_logits).map(|(&d, &b)| d - b).collect();
121 let d_resid = dipole_forward(&resid, &kernel, grid);
122 let scale = if norm_bf > 0.0 && norm_u > 0.0 { 100.0 / (norm_bf * norm_u) } else { 0.0 };
123
124 let grad_r = run_unet(&vjp, &x, grid)?;
127 let inv_n = 1.0 / (n as f64);
128
129 for i in 0..n {
130 let de = lam * (scale * d_resid[i]) + grad_r[i] * inv_n;
131 x[i] = (x[i] - de) * maskf[i];
132 }
133 }
134 Ok(x)
135}
136
137pub(crate) fn dipole_forward(y: &[f64], kernel_shifted: &[f64], grid: &Grid) -> Vec<f64> {
139 let (nx, ny, nz) = grid.dims;
140 let mut spec = fft3d_real(y, nx, ny, nz);
141 for (s, &k) in spec.iter_mut().zip(kernel_shifted) {
142 *s *= Complex64::new(k, 0.0);
143 }
144 ifft3d_real(&spec, nx, ny, nz)
145}
146
147pub(crate) fn dipole_kernel_shifted(grid: &Grid, bdir: (f64, f64, f64)) -> Vec<f64> {
149 let (nx, ny, nz) = grid.dims;
150 let (vx, vy, vz) = grid.voxel_size;
151 let eps = f32::EPSILON as f64;
152 let (bx, by, bz) = bdir;
153 let mut k = vec![0.0f64; nx * ny * nz];
154 let sh = |i: usize, nsz: usize| (i + nsz / 2) % nsz; for z in 0..nz {
157 for y in 0..ny {
158 for x in 0..nx {
159 let rx = ((x as f64) - (nx as f64) / 2.0) / ((nx as f64) / 2.0) / (2.0 * vx);
161 let ry = ((y as f64) - (ny as f64) / 2.0) / ((ny as f64) / 2.0) / (2.0 * vy);
162 let rz = ((z as f64) - (nz as f64) / 2.0) / ((nz as f64) / 2.0) / (2.0 * vz);
163 let r2 = rx * rx + ry * ry + rz * rz;
164 let dot = rx * bx + ry * by + rz * bz;
165 let val = 1.0 / 3.0 - (dot * dot) / (r2 + eps);
166 let (sx, sy, sz) = (sh(x, nx), sh(y, ny), sh(z, nz));
168 k[sx + nx * (sy + ny * sz)] = val;
169 }
170 }
171 }
172 k
173}
174
175fn run_unet(model: &OnnxModel, vol: &[f64], grid: &Grid) -> Result<Vec<f64>, OnnxError> {
177 let (nx, ny, nz) = grid.dims;
178 let mut input = vec![0.0f32; nx * ny * nz];
179 for z in 0..nz {
180 for y in 0..ny {
181 for x in 0..nx {
182 input[(x * ny + y) * nz + z] = vol[x + nx * (y + ny * z)] as f32;
183 }
184 }
185 }
186 let out = model.run_single(&Tensor::new(vec![1, 1, nx, ny, nz], input))?;
187 let mut v = vec![0.0f64; nx * ny * nz];
188 for z in 0..nz {
189 for y in 0..ny {
190 for x in 0..nx {
191 v[x + nx * (y + ny * z)] = out.data[(x * ny + y) * nz + z] as f64;
192 }
193 }
194 }
195 Ok(v)
196}
197
198fn l2(v: &[f64]) -> f64 {
199 v.iter().map(|&a| a * a).sum::<f64>().sqrt()
200}
201
202#[allow(clippy::too_many_arguments)]
207pub fn nextqsm_tiled(
208 total_field: &[f64],
209 mask: &[u8],
210 grid: &Grid,
211 bdir: (f64, f64, f64),
212 bf_onnx: &[u8],
213 vjp_onnx: &[u8],
214 cfg: &super::tiled::TileConfig,
215 progress: impl FnMut(usize, usize),
216) -> Result<Vec<f64>, OnnxError> {
217 super::tiled::tiled_volume_algorithm(
218 total_field, mask, grid, 64, cfg,
219 |f, m, g| nextqsm(f, m, g, bdir, bf_onnx, vjp_onnx),
220 progress,
221 )
222}