Skip to main content

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(&params.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}