Skip to content

Commit 642f19e

Browse files
authored
Merge pull request #365 from jhlegarreta/tst/comprehensive-dmri-to-nifti-testing
TST: Add comprehensive testing for `data.dmri.io.to_nifti`
2 parents ec545eb + f5660d6 commit 642f19e

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed

test/test_data_dmri.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"""Unit tests exercising the dMRI data structure."""
2424

2525
import re
26+
import warnings
2627
from pathlib import Path
2728

2829
import attrs
@@ -40,6 +41,7 @@
4041
GRADIENT_BVAL_BVEC_PRIORITY_WARN_MSG,
4142
GRADIENT_DATA_MISSING_ERROR,
4243
from_nii,
44+
to_nifti,
4345
)
4446
from nifreeze.data.dmri.utils import (
4547
DEFAULT_LOWB_THRESHOLD,
@@ -606,6 +608,164 @@ def test_load_gradients_missing(tmp_path, setup_random_dwi_data):
606608
from_nii(dwi_fname)
607609

608610

611+
@pytest.mark.parametrize("vol_size", [(4, 4, 5)])
612+
@pytest.mark.parametrize("b0_count", [0, 1])
613+
@pytest.mark.parametrize("bval_min, bval_max", [(800.0, 1200.0)])
614+
@pytest.mark.parametrize("provide_bzero", [False, True])
615+
@pytest.mark.parametrize("insert_b0", [False, True])
616+
@pytest.mark.parametrize("motion_affines", [None, 2 * np.eye(4)])
617+
@pytest.mark.parametrize("bvals_dec_places, bvecs_dec_places", [(2, 6), (1, 4)])
618+
@pytest.mark.parametrize("file_basename", [None, "dwi.nii.gz"])
619+
def test_to_nifti(
620+
request,
621+
tmp_path,
622+
monkeypatch,
623+
vol_size,
624+
b0_count,
625+
bval_min,
626+
bval_max,
627+
provide_bzero,
628+
insert_b0,
629+
motion_affines,
630+
bvals_dec_places,
631+
bvecs_dec_places,
632+
file_basename,
633+
):
634+
rng = request.node.rng
635+
636+
# Choose n_vols safely above the minimum DTI orientations
637+
n_vols = max(10, DTI_MIN_ORIENTATIONS + 2)
638+
639+
# Build b-values array: first b0_count are zeros
640+
non_b0_count = n_vols - b0_count
641+
# Sample non-b0 bvals between min and max values
642+
rest_bvals = rng.uniform(bval_min, bval_max, size=non_b0_count)
643+
bvals = np.concatenate((np.zeros(b0_count), rest_bvals)).astype(int)
644+
645+
# Create bvecs and assemble gradients
646+
bzeros = np.zeros((b0_count, 3))
647+
bvecs = normalized_vector(rng.random((3, non_b0_count)), axis=0).T
648+
bvecs = np.vstack((bzeros, bvecs))
649+
gradients = np.column_stack((bvecs, bvals))
650+
651+
# Create random dataobj with shape
652+
dataobj = rng.standard_normal((*vol_size, n_vols)).astype(float)
653+
654+
# Optionally supply a bzero
655+
provided = None
656+
affine = np.eye(4)
657+
_motion_affines = (
658+
np.stack([motion_affines] * non_b0_count) if motion_affines is not None else None
659+
)
660+
if provide_bzero:
661+
# Use a constant map so it's easy to assert equality
662+
provided = np.full((*vol_size, max(1, b0_count)), 42.0, dtype=float).squeeze()
663+
dwi_obj = DWI(
664+
dataobj=dataobj,
665+
affine=affine,
666+
motion_affines=_motion_affines,
667+
gradients=gradients,
668+
bzero=provided,
669+
)
670+
else:
671+
dwi_obj = DWI(
672+
dataobj=dataobj, affine=affine, motion_affines=_motion_affines, gradients=gradients
673+
)
674+
675+
_filename = tmp_path / file_basename if file_basename is not None else file_basename
676+
677+
# Monkeypatch the to_nifti alias to only perform essential operations for
678+
# the purpose of this test
679+
def simple_to_nifti(_dataset, filename=None, write_hmxfms=None, order=None):
680+
_ = write_hmxfms
681+
_ = order
682+
_nii = nb.Nifti1Image(_dataset.dataobj, _dataset.affine)
683+
if filename is not None:
684+
_nii.to_filename(filename)
685+
return _nii
686+
687+
monkeypatch.setattr("nifreeze.data.dmri.io._base_to_nifti", simple_to_nifti)
688+
689+
with warnings.catch_warnings(record=True) as caught:
690+
nii = to_nifti(
691+
dwi_obj,
692+
_filename,
693+
write_hmxfms=False,
694+
order=3,
695+
insert_b0=insert_b0,
696+
bvals_dec_places=bvals_dec_places,
697+
bvecs_dec_places=bvecs_dec_places,
698+
)
699+
700+
no_bzero = dwi_obj.bzero is None or not insert_b0
701+
702+
# Check the warning
703+
if no_bzero:
704+
if insert_b0:
705+
assert (
706+
str(caught[0].message)
707+
== "Ignoring ``insert_b0`` argument as the data object's bzero field is unset"
708+
)
709+
710+
bvecs_dwi = dwi_obj.bvecs
711+
bvals_dwi = dwi_obj.bvals
712+
# Transform bvecs if motion affines are present
713+
if dwi_obj.motion_affines is not None:
714+
rotated = [
715+
transform_fsl_bvec(_bvec, _affine, dwi_obj.affine, invert=True)
716+
for _bvec, _affine in zip(bvecs_dwi, dwi_obj.motion_affines, strict=True)
717+
]
718+
bvecs_dwi = np.asarray(rotated)
719+
720+
# Check the primary NIfTI output
721+
_dataobj = dwi_obj.dataobj
722+
# Concatenate the b0 if the primary data has a b0 volume or if it was
723+
# requested to do so
724+
if not no_bzero:
725+
assert dwi_obj.bzero is not None
726+
# ToDo
727+
# The code will concatenate as many zeros as they exist to the data
728+
_dataobj = np.concatenate((dwi_obj.bzero[..., np.newaxis], dwi_obj.dataobj), axis=-1)
729+
# But when inserting b0 data to the gradients, it inserts a single b0.
730+
# Here I will insert as many values as b0 volumes to make the test fail
731+
dwi_b0_count = dwi_obj.bzero.shape[-1] if dwi_obj.bzero.ndim == 4 else 1
732+
bvals_dwi = np.concatenate((np.zeros(dwi_b0_count), bvals_dwi))
733+
bvecs_dwi = np.vstack((np.zeros((dwi_b0_count, bvecs_dwi.shape[1])), bvecs_dwi))
734+
735+
assert isinstance(nii, nb.Nifti1Image)
736+
assert np.allclose(nii.get_fdata(), _dataobj)
737+
assert np.allclose(nii.affine, dwi_obj.affine)
738+
739+
# Check the written files, if any
740+
if _filename is None:
741+
assert not any(tmp_path.iterdir()), "Directory is not empty"
742+
else:
743+
# Check the written NIfTI file
744+
assert _filename.is_file()
745+
746+
_nii_load = load_api(_filename, nb.Nifti1Image)
747+
748+
# Build a NIfTI file with the data object that potentially contains
749+
# concatenated b0 data
750+
_nii_dataobj = nb.Nifti1Image(_dataobj, nii.affine, nii.header)
751+
752+
np.allclose(_nii_dataobj.get_fdata(), _nii_load.get_fdata())
753+
np.allclose(_nii_dataobj.affine, _nii_load.affine)
754+
755+
# Check gradients
756+
bvecs_file = _filename.with_suffix("").with_suffix(".bvec")
757+
bvals_file = _filename.with_suffix("").with_suffix(".bval")
758+
assert bvals_file.is_file()
759+
assert bvecs_file.is_file()
760+
761+
# Read the files
762+
bvals_from_file = np.loadtxt(bvals_file)
763+
bvecs_from_file = np.loadtxt(bvecs_file).T
764+
765+
assert np.allclose(bvals_from_file, bvals_dwi, rtol=0, atol=10**-bvals_dec_places)
766+
assert np.allclose(bvecs_from_file, bvecs_dwi, rtol=0, atol=10**-bvecs_dec_places)
767+
768+
609769
@pytest.mark.skip(reason="to_nifti takes absurdly long")
610770
@pytest.mark.parametrize("insert_b0", (False, True))
611771
@pytest.mark.parametrize("rotate_bvecs", (False, True))

0 commit comments

Comments
 (0)