Skip to main content

multi_echo_robust_fit

Function multi_echo_robust_fit 

Source
pub fn multi_echo_robust_fit(
    unwrapped_phases: &[impl AsRef<[f64]>],
    mags: &[impl AsRef<[f64]>],
    tes: &[f64],
    mask: &[u8],
    params: &RobustFitParams,
) -> RobustFitResult
Expand description

Multi-echo fit that downweights echoes which disagree with the rest, and reports which.

Iteratively reweighted least squares around multi_echo_linear_fit’s model (phase = intercept + slope·TE, magnitude-weighted), with a robust loss applied to each echo’s residual. An echo that is corrupted — through-plane motion, a spin-history dropout, a shot artefact, anything that is not a straight line in TE — is pulled out of the fit at the voxels where it misbehaves, instead of dragging the slope at every voxel it touches.

Supported by Biondetti et al., MRM 2022, which found weighted and nonlinear-fit echo combination clearly outperforms unweighted averaging or plain linear fitting.

§The scale is global, and it has to be

IRLS needs a scale to measure “large” against. The textbook choice is a per-observation-set MAD, which here means the MAD of one voxel’s n_echoes residuals — and that is unusable on this data. A four-echo fit with an intercept has two degrees of freedom, so a per-voxel MAD is the MAD of four numbers of which two are structurally near zero; it collapses toward zero at a large fraction of voxels, and dividing by it rejects healthy echoes essentially at random.

So the scale is estimated once per iteration over the whole mask, from residuals standardised by magnitude. Phase noise goes as 1/SNR, so residual × magnitude is roughly homoscedastic across the volume and a single scale is meaningful for all of it. That is also what makes the per-echo fractions in EchoQuality comparable between echoes: every echo is being measured against the same ruler.

§What it does not detect

Most of what follows is measured at four echoes, which is this function’s own minimum. With an intercept the model has two parameters, so four is the fewest that leaves anything to detect with, and the tests sit exactly on that boundary. More echoes improve both halves markedly — on the same corruption, going from four to six tightens the clean-series score spread from 1.88 to 1.34 (more headroom under the flagging threshold) and takes the slope correction from 9% to 28% of the plain fit’s error. Read the limitations below as the four-echo case, not as the method.

It reports more reliably than it repairs. Detection and correction are separate capabilities with different sensitivities, and the gap is worth knowing about. On the phantom, a dropout over 12% of the volume is flagged (score ratio 9.8) while the field map it produces is neither better nor worse than the plain fit (0.0259 against 0.0246). The same is true of corruption severe enough to pull the per-voxel fit at the voxels it affects: the echo is named every time, but the weighting recovers progressively less of the field as the corruption grows. Treat a flag as “look at this echo”, not as “this has been handled”.

Corruption of the first or last echo, as readily as a middle one. The endpoints of a linear fit carry the most leverage, so the fit follows them and their residuals shrink. An echo in the middle of the train is the easiest to catch and the first and last the hardest.

A series in which an echo carries no signal. This wants n_params + 2 echoes carrying signal, not merely present, and it cannot tell the difference: a near-dead echo scores ~0 because the score is magnitude-weighted, and the remaining three points then put all their leverage on the middle echo, which gets flagged with nothing wrong with it. Drop a dead echo before believing this. a_dead_echo_causes_a_known_false_positive pins the behaviour and records the two fixes that do not work.

Anything that is still linear in TE. A per-echo scaling of the whole field, or a corruption that happens to look like a different slope, fits the model perfectly well and leaves no residual to find. What this detects is departure from a straight line, which is what motion, dropout and shot artefacts produce — not every way an echo can be wrong.

§How many echoes this needs

Robustness costs degrees of freedom that a short echo train does not have. With estimate_offset the model has two parameters, so n_echoes must be at least 4 for a corrupted echo to be distinguishable from the fit; without it, at least 3. Below that there is nothing to detect with and this returns the ordinary magnitude-weighted fit unchanged, with every quality figure zeroed and nothing flagged — not an error, because a short series is not a mistake, but the caller should not read a clean report as evidence of a clean series. Check EchoQuality::len against the echo count if that distinction matters.

Even at 4 echoes this is working near its limit: one corrupted echo is 25% of the data, and the fit it is being measured against is itself pulled by that echo. Expect it to catch an echo that is badly wrong and to be unreliable about one that is marginally wrong.

§Arguments

Identical to multi_echo_linear_fit except that the two fit options are carried in params: unwrapped phase and magnitude per echo, echo times in seconds, a mask, and RobustFitParams.