ENH: python: Add magnitude_only mode, fix three bugs, tighten output dtype and docstrings - #1
Conversation
tsalo
left a comment
There was a problem hiding this comment.
Thank you for addressing those bugs- they all look solid. I'm not really a fan of adding a magnitude_only parameter though. I think it's fine to infer if phase data should be used by if phase data are present, even if that doesn't align with the original MATLAB implementation.
Co-authored-by: Taylor Salo <tsalo90@gmail.com>
Co-authored-by: Taylor Salo <tsalo90@gmail.com>
Co-authored-by: Taylor Salo <tsalo90@gmail.com>
Co-authored-by: Taylor Salo <tsalo90@gmail.com>
|
Thanks for the review! The four suggestions are all clear improvements, I've just applied them.
On magnitude_only: My one concern with pure inference is silent fallback — if a caller meant to pass phase data but forgot, they'd silently get a different algorithm with no warning, and Vizioli 2021 shows complex NORDIC removes meaningfully more thermal noise. Would a compromise work: drop the parameter as you suggest, but emit a UserWarning when pha_file is None and temporal_phase != 0 ("no phase data provided; running magnitude-only NORDIC")? That keeps the inference-from-arguments API and prevents the silent-fallback case. Happy to push that as a follow-up commit. |
Note: issues appear disabled on this repository, so opening this PR directly.
This PR adds support for ARG.magnitude_only=1 mode in the Python NORDIC implementation (
python/src/nordic/denoise.py), fixes three bugs in the same file, tightens the output dtype and docstrings, and adds pytest tests covering the magnitude_only mode and dtype behaviour (python/tests/test_matlab_parity.py). It includes an output-comparison notebook on a public dataset (OpenNeuro ds004928) showing the resulting Python output matches MATLAB NIFTI_NORDIC across all three algorithm flavours.Changes
Bug fixes in
python/src/nordic/denoise.pyThree issues in
subfunction_loop_for_nvr_avg_update:rangeDatalength mismatch —vals[: R - centering + 1]is one element longer thanrangeMP, breaking the element-wise divisionrangeData / rangeMP. Fixed by trimming the trailing element.np.wherecrash — whensigmasq_2 < sigmasq_1is empty (noise never exceeds signal), the code raisedIndexError. Now interpreted as "no components are noise" — keep all singular values.first_removed_component = S.sizecan exceedlen(sigmasq_2); guarded in both update paths.New:
magnitude_onlymodeMirrors MATLAB
ARG.magnitude_only=1:magnitude_only=Trueforcestemporal_phase=0and ignores anypha_file(with warnings).pha_file=Nonewithtemporal_phase != 0is auto-corrected with a warning. Previously this silently applied a phase filter to non-existent phase data, introducing a spurious complex modulation.Output dtype
float32on the magnitude and phase outputs (matches MATLABinfo.Datatype = single). Without this, integer-typed inputs (e.g. int16 BOLD) were silently truncated by nibabel on write.Docstring clarifications
Document the actual defaults for
kernel_size_gfactor([14, 14, 1, 90], with the temporal axis capped atn_vols) andkernel_size_pca(cubic patch with 11:1 spatial:temporal ratio, with slice-clamping whenn_slices < cubic edge).Validation
Tested against the MATLAB reference (
NIFTI_NORDIC.m) on three subjects of the publicly available OpenNeurods004928(Faes/Vizioli/Moeller, 7T submillimetre auditory fMRI with magnitude, phase, and trailing noise volumes), across all three documented algorithm flavours.Headline numbers:
nordicgfactor+mppcamppcanordicgfactor+mppcamppcanordicgfactor+mppcamppcaAll 9 cells clear r ≥ 0.9999 voxel-wise, median rel-diff < 1 %, and tSNR ratio within ~1 % of unity. Residuals scale with algorithmic complexity (more LAPACK-ordering exposure → looser parity), consistent with float32 SVD ordering noise.
The notebook (
python/validation/matlab_parity_ds004928.ipynb) contains the full reproducibility recipe — DataLad install through both Python and MATLAB invocations — so the result can be replicated end-to-end from public data.