qsm_core/bgremove/lbv.rs
1//! Laplacian Boundary Value (LBV) background field removal
2//!
3//! LBV removes the background field by solving the Laplace equation inside the mask: the local
4//! field `fL` satisfies `∇²fL = ∇²fT` on the mask's interior and is 0 on its boundary layer, so
5//! the background `fT − fL` is harmonic there and matches the total field `fT` on the boundary.
6//!
7//! # Port of `LBV.m` and `mexMGv6`
8//! This module is a port of the authors' code: `functions/_LBV/LBV.m` and its C++ solver
9//! `mexMGv6.cpp` (Dong Zhou, 2013) in the MEDI toolbox, release 2024.11.26. The solver is a full
10//! multigrid (FMG) with Gauss–Seidel relaxation, ported step for step, including its quirks:
11//!
12//! - **Grids.** Level 0 is the input grid; level `d+1` has `(1 + n_d) / 2` voxels along each axis
13//! and samples the level-0 grid at `2^(d+1)` × the index (`MG::initializeMem`). Its mask
14//! samples the (peeled) level-0 mask the same way (`MG::initializeROI`), and every level's
15//! *interior* is its mask minus the voxels on the array edge and those with a 6-neighbour
16//! outside the mask (`MG::createInterior`). Only interior voxels are ever updated.
17//! - **Depth.** `depth` is the coarsest level. Automatic (`depth` −1 in `LBV.m`, `None` here):
18//! `⌊log2 m⌋ − 3` for the smallest dimension `m`, at least 0 (`MG::determineDepth`). mexMGv6
19//! holds at most 10 levels, so `depth` ≤ 8.
20//! - **Right-hand side.** Each level's source `ρ_d` is the 7-point Laplacian of the total field
21//! sampled onto that level, with the *finest* voxel size on every level
22//! (`MG::initializeRho`; the coarse grids are not rescaled by `2^d`).
23//! - **Relaxation** (`MG::relax`): lexicographic Gauss–Seidel in place,
24//! `v ← (Σ_axes h⁻²(v₊ + v₋) − r) / (2 Σ h⁻²)`. On the coarsest level it ignores the sweep count
25//! and repeats until the summed absolute update over the interior is at most `tol`; the
26//! check runs before the first sweep with the update set to 1, so `tol` ≥ 1 skips the
27//! coarsest solve. `tol` is absolute, in the field's units: LBV.m works on MEDI's `iFreq`,
28//! radians accumulated over the echo spacing, so [`lbv`] converts the field from ppm to
29//! those units first ([`LbvParams::te`]) and back at the end.
30//! - **FMG** (`MG::FMG`): solve the coarsest level, then on each finer level prolong the coarser
31//! solution (`MG::C2F`, no correction) and run one V-cycle on `L v = ρ_d`.
32//! - **V-cycle** (`MG::V_cycle`): `N1` relaxations; the residual `f − L v` on the interior,
33//! injected onto the next level (`MG::updateRes`, `MG::F2C`: the even-indexed samples, no
34//! full weighting); zero the coarse solution; recurse; add the prolonged coarse correction
35//! scaled by `4^−(d+1)` for coarse level `d+1` (the absolute level, `MG::C2F`'s `prefac`);
36//! `N2` relaxations. On the coarser levels of a V-cycle the right-hand side is the residual
37//! buffer itself, which `updateRes` overwrites with the new residual before the recursion,
38//! so their post-relaxation runs on that new residual. Kept as in the original.
39//! - **Prolongation** (`MG::C2F`): every coarse mask voxel is copied to its fine position, and
40//! the fine voxels between it and its +x/+y/+z neighbours (edges, face diagonals, the body
41//! diagonal) get the mean of the coarse voxels around them when those are all in the coarse
42//! mask. These writes reach the fine mask's boundary layer and a few voxels just outside the
43//! mask, which the relaxation never resets, so the returned field is non-zero there too
44//! (in silico 1723 boundary voxels, 1.1 % of the norm inside the mask; in vivo 1192, and 64
45//! outside the mask, 2.3 %). `LBV.m` returns them and MEDI's pipeline passes that field to
46//! `MEDI_L1` with the full mask, so they are kept; the returned mask is LBV.m's `interior`.
47//! - **Finish** (`MG::postProcessing`): `N3` relaxations on level 0 (to convergence if
48//! `depth` is 0).
49//! - **Peel** (`MG::preProcessing`): `peel` more layers are removed from the mask before the
50//! levels are built, so the interior is the mask minus `peel + 1` layers.
51//!
52//! The authors' defaults are `LbvParams::default()`: `tol` 0.01, automatic depth, `peel` 0 and
53//! `N1`/`N2`/`N3` = 30/100/100 (`LBV.m:29-46`). MEDI's pipeline script has LBV as a commented
54//! alternative to PDF (`README.m:129`) with `tol` 0.005; Ashley's decision (2026-10-30) was to
55//! keep the function's own 0.01.
56//!
57//! ## Precision
58//! mexMGv6 computes in single precision (`float`), with the prolongation's averages and the
59//! coarse-grid correction evaluated in double and rounded (`MG::C2F`). [`LbvPrecision::Single`]
60//! reproduces that arithmetic operation for operation. The default, [`LbvPrecision::Double`],
61//! runs the same algorithm in double precision, which does no harm and avoids single precision's
62//! rounding in the relaxation (QSM.rs keeps double where the reference uses single).
63//!
64//! ## Parity
65//! [`LbvPrecision::Single`] against the compiled mexMGv6 (MEDI 2024.11.26's `mexMGv6.cpp` built
66//! with R2023b's `mex`, as QSM-CI's verified `lbv-cornell` container runs it), on the same
67//! `iFreq`: bit-identical in the local field and the interior, at LBV.m's defaults, on QSM-CI's
68//! in-silico head (164×205×205, 7 T) and an EMTP travelling-head scan (184×256×128, 3 T), and on
69//! `tests/data/lbv_cornell_small.bin` (`tests/lbv_cornell_parity.rs`; made with the container by
70//! `tests/data/lbv_cornell_small.py`), which also covers `peel`, explicit depths, another `tol`
71//! and other relaxation counts. The default, double precision, against the same mex: NRMSE
72//! 0.00077 % in silico and 0.0011 % in vivo (max |Δ| 5.9e-6 and 4.4e-6 rad). QSM-CI's `lbv` pair
73//! (QSMxT, default double precision, run 41423039167): NRMSE 7.7e-4 % in silico and 0.0018 % on
74//! KKI (max |Δ| 3.9e-7 and 2.0e-6 ppm).
75//!
76//! Not ported: with `peel` > 0 mexMGv6 also writes the peeled mask to `mask_p<peel>.bin` in the
77//! working directory (and crashes MATLAB where it cannot write there).
78//!
79//! ## The previous solver
80//! Until astewartau/QSM.rs#406 this module solved the boundary value problem by successive
81//! over-relaxation (ω 1.5, to a relative tolerance, at most `min(3·max dim, 500)` sweeps). That is
82//! a different solver, and it stopped elsewhere: against mexMGv6 it measured 7.72 % NRMSE in
83//! silico and 24.0 % on KKI (QSMxT/QSM-CI parity run 41384710780). It was not kept as an option,
84//! because it was not better: against the exact solution of the discrete problem (conjugate
85//! gradients to 1e-12), the multigrid at LBV.m's defaults is off by 2.4 % in silico and 8.5 % in
86//! vivo (NRMSE over the interior), the SOR solver by 7.0 % and 16.7 %; against the in-silico
87//! head's true local field (interior, mean removed) all three are at LBV's own limit (exact
88//! solution 43.5 %, multigrid 43.4 %, SOR 42.4 %).
89//!
90//! Reference:
91//! Zhou, D., Liu, T., Spincemaille, P., Wang, Y. (2014).
92//! "Background field removal by solving the Laplacian boundary value problem."
93//! NMR in Biomedicine, 27(3):312-319. https://doi.org/10.1002/nbm.3064
94//!
95//! Reference implementation: MEDI toolbox 2024.11.26, `functions/_LBV/LBV.m` and `mexMGv6.cpp`
96//! (http://pre.weill.cornell.edu/mri/pages/qsm.html).
97
98use crate::error::{check_len, InputError};
99use crate::utils::echo_time::EchoTime;
100use crate::Grid;
101
102/// Arithmetic of the multigrid solver.
103#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
104#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
105pub enum LbvPrecision {
106 /// Double precision throughout (the default).
107 #[default]
108 Double,
109 /// mexMGv6's arithmetic: single precision, with the prolongation's averages and corrections
110 /// in double rounded to single. Reproduces the compiled mex bit for bit.
111 Single,
112}
113
114/// LBV parameters (`LBV.m`'s defaults)
115#[cfg_attr(feature = "introspection", derive(serde::Serialize))]
116#[derive(Clone, Debug)]
117pub struct LbvParams {
118 /// Stopping tolerance of the coarsest-grid relaxation (`tol`, default 0.01): the
119 /// relaxation repeats while the summed absolute update over the coarsest interior exceeds
120 /// it. Absolute, in MEDI's units: radians accumulated over the echo time [`te`](Self::te)
121 /// picks. Must be finite and ≥ 0. Any other value is kept as given; the rule
122 /// `lbv.tolerance` ([`crate::pipeline::rules::RULES`]) suggests LBV.m's 0.01 (e.g. to a
123 /// settings file holding the earlier SOR solver's relative 1e-6).
124 pub tol: f64,
125 /// Coarsest multigrid level (`depth`). `None` (LBV.m's −1, the default): `⌊log2 m⌋ − 3`
126 /// for the smallest dimension `m`, at least 0. At most 8.
127 pub depth: Option<usize>,
128 /// Boundary layers removed beyond the one boundary layer (`peel`, default 0)
129 pub peel: usize,
130 /// Relaxations on each level before the recursive call (`N1`, default 30)
131 pub n1: usize,
132 /// Relaxations on each level after the recursive call (`N2`, default 100)
133 pub n2: usize,
134 /// Relaxations on the finest level after the full multigrid cycle (`N3`, default 100)
135 pub n3: usize,
136 /// Arithmetic (default [`LbvPrecision::Double`]; `Single` is mexMGv6's own)
137 pub precision: LbvPrecision,
138 /// Field strength in Tesla (scan parameter, for the radian conversion)
139 pub b0: f64,
140 /// Echo times in seconds (scan parameter, for the radian conversion)
141 pub echo_times: Vec<f64>,
142 /// Which echo time converts the field from ppm to radians. Default
143 /// [`EchoTime::Spacing`], MEDI's `delta_TE` (`TE(2) − TE(1)`, or `TE` for one echo): LBV.m
144 /// takes `iFreq` from `Fit_ppm_complex`, the phase accrued over the echo spacing, and its
145 /// `tol` is absolute in those units.
146 pub te: EchoTime,
147}
148
149impl Default for LbvParams {
150 fn default() -> Self {
151 Self {
152 tol: 0.01,
153 depth: None,
154 peel: 0,
155 n1: 30,
156 n2: 100,
157 n3: 100,
158 precision: LbvPrecision::Double,
159 b0: 3.0,
160 echo_times: vec![0.005],
161 te: EchoTime::Spacing,
162 }
163 }
164}
165
166impl LbvParams {
167 /// Check the parameters (not the scan's echo times, which [`lbv`] resolves).
168 pub fn validate(&self) -> Result<(), InputError> {
169 if !(self.tol.is_finite() && self.tol >= 0.0) {
170 return Err(InputError::param("LBV tol", self.tol, "finite and ≥ 0"));
171 }
172 if let Some(d) = self.depth {
173 if d > MAX_DEPTH {
174 return Err(InputError::param("LBV depth", d, "≤ 8 (mexMGv6 keeps 10 grids)"));
175 }
176 }
177 if !(self.b0.is_finite() && self.b0 > 0.0) {
178 return Err(InputError::param("LBV b0", self.b0, "finite and > 0 T"));
179 }
180 Ok(())
181 }
182}
183
184/// mexMGv6's arrays hold 10 levels, and `initializeMem` writes the size of level `depth + 1`.
185const MAX_DEPTH: usize = 8;
186
187/// Guard against a coarsest-level relaxation that never meets `tol` (e.g. `tol` 0 in single
188/// precision, where the update can cycle at the last bit). mexMGv6 has no limit; at its default
189/// `tol` the coarsest level converges in far fewer sweeps.
190const MAX_COARSEST_SWEEPS: usize = 100_000;
191
192/// LBV background field removal (`LBV.m`)
193///
194/// # Arguments
195/// * `field` - Total field in ppm (nx * ny * nz)
196/// * `mask` - Binary mask (nx * ny * nz), non-zero = brain
197/// * `grid` - Volume dimensions and voxel sizes
198/// * `params` - LBV parameters, with the scan's field strength and echo times
199/// * `progress` - Progress callback (step, steps): one step per multigrid level, and the finish
200///
201/// # Returns
202/// `(local_field, interior)`: the local field in ppm as LBV.m returns it (defined on the
203/// interior; see the module docs for the voxels around it), and LBV.m's `interior` (the mask
204/// minus `peel + 1` layers).
205pub fn lbv(
206 field: &[f64],
207 mask: &[u8],
208 grid: &Grid,
209 params: &LbvParams,
210 progress: impl FnMut(usize, usize),
211) -> Result<(Vec<f64>, Vec<u8>), InputError> {
212 params.validate()?;
213 let n = grid.n_total();
214 check_len("LBV field", field.len(), n)?;
215 check_len("LBV mask", mask.len(), n)?;
216 let delta_te = params
217 .te
218 .resolve(¶ms.echo_times)
219 .map_err(|e| InputError::new(format!("LBV: {e}")))?;
220 let cf = crate::constants::GAMMA_BAR_HZ_PER_T * params.b0;
221 let pi = std::f64::consts::PI;
222
223 // ppm -> radians over delta_TE, in the order QSM-CI's lbv-cornell writes it
224 // (field * 1e-6 * 2*pi * CF * delta_TE), so both hand mexMGv6 the same numbers.
225 let ifreq: Vec<f64> = field.iter().map(|&f| f * 1e-6 * 2.0 * pi * cf * delta_te).collect();
226 let (fl, interior) = lbv_multigrid(&ifreq, mask, grid, params, progress);
227 let back = 2.0 * pi * cf * delta_te;
228 let local = fl.iter().map(|&v| v / back * 1e6).collect();
229 Ok((local, interior))
230}
231
232/// mexMGv6 on a field already in the units `tol` is given in (MEDI's `iFreq`), as
233/// `[fL, interior] = LBV(iFreq, Mask, matrix_size, voxel_size, tol, depth, peel, N1, N2, N3)`.
234/// `params.b0`, `echo_times` and `te` are not used; the other parameters are not validated
235/// (see [`LbvParams::validate`]).
236pub fn lbv_multigrid(
237 ifreq: &[f64],
238 mask: &[u8],
239 grid: &Grid,
240 params: &LbvParams,
241 progress: impl FnMut(usize, usize),
242) -> (Vec<f64>, Vec<u8>) {
243 match params.precision {
244 LbvPrecision::Double => Mg::<f64>::run(ifreq, mask, grid, params, progress),
245 LbvPrecision::Single => Mg::<f32>::run(ifreq, mask, grid, params, progress),
246 }
247}
248
249/// The arithmetic mexMGv6's steps need, in `f32` (its own) or `f64`.
250trait Real:
251 Copy
252 + PartialOrd
253 + std::ops::Add<Output = Self>
254 + std::ops::Sub<Output = Self>
255 + std::ops::Mul<Output = Self>
256{
257 const ZERO: Self;
258 const ONE: Self;
259 const TWO: Self;
260 fn from_f64(x: f64) -> Self;
261 fn to_f64(self) -> f64;
262 fn abs(self) -> Self;
263}
264
265impl Real for f32 {
266 const ZERO: Self = 0.0;
267 const ONE: Self = 1.0;
268 const TWO: Self = 2.0;
269 fn from_f64(x: f64) -> Self { x as f32 }
270 fn to_f64(self) -> f64 { self as f64 }
271 fn abs(self) -> Self { f32::abs(self) }
272}
273
274impl Real for f64 {
275 const ZERO: Self = 0.0;
276 const ONE: Self = 1.0;
277 const TWO: Self = 2.0;
278 fn from_f64(x: f64) -> Self { x }
279 fn to_f64(self) -> f64 { self }
280 fn abs(self) -> Self { f64::abs(self) }
281}
282
283/// Which buffer is the right-hand side of a relaxation or V-cycle on a level.
284#[derive(Clone, Copy)]
285enum Rhs {
286 /// `ρ_d`, the Laplacian of the sampled total field (`rho[d]`)
287 Rho,
288 /// the residual injected from the finer level (`res[d]`)
289 Res,
290}
291
292/// Level `d` of the hierarchy (`MG`'s per-depth arrays).
293struct Level<T> {
294 nx: usize,
295 ny: usize,
296 nz: usize,
297 mask: Vec<bool>,
298 /// interior voxel indices in increasing order (the `interior[d][i] == true` loops)
299 interior: Vec<usize>,
300 rho: Vec<T>,
301 v: Vec<T>,
302 res: Vec<T>,
303}
304
305impl<T> Level<T> {
306 fn nxy(&self) -> usize {
307 self.nx * self.ny
308 }
309}
310
311struct Mg<T> {
312 levels: Vec<Level<T>>,
313 depth: usize,
314 /// `aux3`: 1/dx², 1/dy², 1/dz² of the finest grid, used on every level
315 aux3: [T; 3],
316 /// `aux2`: 1 / (2 Σ aux3)
317 aux2: T,
318 tol: T,
319 n1: usize,
320 n2: usize,
321}
322
323/// `MG::createInterior`: the mask minus voxels on the array edge or with a 6-neighbour outside.
324fn create_interior(mask: &[bool], nx: usize, ny: usize, nz: usize) -> Vec<bool> {
325 let nxy = nx * ny;
326 let mut interior = mask.to_vec();
327 for z in 0..nz {
328 for y in 0..ny {
329 for x in 0..nx {
330 let i = x + y * nx + z * nxy;
331 if !mask[i] {
332 continue;
333 }
334 let on_edge = x == 0 || y == 0 || z == 0 || x == nx - 1 || y == ny - 1 || z == nz - 1;
335 if on_edge
336 || !mask[i - 1]
337 || !mask[i + 1]
338 || !mask[i - nx]
339 || !mask[i + nx]
340 || !mask[i - nxy]
341 || !mask[i + nxy]
342 {
343 interior[i] = false;
344 }
345 }
346 }
347 }
348 interior
349}
350
351/// `MG::determineDepth`: `⌊log2⌋` of the smallest dimension, minus 3, at least 0.
352fn auto_depth(nx: usize, ny: usize, nz: usize) -> usize {
353 let mut m = nx.min(ny).min(nz);
354 let mut scale = 0usize;
355 while m > 1 {
356 m >>= 1;
357 scale += 1;
358 }
359 scale.saturating_sub(3)
360}
361
362/// One value `MG::C2F` writes at fine index `t`: the mean `sum · w` of the coarse values around
363/// it (w = 1/2, 1/4, 1/8), formed in double as the C++ does (`/2.0` is a double literal). As a
364/// correction it is scaled by `prefac` and added to the fine value in double, then rounded.
365#[inline]
366fn c2f_put<T: Real>(out: &mut [T], t: usize, sum: T, w: f64, cor: bool, prefac: f64) {
367 out[t] = if cor {
368 T::from_f64(out[t].to_f64() + sum.to_f64() * w * prefac)
369 } else {
370 T::from_f64(sum.to_f64() * w)
371 };
372}
373
374impl<T: Real> Mg<T> {
375 fn run(
376 ifreq: &[f64],
377 mask_in: &[u8],
378 grid: &Grid,
379 params: &LbvParams,
380 mut progress: impl FnMut(usize, usize),
381 ) -> (Vec<f64>, Vec<u8>) {
382 let (nx0, ny0, nz0) = grid.dims;
383 let depth = params.depth.unwrap_or_else(|| auto_depth(nx0, ny0, nz0));
384 let steps = depth + 2;
385
386 // initializeConstants: aux3[i] = 1.0 / v / v in double from the voxel size in T,
387 // stored in T; aux2 sums them in T, then 1/2.0/aux2 in double.
388 let vs = [grid.voxel_size.0, grid.voxel_size.1, grid.voxel_size.2];
389 let mut aux3 = [T::ZERO; 3];
390 let mut aux2 = T::ZERO;
391 for i in 0..3 {
392 let v = T::from_f64(vs[i]).to_f64();
393 aux3[i] = T::from_f64(1.0 / v / v);
394 aux2 = aux2 + aux3[i];
395 }
396 let aux2 = T::from_f64(1.0 / 2.0 / aux2.to_f64());
397
398 // preProcessing: the level-0 interior, then `peel` more layers off the mask.
399 let mut mask0: Vec<bool> = mask_in.iter().map(|&m| m != 0).collect();
400 let mut interior0 = create_interior(&mask0, nx0, ny0, nz0);
401 for _ in 0..params.peel {
402 mask0.clone_from(&interior0);
403 interior0 = create_interior(&mask0, nx0, ny0, nz0);
404 }
405
406 // initializeMem + initializeROI + initializeRho, level by level.
407 let fine_index = |x: usize, y: usize, z: usize, s: usize| x * s + y * s * nx0 + z * s * nx0 * ny0;
408 let mut levels: Vec<Level<T>> = Vec::with_capacity(depth + 1);
409 let (mut nx, mut ny, mut nz) = (nx0, ny0, nz0);
410 for d in 0..=depth {
411 let n = nx * ny * nz;
412 let nxy = nx * ny;
413 let s = 1usize << d;
414 let (mask, interior) = if d == 0 {
415 (mask0.clone(), std::mem::take(&mut interior0))
416 } else {
417 // the coarse mask samples the peeled level-0 mask
418 let mut m = vec![false; n];
419 for z in 0..nz {
420 for y in 0..ny {
421 for x in 0..nx {
422 m[x + y * nx + z * nxy] = mask0[fine_index(x, y, z, s)];
423 }
424 }
425 }
426 let it = create_interior(&m, nx, ny, nz);
427 (m, it)
428 };
429 let interior: Vec<usize> = (0..n).filter(|&i| interior[i]).collect();
430
431 // ρ_d: the Laplacian of the total field sampled onto this level, with the finest
432 // voxel size, in initializeRho's order of operations (its tmpi/tmpj/tmpk are the
433 // sample times aux3, formed here where they are used: the same products).
434 let mut t = vec![T::ZERO; n];
435 for z in 0..nz {
436 for y in 0..ny {
437 for x in 0..nx {
438 t[x + y * nx + z * nxy] = T::from_f64(ifreq[fine_index(x, y, z, s)]);
439 }
440 }
441 }
442 let mut rho = vec![T::ZERO; n];
443 let [a0, a1, a2] = aux3;
444 for &j in &interior {
445 rho[j] = t[j + nxy] * a2 + t[j - nxy] * a2 + t[j + nx] * a1 + t[j - nx] * a1 + t[j + 1] * a0
446 + t[j - 1] * a0
447 - T::TWO * (t[j] * a0 + t[j] * a1 + t[j] * a2);
448 }
449 drop(t);
450
451 levels.push(Level { nx, ny, nz, mask, interior, rho, v: vec![T::ZERO; n], res: vec![T::ZERO; n] });
452 // (1 + n) / 2
453 nx = nx.div_ceil(2);
454 ny = ny.div_ceil(2);
455 nz = nz.div_ceil(2);
456 }
457
458 let mut mg = Mg { levels, depth, aux3, aux2, tol: T::from_f64(params.tol), n1: params.n1, n2: params.n2 };
459 mg.fmg(0, &mut progress, steps);
460 // postProcessing: N3 more relaxations on the finest level.
461 mg.relax(0, Rhs::Rho, params.n3);
462 progress(steps, steps);
463
464 let l0 = &mg.levels[0];
465 let fl = l0.v.iter().map(|v| v.to_f64()).collect();
466 let mut interior = vec![0u8; l0.v.len()];
467 for &i in &l0.interior {
468 interior[i] = 1;
469 }
470 (fl, interior)
471 }
472
473 /// `MG::FMG`
474 fn fmg(&mut self, d: usize, progress: &mut impl FnMut(usize, usize), steps: usize) {
475 if d == self.depth {
476 self.relax(d, Rhs::Rho, 0);
477 } else {
478 self.fmg(d + 1, progress, steps);
479 self.c2f(d + 1, false);
480 self.v_cycle(d, Rhs::Rho);
481 }
482 progress(self.depth - d + 1, steps);
483 }
484
485 /// `MG::V_cycle` on level `d` with right-hand side `f`
486 fn v_cycle(&mut self, d: usize, f: Rhs) {
487 self.relax(d, f, self.n1);
488 if d != self.depth {
489 self.update_res(d, f);
490 self.f2c(d);
491 self.levels[d + 1].v.fill(T::ZERO);
492 self.v_cycle(d + 1, Rhs::Res);
493 self.c2f(d + 1, true);
494 }
495 self.relax(d, f, self.n2);
496 }
497
498 /// `MG::relax`: lexicographic Gauss–Seidel on the interior, `n` sweeps, or on the coarsest
499 /// level until the summed absolute update is at most `tol`.
500 fn relax(&mut self, d: usize, f: Rhs, n: usize) {
501 let aux3 = self.aux3;
502 let aux2 = self.aux2;
503 let coarsest = d == self.depth;
504 let tol = self.tol;
505 let lv = &mut self.levels[d];
506 let (nx, nxy) = (lv.nx, lv.nx * lv.ny);
507 let rr = match f {
508 Rhs::Rho => &lv.rho,
509 Rhs::Res => &lv.res,
510 };
511 let v = &mut lv.v;
512 let interior = &lv.interior;
513 let sweep = |v: &mut [T]| {
514 for &i in interior {
515 let x = aux3[0] * (v[i + 1] + v[i - 1]) + aux3[1] * (v[i + nx] + v[i - nx])
516 + aux3[2] * (v[i + nxy] + v[i - nxy])
517 - rr[i];
518 v[i] = x * aux2;
519 }
520 };
521 if coarsest {
522 let mut old: Vec<T> = vec![T::ZERO; v.len()];
523 let mut err = T::ONE;
524 let mut sweeps = 0;
525 while err > tol && sweeps < MAX_COARSEST_SWEEPS {
526 old.copy_from_slice(v);
527 sweep(v);
528 // computeDiff(old, v): Σ |old − v| over the interior, in order
529 err = T::ZERO;
530 for &i in interior {
531 err = err + (old[i] - v[i]).abs();
532 }
533 sweeps += 1;
534 }
535 return;
536 }
537 for _ in 0..n {
538 sweep(v);
539 }
540 }
541
542 /// `MG::updateRes`: `res_d = f − L v` on the interior. With `f` = `res_d` this overwrites the
543 /// right-hand side, as in the original.
544 fn update_res(&mut self, d: usize, f: Rhs) {
545 let aux3 = self.aux3;
546 let lv = &mut self.levels[d];
547 let (nx, nxy) = (lv.nx, lv.nx * lv.ny);
548 for &i in &lv.interior {
549 let u = &lv.v;
550 let lap = (u[i + nxy] + u[i - nxy] - T::TWO * u[i]) * aux3[2]
551 + (u[i + nx] + u[i - nx] - T::TWO * u[i]) * aux3[1]
552 + (u[i + 1] + u[i - 1] - T::TWO * u[i]) * aux3[0];
553 let fi = match f {
554 Rhs::Rho => lv.rho[i],
555 Rhs::Res => lv.res[i],
556 };
557 lv.res[i] = fi - lap;
558 }
559 }
560
561 /// `MG::F2C`: inject the residual of level `d` onto level `d + 1` (the even-indexed voxels).
562 fn f2c(&mut self, d: usize) {
563 let (fine, coarse) = self.levels.split_at_mut(d + 1);
564 let (fine, coarse) = (&fine[d], &mut coarse[0]);
565 let (cnx, cnxy) = (coarse.nx, coarse.nxy());
566 let (fnx, fnxy) = (fine.nx, fine.nxy());
567 for z in (0..fine.nz).step_by(2) {
568 for y in (0..fine.ny).step_by(2) {
569 for x in (0..fine.nx).step_by(2) {
570 coarse.res[x / 2 + (y / 2) * cnx + (z / 2) * cnxy] = fine.res[x + y * fnx + z * fnxy];
571 }
572 }
573 }
574 }
575
576 /// `MG::C2F`: prolong level `d` onto level `d − 1`, replacing (`cor` false) or adding the
577 /// correction scaled by `prefac` = `4^−d` (`cor` true).
578 fn c2f(&mut self, d: usize, cor: bool) {
579 let (fine, coarse) = self.levels.split_at_mut(d);
580 let (fine, coarse) = (&mut fine[d - 1], &coarse[0]);
581 let s = (1u64 << d) as f64;
582 let prefac = T::from_f64(1.0 / s / s);
583 let pd = prefac.to_f64();
584 let (nx, ny, nz, nxy) = (coarse.nx, coarse.ny, coarse.nz, coarse.nxy());
585 let (fnx, fnxy) = (fine.nx, fine.nxy());
586 let m = &coarse.mask;
587 let c = &coarse.v;
588 let out = &mut fine.v;
589 for i in 0..nx {
590 for j in 0..ny {
591 for k in 0..nz {
592 let idx = i + j * nx + k * nxy;
593 if !m[idx] {
594 continue;
595 }
596 let f = 2 * (i + j * fnx + k * fnxy);
597 let (xl, yl, zl) = (i != nx - 1, j != ny - 1, k != nz - 1);
598 // the coincident voxel, in T (`vv[d-1][idx_] += vv[d][idx] * prefac`)
599 out[f] = if cor { out[f] + c[idx] * prefac } else { c[idx] };
600 if zl && m[idx + nxy] {
601 c2f_put(out, f + fnxy, c[idx] + c[idx + nxy], 0.5, cor, pd);
602 }
603 if yl && m[idx + nx] {
604 c2f_put(out, f + fnx, c[idx] + c[idx + nx], 0.5, cor, pd);
605 }
606 if xl && m[idx + 1] {
607 c2f_put(out, f + 1, c[idx] + c[idx + 1], 0.5, cor, pd);
608 }
609 if xl && yl && m[idx + 1] && m[idx + nx] && m[idx + nx + 1] {
610 let sum = c[idx] + c[idx + 1] + c[idx + 1 + nx] + c[idx + nx];
611 c2f_put(out, f + 1 + fnx, sum, 0.25, cor, pd);
612 }
613 if xl && zl && m[idx + 1] && m[idx + nxy] && m[idx + nxy + 1] {
614 let sum = c[idx] + c[idx + 1] + c[idx + 1 + nxy] + c[idx + nxy];
615 c2f_put(out, f + 1 + fnxy, sum, 0.25, cor, pd);
616 }
617 if yl && zl && m[idx + nx] && m[idx + nxy] && m[idx + nx + nxy] {
618 let sum = c[idx] + c[idx + nx] + c[idx + nx + nxy] + c[idx + nxy];
619 c2f_put(out, f + fnx + fnxy, sum, 0.25, cor, pd);
620 }
621 if xl && yl && zl
622 && m[idx + 1]
623 && m[idx + 1 + nx]
624 && m[idx + 1 + nxy]
625 && m[idx + 1 + nxy + nx]
626 && m[idx + nx]
627 && m[idx + nxy]
628 && m[idx + nx + nxy]
629 {
630 let sum = c[idx + 1] + c[idx + nxy] + c[idx] + c[idx + nx] + c[idx + nx + 1]
631 + c[idx + nx + nxy] + c[idx + nx + nxy + 1] + c[idx + nxy + 1];
632 c2f_put(out, f + 1 + fnx + fnxy, sum, 0.125, cor, pd);
633 }
634 }
635 }
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 /// A ball of radius `r` voxels in an `n`³ grid.
645 fn ball(n: usize, r: f64) -> Vec<u8> {
646 let c = (n as f64 - 1.0) / 2.0;
647 let mut m = vec![0u8; n * n * n];
648 for z in 0..n {
649 for y in 0..n {
650 for x in 0..n {
651 let d2 = (x as f64 - c).powi(2) + (y as f64 - c).powi(2) + (z as f64 - c).powi(2);
652 m[x + y * n + z * n * n] = (d2 <= r * r) as u8;
653 }
654 }
655 }
656 m
657 }
658
659 fn run(field: &[f64], mask: &[u8], grid: &Grid, p: &LbvParams) -> (Vec<f64>, Vec<u8>) {
660 lbv(field, mask, grid, p, |_, _| {}).unwrap()
661 }
662
663 #[test]
664 fn auto_depth_is_log2_of_the_smallest_dimension_minus_3() {
665 assert_eq!(auto_depth(164, 205, 205), 4);
666 assert_eq!(auto_depth(184, 256, 128), 4);
667 assert_eq!(auto_depth(19, 23, 17), 1);
668 assert_eq!(auto_depth(15, 64, 64), 0);
669 assert_eq!(auto_depth(1, 1, 1), 0);
670 }
671
672 #[test]
673 fn interior_is_the_mask_less_its_boundary_and_peel_layers() {
674 let n = 16;
675 let mask = ball(n, 6.0);
676 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
677 let m: Vec<bool> = mask.iter().map(|&v| v != 0).collect();
678 let once = create_interior(&m, n, n, n);
679 let twice = create_interior(&once, n, n, n);
680 let (_, it0) = run(&vec![0.0; n * n * n], &mask, &grid, &LbvParams::default());
681 let (_, it1) = run(&vec![0.0; n * n * n], &mask, &grid, &LbvParams { peel: 1, ..Default::default() });
682 assert!(it0.iter().zip(&once).all(|(&a, &b)| (a != 0) == b));
683 assert!(it1.iter().zip(&twice).all(|(&a, &b)| (a != 0) == b));
684 assert!(twice.iter().filter(|&&b| b).count() < once.iter().filter(|&&b| b).count());
685 }
686
687 #[test]
688 fn zero_field_gives_zero() {
689 let n = 16;
690 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
691 let (local, _) = run(&vec![0.0; n * n * n], &ball(n, 6.0), &grid, &LbvParams::default());
692 assert!(local.iter().all(|&v| v == 0.0));
693 }
694
695 /// A harmonic field is all background: LBV leaves (nearly) nothing on the interior, in
696 /// both precisions, with anisotropic voxels. A local source inside is kept.
697 #[test]
698 fn removes_a_harmonic_field_and_keeps_a_local_one() {
699 let n = 24;
700 let grid = Grid::new(n, n, n, 0.8, 1.0, 1.3);
701 let mask = ball(n, 9.0);
702 let at = |i: usize| ((i % n) as f64 * 0.8, ((i / n) % n) as f64, (i / (n * n)) as f64 * 1.3);
703 // x² − z² (harmonic) plus a linear term, in ppm
704 let bg: Vec<f64> = (0..n * n * n)
705 .map(|i| {
706 let (x, _, z) = at(i);
707 0.002 * (x * x - z * z) + 0.01 * z
708 })
709 .collect();
710 let bump: Vec<f64> = (0..n * n * n)
711 .map(|i| {
712 let (x, y, z) = at(i);
713 let d2 = (x - 9.6).powi(2) + (y - 12.0).powi(2) + (z - 15.0).powi(2);
714 0.05 / (1.0 + d2 / 4.0)
715 })
716 .collect();
717 for precision in [LbvPrecision::Double, LbvPrecision::Single] {
718 let p = LbvParams { precision, b0: 3.0, echo_times: vec![0.004, 0.008], ..Default::default() };
719 let (l_bg, it) = run(&bg, &mask, &grid, &p);
720 let total: Vec<f64> = bg.iter().zip(&bump).map(|(a, b)| a + b).collect();
721 let (l_tot, _) = run(&total, &mask, &grid, &p);
722 let inside = |v: &[f64]| (0..v.len()).filter(|&i| it[i] != 0).map(|i| v[i].abs()).fold(0.0, f64::max);
723 let bg_max = (0..bg.len()).filter(|&i| it[i] != 0).map(|i| bg[i].abs()).fold(0.0, f64::max);
724 assert!(inside(&l_bg) < 0.01 * bg_max, "{precision:?}: harmonic residue {} of {bg_max}", inside(&l_bg));
725 assert!(inside(&l_tot) > 0.02, "{precision:?}: the local bump is lost ({})", inside(&l_tot));
726 }
727 }
728
729 /// Single and double precision run the same algorithm: they differ by rounding only.
730 #[test]
731 fn single_and_double_agree() {
732 let n = 20;
733 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
734 let mask = ball(n, 8.0);
735 let field: Vec<f64> = (0..n * n * n).map(|i| ((i * 7919) % 101) as f64 * 1e-3 + (i % n) as f64 * 0.01).collect();
736 let (d, _) = run(&field, &mask, &grid, &LbvParams::default());
737 let (s, _) = run(&field, &mask, &grid, &LbvParams { precision: LbvPrecision::Single, ..Default::default() });
738 let num: f64 = d.iter().zip(&s).map(|(a, b)| (a - b).powi(2)).sum();
739 let den: f64 = d.iter().map(|a| a * a).sum();
740 assert!(den > 0.0 && (num / den).sqrt() < 1e-4, "relative difference {}", (num / den).sqrt());
741 }
742
743 /// `tol` ≥ 1 skips the coarsest solve (its update starts at 1), as in mexMGv6; the finer
744 /// levels still relax.
745 #[test]
746 fn tol_at_least_one_skips_the_coarsest_solve() {
747 let n = 32;
748 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
749 let mask = ball(n, 12.0);
750 let field: Vec<f64> = (0..n * n * n).map(|i| ((i % n) as f64 - 16.0).powi(2) * 1e-3).collect();
751 let p = |tol| LbvParams { tol, depth: Some(2), n1: 0, n2: 0, n3: 0, ..Default::default() };
752 let (skipped, _) = run(&field, &mask, &grid, &p(1.0));
753 assert!(skipped.iter().all(|&v| v == 0.0), "no relaxation anywhere, so nothing moves");
754 let (solved, _) = run(&field, &mask, &grid, &p(0.01));
755 assert!(solved.iter().any(|&v| v != 0.0));
756 }
757
758 #[test]
759 fn rejects_bad_parameters_and_inputs() {
760 let n = 8;
761 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
762 let f = vec![0.0; n * n * n];
763 let m = vec![1u8; n * n * n];
764 let bad = |p: LbvParams| lbv(&f, &m, &grid, &p, |_, _| {}).is_err();
765 assert!(bad(LbvParams { tol: -1.0, ..Default::default() }));
766 assert!(bad(LbvParams { tol: f64::NAN, ..Default::default() }));
767 assert!(bad(LbvParams { depth: Some(9), ..Default::default() }));
768 assert!(bad(LbvParams { b0: 0.0, ..Default::default() }));
769 assert!(bad(LbvParams { echo_times: vec![], ..Default::default() }));
770 assert!(lbv(&f[1..], &m, &grid, &LbvParams::default(), |_, _| {}).is_err());
771 }
772
773 #[test]
774 fn reports_progress_to_the_end() {
775 let n = 16;
776 let grid = Grid::new(n, n, n, 1.0, 1.0, 1.0);
777 let mut calls = Vec::new();
778 lbv(&vec![0.1; n * n * n], &ball(n, 6.0), &grid, &LbvParams::default(), |a, b| calls.push((a, b))).unwrap();
779 assert_eq!(calls.last(), Some(&(3, 3)), "depth 1: the coarsest, level 0, the finish ({calls:?})");
780 }
781}