qsm_core/inversion/iqfm.rs
1//! iQFM single-step deep-learning tissue-field mapping (`onnx` feature).
2//!
3//! iQFM (Gao 2022) is the **local (tissue) field** output of the *same network as
4//! iQSM* — a learnable-Laplacian "LoT" front-end + U-Net that maps raw wrapped MRI
5//! **phase** (radians) straight to the background-removed local field (ppm),
6//! folding phase unwrapping and background-field removal into one network. It is
7//! the `iQFM` head of the authors' repo (weights `iQFM_40_v2.pth` +
8//! `LoTLayer_lfs_40_v2.pth`), the sibling of the χ head used by [`super::iqsm`].
9//!
10//! The exported graph, the four+border inputs, the preprocessing (phase sign,
11//! sphere erosion, centre-pad to /16, mask, crop) and the multi-echo combination
12//! ([`IqsmEchoWeighting`]) are **identical** to iQSM — only the trained weights and the meaning
13//! of the output (local field vs susceptibility) differ. So the Rust glue simply
14//! runs iQSM's exact pipeline with the `iqfm.onnx` bytes.
15//!
16//! Weights are not bundled; the caller passes the exported `iqfm.onnx` bytes.
17//!
18//! # Reference pipeline
19//!
20//! Identical to [iQSM](mod@crate::inversion::iqsm#reference-pipeline): the authors run iQFM through the
21//! same `run_iqsm` (`sunhongfu/iQSM` `inference.py:139-237`), which requires a brain mask for the
22//! iQFM output (README).
23//!
24//! # Agreement with the original
25//!
26//! `tests/models_onnx/field.rs` runs it on a 36×32×28 box of in-vivo data (1 mm, 2.89 T, across the
27//! brain's edge) against its original's verified QSM-CI entry, iQFM bb163ef (`iqfm`)
28//! (QSMxT/QSM-CI#316; the fixture is regenerated in Actions by
29//! `tests/data/dl_field_qsmci_small.py`): per echo, NRMSE 2.8e-3 to 6.8e-3 %, max |Δ| up to 4.0e-6
30//! ppm: float32 rounding.
31//!
32//! The original runs one echo at a time and combines the echoes afterwards. As for iQSM, the
33//! authors' MATLAB echo fit (`sunhongfu/iQSM_Plus` `legacy/matlab/iQFM.m:263-270`, and the iQFM
34//! output of `sunhongfu/iQSM` `legacy/matlab/demo_multi_echo.m:152-160`) weights by mag·TE², an
35//! option here, and their Python driver (`sunhongfu/iQSM` `run.py:170`, `app.py:416`) by
36//! (mag·TE)², the inverse-variance weight and the default ([`IqsmEchoWeighting`]). On the box's first and third echoes the driver's
37//! combination is (mag·TE)² of its per-echo outputs to 5e-6 %, and the two weightings differ by
38//! 3.4 % NRMSE; [`iqfm_multi_echo`] matches the MATLAB fit of the original's per-echo outputs to
39//! 2.7e-3 % (measured 2026-10-30), and with the Python weighting their (mag·TE)² combination to
40//! 2.7e-3 % (measured 2026-10-31).
41
42use crate::grid::Grid;
43use crate::inversion::{IqsmEchoWeighting, TileConfig};
44use crate::models::onnx::OnnxError;
45
46/// Run iQFM on a single echo of wrapped phase; returns the local field (ppm),
47/// masked, column-major `(nx,ny,nz)`. See [`super::iqsm::iqsm`] for the argument
48/// semantics — they are shared verbatim (only the network and its output differ).
49#[allow(clippy::too_many_arguments)]
50pub fn iqfm(
51 phase_rad: &[f64],
52 mask: &[u8],
53 grid: &Grid,
54 te: f64,
55 b0: f64,
56 phase_sign: f64,
57 eroded_rad: i32,
58 model_onnx: &[u8],
59) -> Result<Vec<f64>, OnnxError> {
60 super::iqsm::iqsm(phase_rad, mask, grid, te, b0, phase_sign, eroded_rad, model_onnx)
61}
62
63/// Multi-echo iQFM: reconstruct each echo and combine them with `weighting` (default
64/// (magnitude·TE)², the inverse-variance weight; see [`IqsmEchoWeighting`]), exactly as
65/// [`super::iqsm::iqsm_multi_echo`], whole-volume or tiled (`tile`).
66#[allow(clippy::too_many_arguments)]
67pub fn iqfm_multi_echo(
68 phases: &[&[f64]],
69 magnitudes: &[&[f64]],
70 mask: &[u8],
71 grid: &Grid,
72 tes: &[f64],
73 b0: f64,
74 phase_sign: f64,
75 eroded_rad: i32,
76 model_onnx: &[u8],
77 tile: Option<&TileConfig>,
78 weighting: IqsmEchoWeighting,
79) -> Result<Vec<f64>, OnnxError> {
80 super::iqsm::lot_multi_echo(
81 "iqfm", phases, magnitudes, mask, grid, tes, b0, phase_sign, eroded_rad, model_onnx, tile, weighting,
82 )
83}