Skip to content

NODDI fitting - #16

Merged
antonio-dimeglio merged 5 commits into
mainfrom
noddi
Oct 31, 2026
Merged

antonio-dimeglio merged 5 commits into
mainfrom
noddi

Conversation

@antonio-dimeglio

@antonio-dimeglio antonio-dimeglio commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Adds NODDI to Larmor: larmor.dwi.NoddiModel / NoddiFit and the lar noddi command, with a guide page.

Method. Each voxel starts from a dictionary of NODDI signals about its DTI direction, solved as AMICO
does (free water by NNLS, the tissue support by a non-negative L1 + L2 solve, then NNLS on that support).
From there it is fitted as the NODDI Matlab Toolbox 1.05 fits WatsonSHStickTortIsoV_B0: the toolbox's
order-12 series for the Watson sticks and its Watson-averaged zeppelin, S0 from the positive b0s, sigma
from their spread divided by 100, the Rician likelihood, the toolbox's parameter encoding and its two
stages (fractions first, then all five parameters including the direction). The optimiser is a bounded
BFGS with numerical gradients in place of MATLAB's fmincon. The whole voxel loop is C++ on all threads.

Supporting changes.

  • solve_ridge_nnls takes an L1 weight (with x >= 0 it is a linear term, so it shifts the
    correlations, and the ridge rows of the QR path get -l1 / ridge); bound as _core.ridge_l1_nnls.
  • The fits read the masked voxels in chunks (signal_rows, signal_chunks, finite_rows in
    larmor.dwi.inputs) instead of copying every brain voxel as float64 first: DTI, DKI, the WM response,
    CSD and its response, MSMT-CSD and its responses, and mFOD (which also turns each chunk's amplitudes
    into SH before the next). At the default chunk size the outputs are identical to before on clean and
    damaged data; with chunks of a few voxels they differ only by rounding (about 1e-8 relative in
    MSMT-CSD), which tests/dwi/test_chunked_reading.py now checks.
  • compartment_attenuation releases the GIL; the scaled Bessel I0 moves to common/maths.

Validation (reference data in tests/data/noddi, made by make_noddi_phantom.py and
tests/matlab_scripts/make_noddi_ref.m):

  • Noise-free phantom: the fit recovers the truth to < 1e-3 (measured 5e-5); the toolbox to 3e-4.
  • Noisy phantom (SNR 30): errors equal the toolbox's (v_ic 0.037 for both); voxel by voxel the mean
    difference to the toolbox is 0.0007; the 2 voxels that differ by more than 0.02 have a lower negative
    log-likelihood with Larmor.
  • HCP subject 100307, a 1104-voxel crop: mean difference to the toolbox 0.000 (v_ic), 0.001 (OD),
    0.000 (v_iso), directions within 0.0 degrees median; 0.3 s against the toolbox's 211 s.
  • Whole HCP brain (741k voxels, 288 volumes): 3.5 min, peak memory 3.4 GB.
  • C++ tests check the series against the exact Watson integral, the whole signal against Zhang et al.'s
    equations, the coefficient joins, the averaged zeppelin, and noise-free recovery including the direction.

…a dictionary start

Each voxel starts from the dictionary solve (free water by NNLS, the tissue support by a
non-negative L1 + L2 solve, the weights by NNLS on it) and is then fitted as the NODDI Matlab
Toolbox 1.05 fits WatsonSHStickTortIsoV_B0: its order-12 series for the Watson sticks, its
Watson-averaged zeppelin, S0 from the positive b0s, sigma from their spread divided by 100, the
Rician likelihood, its encoding of the parameters and its two stages. The voxel loop runs in C++
on all threads; a whole HCP subject takes 3.5 minutes.

The scaled Bessel function I0 moves from the simulation code to common/maths, so both use it.
Adds the NODDI guide page and its citations.
… and tighter tests

Rename the Python NODDI tests so that mypy no longer sees two test_noddi
modules, initialise every field of NoddiSettings and use std::numbers::pi
for GCC and MSVC, and split the optimiser into small functions with longer
names for clang-tidy. The fit now warns about the voxels it cannot fit,
refuses a table with one shell and starting directions that are zero or not
finite, and normalises the others. The comparison with the toolbox is
bounded at about twice the measured differences, and a new test checks that
the chunked readers do not change DTI, DKI or CSD.
@antonio-dimeglio antonio-dimeglio changed the title NODDI fitting, from a dictionary start to the NODDI toolbox's Rician fit NODDI fitting Oct 31, 2026
@antonio-dimeglio
antonio-dimeglio merged commit 83ec685 into main Oct 31, 2026
10 checks passed
@antonio-dimeglio
antonio-dimeglio deleted the noddi branch October 31, 2026 04:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant