Skip to main content

qsm_core/utils/
gibbs_mrtrix.rs

1//! MRtrix3's `mrdegibbs`, ported: Kellner et al.'s 2-D unringing for 2-D multi-slice acquisitions
2//! ([`gibbs_unring_2d`], [`GibbsMode::Mrtrix2d`](crate::utils::gibbs::GibbsMode::Mrtrix2d)) and
3//! Bautista et al.'s 3-D extension for 3-D acquisitions ([`gibbs_unring_3d`],
4//! [`GibbsMode::Bautista3d`](crate::utils::gibbs::GibbsMode::Bautista3d), the default). Which mode
5//! suits which acquisition, the measurements behind it, and the sources and licences are in
6//! [`crate::utils::gibbs`].
7//!
8//! **Reference code:** MRtrix3 `dev` 9b0a2f1 (2026-09), `cpp/core/degibbs/unring1d.h`,
9//! `unring2d.h`, `unring3d.h` and `cpp/cmd/mrdegibbs.cpp` (MPL-2.0; this is a new Rust
10//! implementation of the same steps, credited here). The 3-D mode is not in a 3.0.x release.
11//! Defaults ([`MrtrixUnringParams`]) are `mrdegibbs`'s: `nshifts` 20, `minW` 1, `maxW` 3.
12//!
13//! # 2-D (`Unring2D`, Kellner et al. 2016)
14//!
15//! Each slice (rows: the first in-plane axis, columns: the second; slices normal to z are
16//! MRtrix's default `-axes 0,1`):
17//! - its 2-D FFT is split into two parts with the weights `ck/(ck+cj)` and `cj/(ck+cj)`,
18//!   `c = (1 + cos(2π·m/N))/2` at the exact FFT frequencies (both parts 0 where `ck+cj = 0`);
19//! - each part is unrung by 1-D shifts along one axis (`Unring1D`): shifts `0, ±1, …, ±nshifts`
20//!   in units of `1/(2·nshifts)` voxel, applied as `exp(iπ·shift·m/(N·nshifts))` on the positive
21//!   bins and the conjugate on their mirrors, the even-length Nyquist bin zeroed in the shifted
22//!   lines (kept in the unshifted one);
23//! - per voxel, the shift of least oscillation: a running sum of `|ΔRe| + |ΔIm|` over neighbours
24//!   `minW..=maxW` to the left and to the right (updated voxel by voxel as MRtrix updates it,
25//!   left before right, the first shift winning ties), then linear interpolation to the voxel;
26//! - the two parts are summed.
27//!
28//! # 3-D (`Unring3DFunctor`, Bautista et al. 2021)
29//!
30//! - The 3-D forward FFT of the input; per axis `i`, the filter
31//!   `∏_{j≠i}(1+cos k_j) / Σ_m ∏_{j≠m}(1+cos k_j)`, `k_j = 2π·n_j/N_j` (0 where its denominator is
32//!   0; the three sum to 1, Kellner's 2-D weights being the 2-D case), then the 3-D inverse FFT;
33//! - along the axis, each line's FFT is shifted by `2·nshifts + 1` subvoxel shifts,
34//!   `0, ±1/(2·nshifts + 1), …, ±nshifts/(2·nshifts + 1)` voxel, as `exp(2πi·n·s/N)` with the
35//!   even-length Nyquist bin zeroed, each inverse transformed;
36//! - per voxel, the shift of least oscillation: the sum of `|ΔRe| + |ΔIm|` between neighbours
37//!   `minW..=maxW` to the left, and likewise to the right, the smaller of the two, the first
38//!   shift winning ties;
39//! - linear interpolation of the chosen shifted line back to the voxel
40//!   (`a₁ − s·(a₁ − a₀)` for `s > 0`, else `a₁ + s·(a₁ − a₂)`), and the three axes' results
41//!   summed.
42//!
43//! # Deviations
44//!
45//! [`gibbs_unring_2d`] and [`gibbs_unring_3d`] (real input) return the real part, as `mrdegibbs`
46//! writes for a real input, with negative values set to 0 (a magnitude cannot be negative;
47//! MRtrix keeps them, e.g. ringing in the background). The `_complex` functions are the
48//! reference's output as is. Nothing else differs.
49//!
50//! # Agreement with `mrdegibbs` (MRtrix3 `dev` 9b0a2f1 built from source; measured 2026-10-31)
51//!
52//! - **2-D:** NRMSE 2.9e-16–3.7e-16, max |Δ| 1.6e-15, on the 96×96×64 phantoms of
53//!   [`crate::utils::gibbs`] (values up to 1.85; slices normal to z and to y), and NRMSE
54//!   3.4e-16–5.4e-16 on the four cases of `tests/gibbs_mrtrix2d_parity.rs` (24×24×16, and
55//!   21×26×15 in slices normal to each axis).
56//! - **3-D:** NRMSE 4.6e-16, max |Δ| 1.8e-15 on a 96×96×64 phantom, and NRMSE 4.5e-16 and 8.2e-16
57//!   on the two small phantoms of `tests/gibbs_mrtrix3d_parity.rs` (24×24×16 and, for odd axes,
58//!   21×26×15).
59
60use num_complex::Complex64;
61use rustfft::{Fft, FftPlanner};
62use std::sync::Arc;
63
64#[cfg(feature = "parallel")]
65use rayon::prelude::*;
66
67/// Parameters of [`gibbs_unring_2d`] and [`gibbs_unring_3d`], with `mrdegibbs`'s defaults.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct MrtrixUnringParams {
70    /// Subvoxel shifts on each side of 0 (`-nshifts`, default 20): `2·nshifts + 1` shifts in all.
71    pub nshifts: usize,
72    /// Nearest neighbour offset in the oscillation measure (`-minW`, default 1).
73    pub min_w: usize,
74    /// Farthest neighbour offset in the oscillation measure (`-maxW`, default 3).
75    pub max_w: usize,
76}
77
78impl Default for MrtrixUnringParams {
79    fn default() -> Self {
80        Self { nshifts: 20, min_w: 1, max_w: 3 }
81    }
82}
83
84/// MRtrix's `indexshift`: the signed Fourier index of bin `n` (`n > size/2` wraps negative).
85fn indexshift(n: usize, size: usize) -> f64 {
86    if n > size / 2 { n as f64 - size as f64 } else { n as f64 }
87}
88
89/// 3-D FFT of a complex volume in place (Fortran order, x fastest), unnormalised both ways, as
90/// MRtrix's `Math::FFT` (FFTW) along axes 0, 1, 2.
91fn fft3(data: &mut [Complex64], dims: [usize; 3], inverse: bool) {
92    let mut planner = FftPlanner::<f64>::new();
93    for axis in 0..3 {
94        let n = dims[axis];
95        if n < 2 {
96            continue;
97        }
98        let plan = if inverse { planner.plan_fft_inverse(n) } else { planner.plan_fft_forward(n) };
99        let (stride, starts) = line_starts(dims, axis);
100        let mut mat = gather(data, n, stride, &starts);
101        #[cfg(feature = "parallel")]
102        mat.par_chunks_mut(n).for_each(|row| plan.process(row));
103        #[cfg(not(feature = "parallel"))]
104        mat.chunks_mut(n).for_each(|row| plan.process(row));
105        scatter(&mat, data, n, stride, &starts, false);
106    }
107}
108
109/// The stride along `axis` and the start index of every line along it.
110fn line_starts(dims: [usize; 3], axis: usize) -> (usize, Vec<usize>) {
111    let [nx, ny, nz] = dims;
112    match axis {
113        0 => (1, (0..ny * nz).map(|l| l * nx).collect()),
114        1 => (nx, (0..nz).flat_map(|z| (0..nx).map(move |x| z * nx * ny + x)).collect()),
115        _ => (nx * ny, (0..ny).flat_map(|y| (0..nx).map(move |x| y * nx + x)).collect()),
116    }
117}
118
119fn gather(data: &[Complex64], n: usize, stride: usize, starts: &[usize]) -> Vec<Complex64> {
120    let mut mat = vec![Complex64::new(0.0, 0.0); starts.len() * n];
121    for (l, &s) in starts.iter().enumerate() {
122        for i in 0..n {
123            mat[l * n + i] = data[s + i * stride];
124        }
125    }
126    mat
127}
128
129fn scatter(mat: &[Complex64], data: &mut [Complex64], n: usize, stride: usize, starts: &[usize], add: bool) {
130    for (l, &s) in starts.iter().enumerate() {
131        for i in 0..n {
132            if add {
133                data[s + i * stride] += mat[l * n + i];
134            } else {
135                data[s + i * stride] = mat[l * n + i];
136            }
137        }
138    }
139}
140
141/// Everything a line of length `n` needs: the shifts, the shift factors and the FFT plans.
142struct LineCtx {
143    n: usize,
144    shifts: Vec<f64>,
145    /// `phases[f*n + m] = exp(2πi·indexshift(m)·shifts[f]/n)`, the Nyquist bin of an even `n` 0.
146    phases: Vec<Complex64>,
147    fft: Arc<dyn Fft<f64>>,
148    ifft: Arc<dyn Fft<f64>>,
149    params: MrtrixUnringParams,
150}
151
152impl LineCtx {
153    fn new(n: usize, params: MrtrixUnringParams) -> Self {
154        let nsh = params.nshifts;
155        // MRtrix's zero-centred `shift_ind`: 0, then +j/(2·nsh+1), then −j/(2·nsh+1).
156        let mut shifts = vec![0.0; 2 * nsh + 1];
157        for j in 0..nsh {
158            shifts[j + 1] = (j + 1) as f64 / (2.0 * nsh as f64 + 1.0);
159            shifts[1 + nsh + j] = -shifts[j + 1];
160        }
161        let mut phases = vec![Complex64::new(0.0, 0.0); shifts.len() * n];
162        for (f, &s) in shifts.iter().enumerate() {
163            for m in 0..n {
164                let arg = 2.0 * indexshift(m, n) * std::f64::consts::PI * s / n as f64;
165                phases[f * n + m] = Complex64::new(0.0, arg).exp();
166            }
167            if n.is_multiple_of(2) {
168                phases[f * n + n / 2] = Complex64::new(0.0, 0.0);
169            }
170        }
171        let mut planner = FftPlanner::<f64>::new();
172        LineCtx { n, shifts, phases, fft: planner.plan_fft_forward(n), ifft: planner.plan_fft_inverse(n), params }
173    }
174
175    /// MRtrix's `LineProcessor` on one line: returns the unrung line divided by `scale`.
176    fn unring(&self, line: &[Complex64], scale: f64, shifted: &mut [Complex64], out: &mut [Complex64]) {
177        let n = self.n;
178        let nf = self.shifts.len();
179        let mut spec = line.to_vec();
180        self.fft.process(&mut spec);
181        for f in 0..nf {
182            let row = &mut shifted[f * n..f * n + n];
183            for m in 0..n {
184                row[m] = spec[m] * self.phases[f * n + m];
185            }
186            self.ifft.process(row);
187        }
188        let wrap = |i: isize| i.rem_euclid(n as isize) as usize;
189        let (min_w, max_w) = (self.params.min_w as isize, self.params.max_w as isize);
190        let d = |a: Complex64, b: Complex64| (a.re - b.re).abs() + (a.im - b.im).abs();
191        for (p, o) in out.iter_mut().enumerate() {
192            let pi = p as isize;
193            let mut ind = 0;
194            let mut opt = f64::MAX;
195            for f in 0..nf {
196                let row = &shifted[f * n..f * n + n];
197                let (mut left, mut right) = (0.0, 0.0);
198                for k in min_w..=max_w {
199                    left += d(row[wrap(pi - k)], row[wrap(pi - k - 1)]);
200                    right += d(row[wrap(pi + k)], row[wrap(pi + k + 1)]);
201                }
202                let tv = left.min(right);
203                if tv < opt {
204                    opt = tv;
205                    ind = f;
206                }
207            }
208            let s = self.shifts[ind];
209            let row = &shifted[ind * n..ind * n + n];
210            let (a0, a1, a2) = (row[wrap(pi - 1)], row[p], row[wrap(pi + 1)]);
211            *o = if s > 0.0 { (a1 - (a1 - a0) * s) / scale } else { (a1 + (a1 - a2) * s) / scale };
212        }
213    }
214}
215
216/// `mrdegibbs -dimensionality 3` on one complex volume (Fortran order, x fastest): the
217/// reference's output as is. See the [module docs](self).
218///
219/// # Panics
220/// If `vol` is not `nx·ny·nz` long, or `min_w >= max_w` (as `mrdegibbs` refuses).
221pub fn gibbs_unring_3d_complex(vol: &[Complex64], dims: (usize, usize, usize), params: MrtrixUnringParams) -> Vec<Complex64> {
222    let dims = [dims.0, dims.1, dims.2];
223    let n_vox = dims[0] * dims[1] * dims[2];
224    assert_eq!(vol.len(), n_vox, "vol length must be nx*ny*nz");
225    assert!(params.min_w < params.max_w, "min_w must be smaller than max_w");
226    let mut ft = vol.to_vec();
227    fft3(&mut ft, dims, false);
228    let x: [Vec<f64>; 3] = std::array::from_fn(|d| {
229        (0..dims[d]).map(|m| 1.0 + (2.0 * std::f64::consts::PI * indexshift(m, dims[d]) / dims[d] as f64).cos()).collect()
230    });
231    let scale_vol = n_vox as f64;
232    let mut out = vec![Complex64::new(0.0, 0.0); n_vox];
233    for axis in 0..3 {
234        // filter for this axis, then back to the image domain
235        let mut filt = ft.clone();
236        for k in 0..dims[2] {
237            for j in 0..dims[1] {
238                for i in 0..dims[0] {
239                    let w = [x[1][j] * x[2][k], x[0][i] * x[2][k], x[0][i] * x[1][j]];
240                    let den = w[0] + w[1] + w[2];
241                    let idx = i + j * dims[0] + k * dims[0] * dims[1];
242                    filt[idx] *= if den == 0.0 { 0.0 } else { w[axis] / den };
243                }
244            }
245        }
246        fft3(&mut filt, dims, true);
247        // unring along the axis and add to the output
248        let n = dims[axis];
249        let ctx = LineCtx::new(n, params);
250        let scale = scale_vol * n as f64;
251        let (stride, starts) = line_starts(dims, axis);
252        let src = gather(&filt, n, stride, &starts);
253        let mut res = vec![Complex64::new(0.0, 0.0); src.len()];
254        let nf = ctx.shifts.len();
255        #[cfg(feature = "parallel")]
256        res.par_chunks_mut(n).zip(src.par_chunks(n)).for_each_init(
257            || vec![Complex64::new(0.0, 0.0); nf * n],
258            |sh, (o, l)| ctx.unring(l, scale, sh, o),
259        );
260        #[cfg(not(feature = "parallel"))]
261        {
262            let mut sh = vec![Complex64::new(0.0, 0.0); nf * n];
263            for (o, l) in res.chunks_mut(n).zip(src.chunks(n)) {
264                ctx.unring(l, scale, &mut sh, o);
265            }
266        }
267        scatter(&res, &mut out, n, stride, &starts, true);
268    }
269    out
270}
271
272/// 3-D Gibbs unringing (Bautista et al. 2021, MRtrix3's `mrdegibbs -dimensionality 3`) of one
273/// real volume, e.g. a magnitude image from a 3-D acquisition: the real part of
274/// [`gibbs_unring_3d_complex`], negative values set to 0.
275///
276/// For 2-D multi-slice acquisitions use Kellner's slice-wise method instead.
277///
278/// # Panics
279/// As [`gibbs_unring_3d_complex`].
280pub fn gibbs_unring_3d(vol: &[f64], dims: (usize, usize, usize), params: MrtrixUnringParams) -> Vec<f64> {
281    let c: Vec<Complex64> = vol.iter().map(|&v| Complex64::new(v, 0.0)).collect();
282    gibbs_unring_3d_complex(&c, dims, params).iter().map(|v| v.re.max(0.0)).collect()
283}
284
285/// MRtrix's `Unring1D` on one line, in place: `data` is the line's spectrum (unnormalised FFT)
286/// on input and the unrung line, scaled by `n` (the unnormalised inverse transform), on output.
287///
288/// Shifts `0, ±1, …, ±nsh` in units of `1/(2·nsh)` voxel, applied as `exp(iπ·shift·m/(n·nsh))`
289/// on bins `m = 1…maxn` and the conjugate on their mirrors (the even-length Nyquist bin zeroed
290/// in the shifted lines, kept in the unshifted one); the oscillation measure is a running sum
291/// of `|ΔRe| + |ΔIm|` over the windows `minW..=maxW` left and right, updated per voxel as
292/// MRtrix updates it.
293fn unring1d_line(data: &mut [Complex64], ifft: &dyn Fft<f64>, p: MrtrixUnringParams, sh: &mut [Complex64], buf: &mut [Complex64]) {
294    let n = data.len();
295    let nsh = p.nshifts;
296    let nf = 2 * nsh + 1;
297    let (min_w, max_w) = (p.min_w as isize, p.max_w as isize);
298    let shift = |j: usize| -> f64 {
299        if j == 0 {
300            0.0
301        } else if j <= nsh {
302            j as f64
303        } else {
304            -((j - nsh) as f64)
305        }
306    };
307    let maxn = if n.is_multiple_of(2) { n / 2 - 1 } else { (n - 1) / 2 };
308    // the original line, then each shift
309    buf.copy_from_slice(data);
310    ifft.process(buf);
311    sh[..n].copy_from_slice(buf);
312    for j in 1..nf {
313        let phi = std::f64::consts::PI * shift(j) / (n * nsh) as f64;
314        let u = Complex64::new(phi.cos(), phi.sin());
315        let mut e = Complex64::new(1.0, 0.0);
316        buf[0] = data[0];
317        if n.is_multiple_of(2) {
318            buf[n / 2] = Complex64::new(0.0, 0.0);
319        }
320        for l in 0..maxn {
321            e = u * e;
322            buf[l + 1] = e * data[l + 1];
323            buf[n - 1 - l] = e.conj() * data[n - 1 - l];
324        }
325        ifft.process(buf);
326        sh[j * n..j * n + n].copy_from_slice(buf);
327    }
328    let w = |i: isize| i.rem_euclid(n as isize) as usize;
329    let at = |i: isize, j: usize| sh[j * n + w(i)];
330    let mut tv1 = vec![0.0_f64; nf];
331    let mut tv2 = vec![0.0_f64; nf];
332    let ni = n as isize;
333    for j in 0..nf {
334        for t in min_w..=max_w {
335            tv1[j] += (at(ni - t, j).re - at(ni - t - 1, j).re).abs();
336            tv1[j] += (at(ni - t, j).im - at(ni - t - 1, j).im).abs();
337            tv2[j] += (at(ni + t, j).re - at(ni + t + 1, j).re).abs();
338            tv2[j] += (at(ni + t, j).im - at(ni + t + 1, j).im).abs();
339        }
340    }
341    for l in 0..ni {
342        let mut min_tv = f64::MAX;
343        let mut min_idx = 0;
344        for j in 0..nf {
345            if tv1[j] < min_tv {
346                min_tv = tv1[j];
347                min_idx = j;
348            }
349            if tv2[j] < min_tv {
350                min_tv = tv2[j];
351                min_idx = j;
352            }
353            tv1[j] += (at(l - min_w + 1, j).re - at(l - min_w, j).re).abs();
354            tv1[j] -= (at(l - max_w, j).re - at(l - (max_w + 1), j).re).abs();
355            tv2[j] += (at(l + max_w + 1, j).re - at(l + (max_w + 2), j).re).abs();
356            tv2[j] -= (at(l + min_w, j).re - at(l + (min_w + 1), j).re).abs();
357
358            tv1[j] += (at(l - min_w + 1, j).im - at(l - min_w, j).im).abs();
359            tv1[j] -= (at(l - max_w, j).im - at(l - (max_w + 1), j).im).abs();
360            tv2[j] += (at(l + max_w + 1, j).im - at(l + (max_w + 2), j).im).abs();
361            tv2[j] -= (at(l + min_w, j).im - at(l + (min_w + 1), j).im).abs();
362        }
363        let (a0, a1, a2) = (at(l - 1, min_idx), at(l, min_idx), at(l + 1, min_idx));
364        let s = shift(min_idx) / (2.0 * nsh as f64);
365        data[l as usize] = if s > 0.0 {
366            Complex64::new(a1.re * (1.0 - s) + a0.re * s, a1.im * (1.0 - s) + a0.im * s)
367        } else {
368            Complex64::new(a1.re * (1.0 + s) - a2.re * s, a1.im * (1.0 + s) - a2.im * s)
369        };
370    }
371}
372
373/// FFT plans and scratch for one slice shape (`nr` rows along the first in-plane axis, `nc`
374/// columns along the second).
375struct SliceCtx {
376    nr: usize,
377    nc: usize,
378    fwd_r: Arc<dyn Fft<f64>>,
379    fwd_c: Arc<dyn Fft<f64>>,
380    inv_r: Arc<dyn Fft<f64>>,
381    inv_c: Arc<dyn Fft<f64>>,
382    p: MrtrixUnringParams,
383}
384
385impl SliceCtx {
386    fn new(nr: usize, nc: usize, p: MrtrixUnringParams) -> Self {
387        let mut planner = FftPlanner::<f64>::new();
388        SliceCtx {
389            nr,
390            nc,
391            fwd_r: planner.plan_fft_forward(nr),
392            fwd_c: planner.plan_fft_forward(nc),
393            inv_r: planner.plan_fft_inverse(nr),
394            inv_c: planner.plan_fft_inverse(nc),
395            p,
396        }
397    }
398
399    /// MRtrix's `Unring2D` on one slice, in place (`s[r + c·nr]`).
400    fn unring(&self, s: &mut [Complex64]) {
401        let (nr, nc) = (self.nr, self.nc);
402        let mut row = vec![Complex64::new(0.0, 0.0); nc];
403        // FFT along each row (over the columns), then along each column
404        let along_row = |s: &mut [Complex64], f: &dyn Fft<f64>, row: &mut [Complex64]| {
405            for r in 0..nr {
406                for c in 0..nc {
407                    row[c] = s[r + c * nr];
408                }
409                f.process(row);
410                for c in 0..nc {
411                    s[r + c * nr] = row[c];
412                }
413            }
414        };
415        let along_col = |s: &mut [Complex64], f: &dyn Fft<f64>| {
416            for c in 0..nc {
417                f.process(&mut s[c * nr..c * nr + nr]);
418            }
419        };
420        along_row(s, &*self.fwd_c, &mut row);
421        along_col(s, &*self.fwd_r);
422        // split into the part unrung along the rows' axis (weight ck) and the other (cj)
423        let mut s2 = vec![Complex64::new(0.0, 0.0); nr * nc];
424        for k in 0..nc {
425            let ck = (1.0 + (2.0 * std::f64::consts::PI * (k as f64 / nc as f64)).cos()) * 0.5;
426            for j in 0..nr {
427                let cj = (1.0 + (2.0 * std::f64::consts::PI * (j as f64 / nr as f64)).cos()) * 0.5;
428                let i = j + k * nr;
429                if ck + cj != 0.0 {
430                    s2[i] = s[i] * cj / (ck + cj);
431                    s[i] *= ck / (ck + cj);
432                } else {
433                    s[i] = Complex64::new(0.0, 0.0);
434                    s2[i] = Complex64::new(0.0, 0.0);
435                }
436            }
437        }
438        along_row(s, &*self.inv_c, &mut row);
439        along_col(&mut s2, &*self.inv_r);
440        let nf = 2 * self.p.nshifts + 1;
441        let mut sh = vec![Complex64::new(0.0, 0.0); nf * nr.max(nc)];
442        let mut buf = vec![Complex64::new(0.0, 0.0); nr.max(nc)];
443        // `s` is a spectrum along the columns' axis: unring each column
444        for c in 0..nc {
445            unring1d_line(&mut s[c * nr..c * nr + nr], &*self.inv_r, self.p, &mut sh[..nf * nr], &mut buf[..nr]);
446        }
447        // `s2` is a spectrum along the rows' axis: unring each row
448        for r in 0..nr {
449            for c in 0..nc {
450                row[c] = s2[r + c * nr];
451            }
452            unring1d_line(&mut row, &*self.inv_c, self.p, &mut sh[..nf * nc], &mut buf[..nc]);
453            for c in 0..nc {
454                s2[r + c * nr] = row[c];
455            }
456        }
457        let scale = (nr * nc) as f64;
458        for (a, b) in s.iter_mut().zip(&s2) {
459            *a = (*a + b) / scale;
460        }
461    }
462}
463
464/// `mrdegibbs` (2-D, Kellner et al. 2016, MRtrix3's default mode) on one complex volume (Fortran
465/// order, x fastest): each slice normal to `slice_axis` is unrung in 2-D, its in-plane axes in
466/// increasing order (`slice_axis` 2 is MRtrix's default `-axes 0,1`). The reference's output as
467/// is. See the [module docs](self).
468///
469/// # Panics
470/// If `vol` is not `nx·ny·nz` long, `slice_axis > 2`, or `min_w >= max_w`.
471pub fn gibbs_unring_2d_complex(vol: &[Complex64], dims: (usize, usize, usize), slice_axis: usize, params: MrtrixUnringParams) -> Vec<Complex64> {
472    let d = [dims.0, dims.1, dims.2];
473    assert_eq!(vol.len(), d[0] * d[1] * d[2], "vol length must be nx*ny*nz");
474    assert!(slice_axis < 3, "slice_axis must be 0, 1 or 2");
475    assert!(params.min_w < params.max_w, "min_w must be smaller than max_w");
476    let (a0, a1) = match slice_axis {
477        0 => (1, 2),
478        1 => (0, 2),
479        _ => (0, 1),
480    };
481    let st = [1, d[0], d[0] * d[1]];
482    let (nr, nc, ns) = (d[a0], d[a1], d[slice_axis]);
483    let ctx = SliceCtx::new(nr, nc, params);
484    let idx = |r: usize, c: usize, z: usize| r * st[a0] + c * st[a1] + z * st[slice_axis];
485    let mut slices = vec![Complex64::new(0.0, 0.0); nr * nc * ns];
486    for z in 0..ns {
487        for c in 0..nc {
488            for r in 0..nr {
489                slices[z * nr * nc + r + c * nr] = vol[idx(r, c, z)];
490            }
491        }
492    }
493    #[cfg(feature = "parallel")]
494    slices.par_chunks_mut(nr * nc).for_each(|s| ctx.unring(s));
495    #[cfg(not(feature = "parallel"))]
496    slices.chunks_mut(nr * nc).for_each(|s| ctx.unring(s));
497    let mut out = vec![Complex64::new(0.0, 0.0); vol.len()];
498    for z in 0..ns {
499        for c in 0..nc {
500            for r in 0..nr {
501                out[idx(r, c, z)] = slices[z * nr * nc + r + c * nr];
502            }
503        }
504    }
505    out
506}
507
508/// MRtrix3's 2-D `mrdegibbs` (Kellner et al. 2016) of one real volume, e.g. a magnitude image
509/// from a 2-D multi-slice acquisition: the real part of [`gibbs_unring_2d_complex`], negative
510/// values set to 0.
511///
512/// # Panics
513/// As [`gibbs_unring_2d_complex`].
514pub fn gibbs_unring_2d(vol: &[f64], dims: (usize, usize, usize), slice_axis: usize, params: MrtrixUnringParams) -> Vec<f64> {
515    let c: Vec<Complex64> = vol.iter().map(|&v| Complex64::new(v, 0.0)).collect();
516    gibbs_unring_2d_complex(&c, dims, slice_axis, params).iter().map(|v| v.re.max(0.0)).collect()
517}
518
519#[cfg(test)]
520mod tests {
521    use super::*;
522
523    /// A box truncated in k-space along all three axes rings along all three; the 3-D unringing
524    /// takes out most of the ringing in the background.
525    #[test]
526    fn test_unring_3d_reduces_overshoot() {
527        let (nx, ny, nz) = (24, 20, 16);
528        let n = nx * ny * nz;
529        let mut img = vec![0.0; n];
530        for k in 4..12 {
531            for j in 5..15 {
532                for i in 6..18 {
533                    img[i + j * nx + k * nx * ny] = 100.0;
534                }
535            }
536        }
537        let mut c: Vec<Complex64> = img.iter().map(|&v| Complex64::new(v, 0.0)).collect();
538        fft3(&mut c, [nx, ny, nz], false);
539        let keep = |m: usize, len: usize| indexshift(m, len).abs() < len as f64 / 4.0;
540        for k in 0..nz {
541            for j in 0..ny {
542                for i in 0..nx {
543                    if !(keep(i, nx) && keep(j, ny) && keep(k, nz)) {
544                        c[i + j * nx + k * nx * ny] = Complex64::new(0.0, 0.0);
545                    }
546                }
547            }
548        }
549        fft3(&mut c, [nx, ny, nz], true);
550        let rung: Vec<f64> = c.iter().map(|v| v.re / n as f64).collect();
551        let err = |a: &[f64]| a.iter().zip(&img).map(|(x, y)| (x - y).powi(2)).sum::<f64>().sqrt();
552        let un = gibbs_unring_3d(&rung, (nx, ny, nz), MrtrixUnringParams::default());
553        assert!(un.iter().all(|&v| v >= 0.0));
554        // ringing in the background, 2+ voxels from the box
555        let far = |a: &[f64]| {
556            let mut s = 0.0;
557            for k in 0..nz { for j in 0..ny { for i in 0..nx {
558                let out = |x: usize, lo: usize, hi: usize| x + 2 <= lo || x >= hi + 2;
559                if out(i, 6, 18) || out(j, 5, 15) || out(k, 4, 12) { s += a[i + j * nx + k * nx * ny].powi(2); }
560            }}}
561            s.sqrt()
562        };
563        assert!(far(&un) < 0.5 * far(&rung), "background ringing {} -> {}", far(&rung), far(&un));
564        assert!(err(&un) < err(&rung), "error {} -> {}", err(&rung), err(&un));
565    }
566
567    /// Odd axes (no Nyquist bin) and a constant volume: the shifts change nothing.
568    #[test]
569    fn test_unring_3d_keeps_a_constant() {
570        let dims = (7, 9, 5);
571        let v = vec![3.0; 7 * 9 * 5];
572        let out = gibbs_unring_3d(&v, dims, MrtrixUnringParams::default());
573        assert!(out.iter().all(|&x| (x - 3.0).abs() < 1e-12), "{:?}", &out[..5]);
574    }
575
576    /// The 2-D mode on odd and even slices along every slice axis: a constant is unchanged.
577    #[test]
578    fn test_unring_2d_keeps_a_constant() {
579        let dims = (7, 8, 5);
580        let v = vec![3.0; 7 * 8 * 5];
581        for axis in 0..3 {
582            let out = gibbs_unring_2d(&v, dims, axis, MrtrixUnringParams::default());
583            assert!(out.iter().all(|&x| (x - 3.0).abs() < 1e-12), "axis {axis}: {:?}", &out[..5]);
584        }
585    }
586}