Skip to content

Commit a5122f7

Browse files
authored
Merge pull request #26 from dPys/enh/vectors
ENH: Vector representation and checking utilities
2 parents 3c5d0f1 + d2bc103 commit a5122f7

File tree

7 files changed

+598
-0
lines changed

7 files changed

+598
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ language: python
66
cache:
77
directories:
88
- $HOME/.cache/pip
9+
- $HOME/.cache/data
910

1011
python:
1112
- 3.6

dmriprep/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@
55
import numpy as np
66
import nibabel as nb
77
import pytest
8+
from dipy.data.fetcher import _make_fetcher, UW_RW_URL
9+
10+
_dipy_datadir_root = os.getenv('DMRIPREP_TESTS_DATA') or Path.home()
11+
dipy_datadir = Path(_dipy_datadir_root) / '.cache' / 'data'
12+
dipy_datadir.mkdir(parents=True, exist_ok=True)
13+
14+
_make_fetcher(
15+
"fetch_sherbrooke_3shell",
16+
str(dipy_datadir),
17+
UW_RW_URL + "1773/38475/",
18+
['HARDI193.nii.gz', 'HARDI193.bval', 'HARDI193.bvec'],
19+
['HARDI193.nii.gz', 'HARDI193.bval', 'HARDI193.bvec'],
20+
['0b735e8f16695a37bfbd66aab136eb66',
21+
'e9b9bb56252503ea49d31fb30a0ac637',
22+
'0c83f7e8b917cd677ad58a078658ebb7'],
23+
doc="Download a 3shell HARDI dataset with 192 gradient direction")()
24+
25+
_sherbrooke_data = {
26+
'dwi_file': dipy_datadir / "HARDI193.nii.gz",
27+
'bvecs': np.loadtxt(dipy_datadir / "HARDI193.bvec").T,
28+
'bvals': np.loadtxt(dipy_datadir / "HARDI193.bval"),
29+
}
830

931

1032
@pytest.fixture(autouse=True)
@@ -15,7 +37,14 @@ def doctest_autoimport(doctest_namespace):
1537
doctest_namespace['os'] = os
1638
doctest_namespace['Path'] = Path
1739
doctest_namespace['data_dir'] = Path(__file__).parent / 'data' / 'tests'
40+
doctest_namespace['dipy_datadir'] = dipy_datadir
1841
tmpdir = tempfile.TemporaryDirectory()
1942
doctest_namespace['tmpdir'] = tmpdir.name
2043
yield
2144
tmpdir.cleanup()
45+
46+
47+
@pytest.fixture()
48+
def dipy_test_data(scope='session'):
49+
"""Create a temporal directory shared across tests to pull data in."""
50+
return _sherbrooke_data

dmriprep/interfaces/vectors.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Handling the gradient table."""
2+
from pathlib import Path
3+
import numpy as np
4+
from nipype.utils.filemanip import fname_presuffix
5+
from nipype.interfaces.base import (
6+
SimpleInterface, BaseInterfaceInputSpec, TraitedSpec,
7+
File, traits, isdefined
8+
)
9+
from ..utils.vectors import DiffusionGradientTable, B0_THRESHOLD, BVEC_NORM_EPSILON
10+
11+
12+
class _CheckGradientTableInputSpec(BaseInterfaceInputSpec):
13+
dwi_file = File(exists=True, mandatory=True)
14+
in_bvec = File(exists=True, xor=['in_rasb'])
15+
in_bval = File(exists=True, xor=['in_rasb'])
16+
in_rasb = File(exists=True, xor=['in_bval', 'in_bvec'])
17+
b0_threshold = traits.Float(B0_THRESHOLD, usedefault=True)
18+
bvec_norm_epsilon = traits.Float(BVEC_NORM_EPSILON, usedefault=True)
19+
b_scale = traits.Bool(True, usedefault=True)
20+
21+
22+
class _CheckGradientTableOutputSpec(TraitedSpec):
23+
out_rasb = File(exists=True)
24+
out_bval = File(exists=True)
25+
out_bvec = File(exists=True)
26+
full_sphere = traits.Bool()
27+
pole = traits.Tuple(traits.Float, traits.Float, traits.Float)
28+
b0_ixs = traits.List(traits.Int)
29+
30+
31+
class CheckGradientTable(SimpleInterface):
32+
"""
33+
Ensure the correctness of the gradient table.
34+
35+
Example
36+
-------
37+
38+
>>> os.chdir(tmpdir)
39+
>>> check = CheckGradientTable(
40+
... dwi_file=str(data_dir / 'dwi.nii.gz'),
41+
... in_rasb=str(data_dir / 'dwi.tsv')).run()
42+
>>> check.outputs.pole
43+
(0.0, 0.0, 0.0)
44+
>>> check.outputs.full_sphere
45+
True
46+
47+
>>> check = CheckGradientTable(
48+
... dwi_file=str(data_dir / 'dwi.nii.gz'),
49+
... in_bvec=str(data_dir / 'bvec'),
50+
... in_bval=str(data_dir / 'bval')).run()
51+
>>> check.outputs.pole
52+
(0.0, 0.0, 0.0)
53+
>>> check.outputs.full_sphere
54+
True
55+
>>> newrasb = np.loadtxt(check.outputs.out_rasb, skiprows=1)
56+
>>> oldrasb = np.loadtxt(str(data_dir / 'dwi.tsv'), skiprows=1)
57+
>>> np.allclose(newrasb, oldrasb, rtol=1.e-3)
58+
True
59+
60+
"""
61+
62+
input_spec = _CheckGradientTableInputSpec
63+
output_spec = _CheckGradientTableOutputSpec
64+
65+
def _run_interface(self, runtime):
66+
rasb_file = _undefined(self.inputs, 'in_rasb')
67+
68+
table = DiffusionGradientTable(
69+
self.inputs.dwi_file,
70+
bvecs=_undefined(self.inputs, 'in_bvec'),
71+
bvals=_undefined(self.inputs, 'in_bval'),
72+
rasb_file=rasb_file,
73+
b_scale=self.inputs.b_scale,
74+
bvec_norm_epsilon=self.inputs.bvec_norm_epsilon,
75+
b0_threshold=self.inputs.b0_threshold,
76+
)
77+
pole = table.pole
78+
self._results['pole'] = tuple(pole)
79+
self._results['full_sphere'] = np.all(pole == 0.0)
80+
self._results['b0_ixs'] = np.where(table.b0mask)[0].tolist()
81+
82+
cwd = Path(runtime.cwd).absolute()
83+
if rasb_file is None:
84+
rasb_file = fname_presuffix(
85+
self.inputs.dwi_file, use_ext=False, suffix='.tsv',
86+
newpath=str(cwd))
87+
table.to_filename(rasb_file)
88+
self._results['out_rasb'] = rasb_file
89+
table.to_filename('%s/dwi' % cwd, filetype='fsl')
90+
self._results['out_bval'] = str(cwd / 'dwi.bval')
91+
self._results['out_bvec'] = str(cwd / 'dwi.bvec')
92+
return runtime
93+
94+
95+
def _undefined(objekt, name, default=None):
96+
value = getattr(objekt, name)
97+
if not isdefined(value):
98+
return default
99+
return value

dmriprep/utils/tests/__init__.py

Whitespace-only changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Test vector utilities."""
2+
import pytest
3+
import numpy as np
4+
from dmriprep.utils import vectors as v
5+
from collections import namedtuple
6+
7+
8+
def test_corruption(tmpdir, dipy_test_data, monkeypatch):
9+
"""Check whether b-value rescaling is operational."""
10+
tmpdir.chdir()
11+
12+
bvals = dipy_test_data['bvals']
13+
bvecs = dipy_test_data['bvecs']
14+
15+
dgt = v.DiffusionGradientTable(**dipy_test_data)
16+
affine = dgt.affine.copy()
17+
18+
# Test vector hemisphere coverage
19+
assert np.all(dgt.pole == [0., 0., 0.])
20+
21+
dgt.to_filename('dwi.tsv')
22+
dgt = v.DiffusionGradientTable(rasb_file='dwi.tsv')
23+
assert dgt.normalized is False
24+
with pytest.raises(TypeError):
25+
dgt.to_filename('dwi', filetype='fsl') # You can do this iff the affine is set.
26+
27+
# check accessing obj.affine
28+
dgt = v.DiffusionGradientTable(dwi_file=namedtuple('Affine', ['affine'])(affine))
29+
assert np.all(dgt.affine == affine)
30+
dgt = v.DiffusionGradientTable(dwi_file=affine)
31+
assert np.all(dgt.affine == affine)
32+
33+
# Perform various corruption checks using synthetic corrupted bval-bvec.
34+
dgt = v.DiffusionGradientTable()
35+
dgt.bvecs = bvecs
36+
with pytest.raises(ValueError):
37+
dgt.bvals = bvals[:-1]
38+
39+
dgt = v.DiffusionGradientTable()
40+
dgt.bvals = bvals
41+
with pytest.raises(ValueError):
42+
dgt.bvecs = bvecs[:-1]
43+
44+
# Missing b0
45+
bval_no_b0 = bvals.copy()
46+
bval_no_b0[0] = 51
47+
with pytest.raises(ValueError):
48+
dgt = v.DiffusionGradientTable(dwi_file=dipy_test_data['dwi_file'],
49+
bvals=bval_no_b0, bvecs=bvecs)
50+
bvec_no_b0 = bvecs.copy()
51+
bvec_no_b0[0] = np.array([1.0, 0.0, 0.0])
52+
with pytest.raises(ValueError):
53+
dgt = v.DiffusionGradientTable(dwi_file=dipy_test_data['dwi_file'],
54+
bvals=bvals, bvecs=bvec_no_b0)
55+
56+
# Corrupt b0 b-val
57+
bval_odd_b0 = bvals.copy()
58+
bval_odd_b0[bval_odd_b0 == 0] = 1e-8
59+
dgt = v.DiffusionGradientTable(dwi_file=dipy_test_data['dwi_file'],
60+
bvals=bval_odd_b0, bvecs=bvecs)
61+
assert dgt.bvals[0] == 0
62+
63+
# Corrupt b0 b-vec
64+
bvec_odd_b0 = bvecs.copy()
65+
b0mask = np.all(bvec_odd_b0 == 0, axis=1)
66+
bvec_odd_b0[b0mask] = [10, 10, 10]
67+
dgt = v.DiffusionGradientTable(dwi_file=dipy_test_data['dwi_file'],
68+
bvals=bvals, bvecs=bvec_odd_b0)
69+
assert np.all(dgt.bvecs[b0mask] == [0., 0., 0.])
70+
71+
# Test normalization
72+
bvecs_factor = 2.0 * bvecs
73+
dgt = v.DiffusionGradientTable(dwi_file=dipy_test_data['dwi_file'],
74+
bvals=bvals, bvecs=bvecs_factor)
75+
assert -1.0 <= np.max(np.abs(dgt.gradients[..., :-1])) <= 1.0
76+
assert dgt.normalized is True
77+
78+
def mock_func(*args, **kwargs):
79+
return 'called!'
80+
81+
with monkeypatch.context() as m:
82+
m.setattr(v, 'normalize_gradients', mock_func)
83+
assert dgt.normalize() is None # Test nothing is executed.
84+
85+
with monkeypatch.context() as m:
86+
m.setattr(v, 'bvecs2ras', mock_func)
87+
assert dgt.generate_vecval() is None # Test nothing is executed.
88+
89+
# Miscellaneous tests
90+
with pytest.raises(ValueError):
91+
dgt.to_filename('path', filetype='mrtrix')

0 commit comments

Comments
 (0)