From eb103a1039b4f8a30eae71dfac775b14a1a34963 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar <MichielCottaar@protonmail.com> Date: Fri, 6 Dec 2019 11:59:48 +0000 Subject: [PATCH] ENH: testing DTI (no kurtosis) single- and multi-shell exact fit (no noise) --- unit_tests/fdt/dtifit/feedsRun | 158 +++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100755 unit_tests/fdt/dtifit/feedsRun diff --git a/unit_tests/fdt/dtifit/feedsRun b/unit_tests/fdt/dtifit/feedsRun new file mode 100755 index 0000000..15f5528 --- /dev/null +++ b/unit_tests/fdt/dtifit/feedsRun @@ -0,0 +1,158 @@ +#!/usr/bin/env fslpython +""" +Generates data following the diffusion tensor model +""" +import sys +import os +from subprocess import run +import numpy as np +from numpy import testing +import nibabel as nib + +OUTDIR = sys.argv[0] + + +def gen_data(): + """ + Populates given directory with diffusion data + + :yield: directories containing reference noiseless data with the eigen-vectors and eigen-values used to generate the data + """ + directory = 'dti' + + # eigen vectors vary along x-axis + eigen_vectors = np.array([ + [(1., 0, 0), (0, 1, 0), (0, 0, 1)], + [(1., 0, 0), (0, np.sqrt(0.5), np.sqrt(0.5)), (0, np.sqrt(0.5), -np.sqrt(0.5))], + [(np.sqrt(0.5), -np.sqrt(0.5), 0), (np.sqrt(0.25), np.sqrt(0.25), np.sqrt(0.5)), + (np.sqrt(0.25), np.sqrt(0.25), -np.sqrt(0.5))], + ])[:, None, None, :, :] + + for idx1 in range(3): + for idx2 in range(3): + testing.assert_allclose((eigen_vectors[..., idx1, :] * eigen_vectors[..., idx2, :]).sum(-1), + 1 if idx1 == idx2 else 0., atol=1e-8) + + # eigen-values vary along y-axis + # eigen-values can not be the same or eigen-vectors will be ill-defined + eigen_values = np.array([ + [1.2, 1., 0.8], + [1., 0.5, 0.], + [2., 1.9, 1.8], + ])[None, :, None, :, None] * 1e-3 + + # S0 varies along the z-axis + S0 = np.array([1., 1.5])[None, None, :, None, None] * 1000. + + eigen_vectors, eigen_values, S0 = np.broadcast_arrays(eigen_vectors, eigen_values, S0) + S0 = S0[:, :, :, 0, 0] + + diffusion_tensor = (eigen_values[..., None] * eigen_vectors[..., None, :] * eigen_vectors[..., :, None]).sum(-3) + + for multi_shell in (False, True): # we should get the same results for single or multi-shell data + directory += '_multi' if multi_shell else '_single' + for kurt in (0, ):#1, 2): # 0: no kurtosis; 1: single kurtosis; 2: parallel and perpendicular kurtosis + if kurt != 0 and not multi_shell: + continue + directory += ['', '_kurt', '_kurtdir'][kurt] + + bvals = np.full(50, 1000.) + if multi_shell: + bvals[25:] = 2000. + bvals[::10] = 0. + + run(['gps', '--ndir=50', '--out=bvecs'], check=True) + bvecs = np.genfromtxt('bvecs') + assert bvecs.shape == (50, 3), f"GPS produced bvecs-file with shape {bvecs.shape} rather than the expected (50, 3)" + testing.assert_allclose((bvecs ** 2).sum(-1), 1.) + + kurt_val = np.array([ + [0, 0, 0.], + [1., 1., 1.], + [1., 0.5, 0.5], + ][kurt]) + data = S0[..., None] * np.exp(np.sum( + -(bvals[:, None] * eigen_values[..., None, :, 0] + + kurt_val * eigen_values[..., None, :, 0] ** 2 * bvals[:, None] ** 2) * + np.sum(bvecs[:, None, :] * eigen_vectors[..., None, :, :], -1) ** 2, axis=-1 + )) + + if (data / S0[..., None]).min() < 0.01: + raise ValueError("dtifit rounds attenuations below 0.01 up to 0.01, " + "so these low values should not be in the test data") + + for flipped in (False, True): + if flipped: + directory += '_flipped' + + if not os.path.isdir(directory): + os.mkdir(directory) + + if flipped: + np.savetxt(f'{directory}/bvals', bvals[:, None]) + np.savetxt(f'{directory}/bvecs', bvecs.T) + else: + np.savetxt(f'{directory}/bvals', bvals[None, :]) + np.savetxt(f'{directory}/bvecs', bvecs) + + affine = np.eye(4) * 1.25 + affine[-1, -1] = 1. + for idx in range(3): + nib.Nifti1Image(eigen_vectors[..., idx, :], affine=affine).to_filename(f'{directory}/ref_V{idx + 1}.nii.gz') + nib.Nifti1Image(eigen_values[..., idx, 0], affine=affine).to_filename(f'{directory}/ref_L{idx + 1}.nii.gz') + nib.Nifti1Image(S0, affine=affine).to_filename(f'{directory}/ref_S0.nii.gz') + nib.Nifti1Image(data, affine=affine).to_filename(f'{directory}/ref_data.nii.gz') + nib.Nifti1Image(np.ones(data.shape[:3], dtype=int), affine=affine).to_filename(f'{directory}/nodif_brain_mask.nii.gz') + + tensor_components = diffusion_tensor[:, :, :, [0, 0, 0, 1, 1, 2], [0, 1, 2, 1, 2, 2]] + nib.Nifti1Image(tensor_components, affine=affine).to_filename(f'{directory}/ref_tensor.nii.gz') + yield directory, kurt + + +def fit_data(directory, kurt=0): + for wls in (False, True): # in this noise-free data the --wls flag should not matter + base_output = f'{directory}/dti{"_wls" if wls else ""}' + cmd = [ + 'dtifit', + '-k', f'{directory}/ref_data.nii.gz', + '-m', f'{directory}/nodif_brain_mask.nii.gz', + '-r', f'{directory}/bvecs', + '-b', f'{directory}/bvals', + '-o', base_output, + '--sse', + '--save_tensor', + ] + if wls: + cmd += ['--wls'] + if kurt == 1: + cmd += ['--kurt'] + if kurt == 2: + cmd += ['--kurt'] + run(cmd, check=True) + yield base_output + + +for directory, kurt in gen_data(): + for base_output in fit_data(directory, kurt): + def compare(name): + ref = nib.load(f'{directory}/ref_{name}.nii.gz').get_fdata() + fit = nib.load(f'{base_output}_{name}.nii.gz').get_fdata() + assert ref.shape == fit.shape, f'incorrect NIFTI image shape for {name}' + testing.assert_allclose(ref, fit, atol=1e-8, + err_msg=f'mismatch in {name}') + + compare('S0') + compare('tensor') + for idx in (3, 2, 1): + compare(f'L{idx}') + + ref = nib.load(f'{directory}/ref_V{idx}.nii.gz').get_fdata() + fit = nib.load(f'{base_output}_V{idx}.nii.gz').get_fdata() + assert ref.shape == fit.shape + inner = (ref * fit).sum(-1) + testing.assert_allclose(abs(inner), 1.) + + sse = nib.load(f'{base_output}_sse.nii.gz').get_fdata() + testing.assert_allclose(sse, 0., atol=1e-8) + + -- GitLab