1use num_complex::Complex64;
70
71use crate::fft::Fft3dWorkspace;
72use crate::utils::apply_mask_zero;
73use crate::Grid;
74
75use super::ilsqr::lsqr_matlab;
76
77pub const RESIDUAL_WEIGHTING_PER_TESLA: f64 = 0.2 / 9.4;
79
80pub const ANTIALIASING_FRAME: usize = 40;
83
84#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
86#[derive(Clone, Debug)]
87pub struct LsqrQsmParams {
88 pub b0: f64,
91 pub residual_weighting: Option<f64>,
95 pub fit_global_offset: bool,
97 pub tol: f64,
99 pub max_iter: usize,
101 pub mask_output: bool,
104}
105
106impl Default for LsqrQsmParams {
107 fn default() -> Self {
109 Self {
110 b0: 3.0,
111 residual_weighting: None,
112 fit_global_offset: true,
113 tol: 1e-5,
114 max_iter: 400,
115 mask_output: true,
116 }
117 }
118}
119
120impl LsqrQsmParams {
121 pub fn effective_residual_weighting(&self) -> f64 {
123 self.residual_weighting
124 .unwrap_or(RESIDUAL_WEIGHTING_PER_TESLA * self.b0)
125 }
126}
127
128pub(crate) type CropBox = [(usize, usize, usize); 3];
130
131pub(crate) fn crop_grid(mask: &[u8], dims: (usize, usize, usize)) -> Option<CropBox> {
140 let (nx, ny, nz) = dims;
141 let (mut lo, mut hi) = ([usize::MAX; 3], [0usize; 3]);
142 for k in 0..nz {
143 for j in 0..ny {
144 for i in 0..nx {
145 if mask[i + nx * (j + ny * k)] != 0 {
146 for (d, c) in [i, j, k].into_iter().enumerate() {
147 lo[d] = lo[d].min(c);
148 hi[d] = hi[d].max(c);
149 }
150 }
151 }
152 }
153 }
154 if lo[0] == usize::MAX {
155 return None;
156 }
157 Some(std::array::from_fn(|d| {
158 let e = hi[d] - lo[d] + 1;
159 (lo[d], e, e + e % 2 + ANTIALIASING_FRAME - 1)
160 }))
161}
162
163pub(crate) const FRAME_OFFSET: usize = ANTIALIASING_FRAME / 2;
165
166pub(crate) fn crop_pad<T: Copy + Default>(x: &[T], dims: (usize, usize, usize), crop: &CropBox) -> Vec<T> {
169 let (nx, ny, _) = dims;
170 let (px, py, pz) = (crop[0].2, crop[1].2, crop[2].2);
171 let mut out = vec![T::default(); px * py * pz];
172 for k in 0..crop[2].1 {
173 for j in 0..crop[1].1 {
174 let src = crop[0].0 + nx * (crop[1].0 + j + ny * (crop[2].0 + k));
175 let dst = FRAME_OFFSET + px * (FRAME_OFFSET + j + py * (FRAME_OFFSET + k));
176 out[dst..dst + crop[0].1].copy_from_slice(&x[src..src + crop[0].1]);
177 }
178 }
179 out
180}
181
182pub(crate) fn uncrop(x: &[f64], dims: (usize, usize, usize), crop: &CropBox) -> Vec<f64> {
184 let (nx, ny, nz) = dims;
185 let (px, py) = (crop[0].2, crop[1].2);
186 let mut out = vec![0.0; nx * ny * nz];
187 for k in 0..crop[2].1 {
188 for j in 0..crop[1].1 {
189 let dst = crop[0].0 + nx * (crop[1].0 + j + ny * (crop[2].0 + k));
190 let src = FRAME_OFFSET + px * (FRAME_OFFSET + j + py * (FRAME_OFFSET + k));
191 out[dst..dst + crop[0].1].copy_from_slice(&x[src..src + crop[0].1]);
192 }
193 }
194 out
195}
196
197pub fn spatial_dipole_kernel(grid: &Grid, bdir: (f64, f64, f64)) -> Vec<f64> {
207 let dims = [grid.dims.0, grid.dims.1, grid.dims.2];
208 let vs = [grid.voxel_size.0, grid.voxel_size.1, grid.voxel_size.2];
209 assert!(dims.iter().all(|&n| n % 2 == 1), "the spatial dipole kernel needs odd extents, got {dims:?}");
210 let vmin = vs.iter().cloned().fold(f64::INFINITY, f64::min);
211 let fine: [usize; 3] = std::array::from_fn(|d| {
212 let x = dims[d] as f64 * vs[d] / vmin;
213 2 * (x / 2.0).floor() as usize + 1
214 });
215 let bn = (bdir.0 * bdir.0 + bdir.1 * bdir.1 + bdir.2 * bdir.2).sqrt();
216 let b = [bdir.0 / bn, bdir.1 / bn, bdir.2 / bn];
217 let coord = |i: usize, n: usize| if i <= (n - 1) / 2 { i as f64 } else { i as f64 - n as f64 };
219 let (fx, fy, fz) = (fine[0], fine[1], fine[2]);
220 let mut k: Vec<Complex64> = vec![Complex64::new(0.0, 0.0); fx * fy * fz];
221 for iz in 0..fz {
222 let z = coord(iz, fz);
223 for iy in 0..fy {
224 let y = coord(iy, fy);
225 for ix in 0..fx {
226 let x = coord(ix, fx);
227 let r2 = x * x + y * y + z * z;
228 if r2 == 0.0 {
229 continue;
230 }
231 let r = r2.sqrt();
232 let c = (b[0] * x + b[1] * y + b[2] * z) / r;
233 k[ix + fx * (iy + fy * iz)] = Complex64::new((3.0 * c * c - 1.0) / (r2 * r * 4.0 * std::f64::consts::PI), 0.0);
234 }
235 }
236 }
237 Fft3dWorkspace::new(fx, fy, fz).fft3d(&mut k);
238 let fidx = |i: usize, n: usize, f: usize| if i <= (n - 1) / 2 { i } else { f + i - n };
240 let (nx, ny, nz) = grid.dims;
241 let mut d = vec![0.0; nx * ny * nz];
242 for iz in 0..nz {
243 let jz = fidx(iz, nz, fz);
244 for iy in 0..ny {
245 let jy = fidx(iy, ny, fy);
246 for ix in 0..nx {
247 d[ix + nx * (iy + ny * iz)] = k[fidx(ix, nx, fx) + fx * (jy + fy * jz)].re;
248 }
249 }
250 }
251 d[0] = 0.0;
252 d
253}
254
255pub(crate) fn lsqr_qsm_padded(
258 local_field: &[f64],
259 mask: &[u8],
260 magnitude: Option<&[f64]>,
261 grid: &Grid,
262 bdir: (f64, f64, f64),
263 params: &LsqrQsmParams,
264 progress: impl FnMut(usize, usize),
265) -> Option<(Grid, CropBox, Vec<f64>)> {
266 let n_total = grid.n_total();
267 assert_eq!(local_field.len(), n_total, "field length must match grid");
268 assert_eq!(mask.len(), n_total, "mask length must match grid");
269 if let Some(m) = magnitude {
270 assert_eq!(m.len(), n_total, "magnitude length must match grid");
271 }
272 let crop = crop_grid(mask, grid.dims)?;
273 let (vsx, vsy, vsz) = grid.voxel_size;
274 let pgrid = Grid::new(crop[0].2, crop[1].2, crop[2].2, vsx, vsy, vsz);
275 let field = crop_pad(local_field, grid.dims, &crop);
276 let pmask = crop_pad(mask, grid.dims, &crop);
277 let pmag = magnitude.map(|m| crop_pad(m, grid.dims, &crop));
278 let chi = lsqr_qsm_core(&field, &pmask, pmag.as_deref(), &pgrid, bdir, params, progress);
279 Some((pgrid, crop, chi))
280}
281
282pub fn lsqr_qsm(
298 local_field: &[f64],
299 mask: &[u8],
300 magnitude: Option<&[f64]>,
301 grid: &Grid,
302 bdir: (f64, f64, f64),
303 params: &LsqrQsmParams,
304 progress: impl FnMut(usize, usize),
305) -> Vec<f64> {
306 let Some((_, crop, chi)) = lsqr_qsm_padded(local_field, mask, magnitude, grid, bdir, params, progress) else {
307 return vec![0.0; grid.n_total()];
308 };
309 let mut chi = uncrop(&chi, grid.dims, &crop);
310 if params.mask_output {
311 apply_mask_zero(&mut chi, mask);
312 }
313 chi
314}
315
316fn lsqr_qsm_core(
317 local_field: &[f64],
318 mask: &[u8],
319 magnitude: Option<&[f64]>,
320 grid: &Grid,
321 bdir: (f64, f64, f64),
322 params: &LsqrQsmParams,
323 progress: impl FnMut(usize, usize),
324) -> Vec<f64> {
325 let (nx, ny, nz) = grid.dims;
326 let n_total = grid.n_total();
327
328 let mask_idx: Vec<usize> = (0..n_total).filter(|&i| mask[i] != 0).collect();
330 let m = mask_idx.len();
331
332 let w_res = params.effective_residual_weighting();
333 let use_residual = w_res != 0.0;
334 let n_res = if use_residual { m } else { 0 };
335 let n_offset = usize::from(params.fit_global_offset);
336 let n_unknown = n_total + n_res + n_offset;
337
338 let row_weight: Vec<f64> = match magnitude {
340 Some(mag) => mask_idx.iter().map(|&i| mag[i]).collect(),
341 None => vec![1.0; m],
342 };
343
344 let b: Vec<Complex64> = mask_idx
347 .iter()
348 .zip(&row_weight)
349 .map(|(&i, &w)| {
350 let v = local_field[i] * w;
351 Complex64::new(if v.is_finite() { v } else { 0.0 }, 0.0)
352 })
353 .collect();
354
355 let d = spatial_dipole_kernel(grid, bdir);
356 let mut fft = Fft3dWorkspace::new(nx, ny, nz);
357 let mut buf = vec![Complex64::new(0.0, 0.0); n_total];
358 let mut apply_dipole = |x: &[Complex64], buf: &mut Vec<Complex64>| {
359 for (o, v) in buf.iter_mut().zip(x) {
360 *o = Complex64::new(v.re, 0.0);
361 }
362 fft.fft3d(buf);
363 for (o, &dk) in buf.iter_mut().zip(&d) {
364 *o *= dk;
365 }
366 fft.ifft3d(buf);
367 for o in buf.iter_mut() {
369 o.im = 0.0;
370 }
371 };
372
373 let mut progress = progress;
374 let max_iter = params.max_iter;
375 let mut calls = 0usize;
376 let buf_cell = std::cell::RefCell::new(&mut buf);
377 let apply_dipole_cell = std::cell::RefCell::new(&mut apply_dipole);
378
379 let apply_a = |x: &[Complex64]| -> Vec<Complex64> {
380 calls += 1;
381 progress(calls.min(max_iter), max_iter);
382 let mut buf = buf_cell.borrow_mut();
383 (apply_dipole_cell.borrow_mut())(&x[..n_total], &mut buf);
384 let offset = if n_offset == 1 { x[n_total + n_res].re } else { 0.0 };
385 mask_idx
386 .iter()
387 .zip(&row_weight)
388 .enumerate()
389 .map(|(row, (&idx, &w))| {
390 let mut v = buf[idx].re + offset;
391 if use_residual {
392 v += w_res * x[n_total + row].re;
393 }
394 Complex64::new(w * v, 0.0)
395 })
396 .collect()
397 };
398
399 let apply_at = |y: &[Complex64]| -> Vec<Complex64> {
400 let mut scattered = vec![Complex64::new(0.0, 0.0); n_total];
402 for (row, (&idx, &w)) in mask_idx.iter().zip(&row_weight).enumerate() {
403 scattered[idx] = Complex64::new(y[row].re * w, 0.0);
404 }
405 let mut out = vec![Complex64::new(0.0, 0.0); n_unknown];
406 if use_residual {
407 for (row, &idx) in mask_idx.iter().enumerate() {
408 out[n_total + row] = scattered[idx] * w_res;
409 }
410 }
411 if n_offset == 1 {
412 out[n_total + n_res] = Complex64::new(mask_idx.iter().map(|&i| scattered[i].re).sum(), 0.0);
413 }
414 let mut buf = buf_cell.borrow_mut();
416 (apply_dipole_cell.borrow_mut())(&scattered, &mut buf);
417 out[..n_total].copy_from_slice(&buf[..]);
418 out
419 };
420
421 let (solution, _) = lsqr_matlab(apply_a, apply_at, &b, params.tol, params.max_iter);
422 solution[..n_total].iter().map(|c| c.re).collect()
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428 use crate::kernels::dipole::dipole_kernel;
429
430 fn test_grid(n: usize) -> Grid {
431 Grid::new(n, n, n, 1.0, 1.0, 1.0)
432 }
433
434 #[test]
435 fn zero_field_gives_zero_chi() {
436 let g = test_grid(12);
437 let n = g.n_total();
438 let chi = lsqr_qsm(
439 &vec![0.0; n],
440 &vec![1u8; n],
441 None,
442 &g,
443 (0.0, 0.0, 1.0),
444 &LsqrQsmParams::default(),
445 |_, _| {},
446 );
447 assert!(chi.iter().all(|v| v.abs() < 1e-12), "expected an all-zero map");
448 }
449
450 #[test]
451 fn empty_mask_is_handled() {
452 let g = test_grid(8);
453 let n = g.n_total();
454 let chi = lsqr_qsm(
455 &vec![0.1; n],
456 &vec![0u8; n],
457 None,
458 &g,
459 (0.0, 0.0, 1.0),
460 &LsqrQsmParams::default(),
461 |_, _| {},
462 );
463 assert_eq!(chi.len(), n);
464 assert!(chi.iter().all(|v| *v == 0.0));
465 }
466
467 #[test]
469 fn padding_path_preserves_dimensions() {
470 let (nx, ny, nz) = (7usize, 11usize, 13usize);
471 let g = Grid::new(nx, ny, nz, 1.0, 1.0, 1.0);
472 let n_total = g.n_total();
473 let field: Vec<f64> = (0..n_total).map(|i| ((i as f64) * 0.21).sin() * 0.02).collect();
474 let chi = lsqr_qsm(
475 &field,
476 &vec![1u8; n_total],
477 None,
478 &g,
479 (0.0, 0.0, 1.0),
480 &LsqrQsmParams { max_iter: 20, ..Default::default() },
481 |_, _| {},
482 );
483 assert_eq!(chi.len(), n_total);
484 assert!(chi.iter().all(|v| v.is_finite()));
485 assert!(chi.iter().any(|v| v.abs() > 0.0));
486 }
487
488 #[test]
490 fn recovers_a_sphere() {
491 let n = 32;
492 let g = test_grid(n);
493 let n_total = g.n_total();
494 let d = dipole_kernel(&g, (0.0, 0.0, 1.0));
495
496 let c = n as f64 / 2.0;
497 let mut chi_true = vec![0.0; n_total];
498 let mut mask = vec![0u8; n_total];
499 for k in 0..n {
500 for j in 0..n {
501 for i in 0..n {
502 let idx = i + j * n + k * n * n;
503 let r = ((i as f64 - c).powi(2)
504 + (j as f64 - c).powi(2)
505 + (k as f64 - c).powi(2))
506 .sqrt();
507 if r < 4.0 {
508 chi_true[idx] = 0.1;
509 }
510 if r < 11.0 {
511 mask[idx] = 1;
512 }
513 }
514 }
515 }
516
517 let mut ws = Fft3dWorkspace::new(n, n, n);
519 let mut buf: Vec<Complex64> =
520 chi_true.iter().map(|&v| Complex64::new(v, 0.0)).collect();
521 ws.fft3d(&mut buf);
522 for (b, &dk) in buf.iter_mut().zip(&d) {
523 *b *= dk;
524 }
525 ws.ifft3d(&mut buf);
526 let field: Vec<f64> = buf.iter().map(|c| c.re).collect();
527
528 let params = LsqrQsmParams {
529 residual_weighting: Some(0.0),
530 fit_global_offset: false,
531 max_iter: 120,
532 ..Default::default()
533 };
534 let chi = lsqr_qsm(&field, &mask, None, &g, (0.0, 0.0, 1.0), ¶ms, |_, _| {});
535
536 let (sum, count) = chi_true.iter().zip(&chi).fold((0.0, 0usize), |(s, c), (t, r)| {
541 if *t > 0.0 { (s + r, c + 1) } else { (s, c) }
542 });
543 let mean = sum / count as f64;
544 assert!(
545 (0.04..0.12).contains(&mean),
546 "sphere mean {mean} is outside the expected range for a min-norm solve"
547 );
548
549 let n_vox = chi.len() as f64;
550 let mean_true: f64 = chi_true.iter().sum::<f64>() / n_vox;
551 let mean_rec: f64 = chi.iter().sum::<f64>() / n_vox;
552 let (mut cov, mut var_t, mut var_r) = (0.0, 0.0, 0.0);
553 for (t, r) in chi_true.iter().zip(&chi) {
554 let (a, b) = (t - mean_true, r - mean_rec);
555 cov += a * b;
556 var_t += a * a;
557 var_r += b * b;
558 }
559 let corr = cov / (var_t.sqrt() * var_r.sqrt());
560 assert!(corr > 0.8, "recovered map correlates only {corr} with the truth");
561 }
562
563 #[test]
565 fn crop_grid_matches_prepareforconvolution() {
566 let dims = (20, 21, 22);
567 let mut mask = vec![0u8; 20 * 21 * 22];
568 for j in 2..=11 {
570 for i in 3..=9 {
571 mask[i + 20 * (j + 21 * 5)] = 1;
572 }
573 }
574 let c = crop_grid(&mask, dims).unwrap();
575 assert_eq!(c, [(3, 7, 47), (2, 10, 49), (5, 1, 41)]);
576 assert!(crop_grid(&vec![0u8; 20 * 21 * 22], dims).is_none());
577 let x: Vec<f64> = (0..mask.len()).map(|i| i as f64).collect();
578 let back = uncrop(&crop_pad(&x, dims, &c), dims, &c);
579 for (i, (&a, &b)) in x.iter().zip(&back).enumerate() {
580 let (ii, jj, kk) = (i % 20, (i / 20) % 21, i / 420);
581 let inside = (3..=9).contains(&ii) && (2..=11).contains(&jj) && kk == 5;
582 assert_eq!(b, if inside { a } else { 0.0 });
583 }
584 }
585
586 #[test]
589 fn spatial_dipole_kernel_approximates_the_kspace_kernel() {
590 let n = 33;
591 let g = Grid::new(n, n, n, 1.0, 1.0, 1.0);
592 let d = spatial_dipole_kernel(&g, (0.0, 0.0, 1.0));
593 assert_eq!(d[0], 0.0);
594 let at = |i: usize, j: usize, k: usize| d[i + n * (j + n * k)];
595 assert!((at(3, 5, 7) - at(n - 3, n - 5, n - 7)).abs() < 1e-12);
597 assert!((at(0, 0, 2) + 2.0 / 3.0).abs() < 0.05, "{}", at(0, 0, 2));
599 assert!((at(2, 0, 0) - 1.0 / 3.0).abs() < 0.05, "{}", at(2, 0, 0));
600 let ga = Grid::new(9, 9, 9, 1.0, 1.0, 2.0);
602 let da = spatial_dipole_kernel(&ga, (0.0, 0.0, 1.0));
603 assert_eq!(da.len(), 729);
604 assert_eq!(da[0], 0.0);
605 assert!(da.iter().all(|v| v.is_finite()));
606 }
607
608 #[test]
609 #[should_panic(expected = "odd extents")]
610 fn spatial_dipole_kernel_rejects_even_grids() {
611 spatial_dipole_kernel(&Grid::new(8, 9, 9, 1.0, 1.0, 1.0), (0.0, 0.0, 1.0));
612 }
613
614 #[test]
617 fn magnitude_scale_does_not_matter() {
618 let n = 10;
619 let g = test_grid(n);
620 let nt = g.n_total();
621 let field: Vec<f64> = (0..nt).map(|i| ((i as f64) * 0.37).sin() * 0.02).collect();
622 let mag: Vec<f64> = (0..nt).map(|i| 1.0 + ((i as f64) * 0.11).cos().abs()).collect();
623 let mag2: Vec<f64> = mag.iter().map(|v| v * 1024.0).collect();
624 let mask = vec![1u8; nt];
625 let p = LsqrQsmParams { max_iter: 15, ..Default::default() };
626 let a = lsqr_qsm(&field, &mask, Some(&mag), &g, (0.0, 0.0, 1.0), &p, |_, _| {});
627 let b = lsqr_qsm(&field, &mask, Some(&mag2), &g, (0.0, 0.0, 1.0), &p, |_, _| {});
628 for (x, y) in a.iter().zip(&b) {
629 assert_eq!(x.to_bits(), y.to_bits(), "{x} vs {y}");
630 }
631 }
632
633 #[test]
634 fn residual_weighting_scales_with_field_strength() {
635 let p = LsqrQsmParams { b0: 9.4, residual_weighting: None, ..Default::default() };
636 assert!((p.effective_residual_weighting() - 0.2).abs() < 1e-12);
637
638 let p3 = LsqrQsmParams { b0: 3.0, residual_weighting: None, ..Default::default() };
639 assert!((p3.effective_residual_weighting() - 0.2 / 9.4 * 3.0).abs() < 1e-12);
640
641 let fixed = LsqrQsmParams { residual_weighting: Some(0.5), ..Default::default() };
642 assert_eq!(fixed.effective_residual_weighting(), 0.5);
643 }
644}