Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b7ca804
add new simulation interface
oesteban Mar 2, 2015
27ab170
advance new interface
oesteban Mar 2, 2015
cfc0987
Merge branch 'master' into enh/DipyMultiTensorSimulate
oesteban Apr 14, 2015
29bb49c
base implementation, now the mapper
oesteban Apr 14, 2015
c73fb80
add automated generation of gradient table, add sticks&balls
oesteban Apr 17, 2015
dcd0f15
add documentation
oesteban Apr 17, 2015
4748184
add noise, add test
oesteban Apr 17, 2015
04995a8
fix doctests
oesteban Apr 17, 2015
6b00c67
update CHANGES
oesteban Apr 17, 2015
3091e7e
fix error in input names
oesteban Apr 21, 2015
354371c
Merge branch 'master' into enh/DipyMultiTensorSimulate
oesteban Apr 22, 2015
e11a2e5
mask should be generated only from volume fractions
oesteban May 7, 2015
11ada63
Merge branch 'master' into enh/DipyMultiTensorSimulate
oesteban May 8, 2015
0ef93e8
allow for N number of fibers per voxel
oesteban May 8, 2015
53b602e
remove isotropic compartiments from parallelization
oesteban May 11, 2015
00a7d28
make diffusivity parameters modifiable
oesteban May 11, 2015
92c7c72
moving to dict as args
oesteban May 11, 2015
a6338da
delegate isotropic diffusion simulation to dipy
oesteban May 11, 2015
f7c2ff9
integrate S0 and snr into dipy routine
oesteban May 12, 2015
fc3f27c
check volume fractions before call multi_tensor
oesteban May 12, 2015
ef0a9c6
use multi_tensor instead of custom code in isotropic compartments
oesteban May 25, 2015
cbbd59b
Merge branch 'master' into enh/DipyMultiTensorSimulate
oesteban Jul 2, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Next release
============

* ENH: Added interface to simulate DWIs using the multi-tensor model
(https://github.com/nipy/nipype/pull/1085)
* ENH: New mesh.MeshWarpMaths to operate on surface-defined warpings
(https://github.com/nipy/nipype/pull/1016)
* FIX: Refactor P2PDistance, change name to ComputeMeshWarp, add regression tests,
Expand Down
6 changes: 3 additions & 3 deletions nipype/interfaces/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1349,9 +1349,9 @@ def _terminal_output_update(self):
def set_default_terminal_output(cls, output_type):
"""Set the default terminal output for CommandLine Interfaces.

This method is used to set default terminal output for
CommandLine Interfaces. However, setting this will not
update the output type for any existing instances. For these,
This method is used to set default terminal output for
CommandLine Interfaces. However, setting this will not
update the output type for any existing instances. For these,
assign the <instance>.inputs.terminal_output.
"""

Expand Down
1 change: 1 addition & 0 deletions nipype/interfaces/dipy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .tracks import TrackDensityMap
from .tensors import TensorMode
from .preprocess import Resample, Denoise
from .simulate import SimulateMultiTensor
274 changes: 274 additions & 0 deletions nipype/interfaces/dipy/simulate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
"""Change directory to provide relative paths for doctests
>>> import os
>>> filepath = os.path.dirname( os.path.realpath( __file__ ) )
>>> datadir = os.path.realpath(os.path.join(filepath, '../../testing/data'))
>>> os.chdir(datadir)
"""

from nipype.interfaces.base import (
traits, TraitedSpec, BaseInterface, BaseInterfaceInputSpec, File,
InputMultiPath, isdefined)
from nipype.utils.filemanip import split_filename
import os.path as op
import nibabel as nb
import numpy as np
from nipype.utils.misc import package_check
import warnings

from multiprocessing import (Process, Pool, cpu_count, pool,
Manager, TimeoutError)

from ... import logging
iflogger = logging.getLogger('interface')

have_dipy = True
try:
package_check('dipy', version='0.8.0')
except Exception, e:
have_dipy = False
else:
import numpy as np
from dipy.sims.voxel import (multi_tensor, add_noise,
all_tensor_evecs)
from dipy.core.gradients import gradient_table


class SimulateMultiTensorInputSpec(BaseInterfaceInputSpec):
in_dirs = InputMultiPath(File(exists=True), mandatory=True,
desc='list of fibers (principal directions)')
in_frac = InputMultiPath(File(exists=True), mandatory=True,
desc=('volume fraction of each fiber'))
in_vfms = InputMultiPath(File(exists=True), mandatory=True,
desc='volume fraction map')
in_mask = File(exists=True, desc='mask to simulate data')

n_proc = traits.Int(0, usedefault=True, desc='number of processes')
baseline = File(exists=True, mandatory=True, desc='baseline T2 signal')
gradients = File(exists=True, desc='gradients file')
in_bvec = File(exists=True, desc='input bvecs file')
in_bval = File(exists=True, desc='input bvals file')
num_dirs = traits.Int(32, usedefault=True,
desc=('number of gradient directions (when table '
'is automatically generated)'))
bvalues = traits.List(traits.Int, value=[1000, 3000], usedefault=True,
desc=('list of b-values (when table '
'is automatically generated)'))
out_file = File('sim_dwi.nii.gz', usedefault=True,
desc='output file with fractions to be simluated')
out_mask = File('sim_msk.nii.gz', usedefault=True,
desc='file with the mask simulated')
out_bvec = File('bvec.sim', usedefault=True, desc='simulated b vectors')
out_bval = File('bval.sim', usedefault=True, desc='simulated b values')
snr = traits.Int(30, usedefault=True, desc='signal-to-noise ratio (dB)')


class SimulateMultiTensorOutputSpec(TraitedSpec):
out_file = File(exists=True, desc='simulated DWIs')
out_mask = File(exists=True, desc='mask file')
out_bvec = File(exists=True, desc='simulated b vectors')
out_bval = File(exists=True, desc='simulated b values')


class SimulateMultiTensor(BaseInterface):

"""
Interface to MultiTensor model simulator in dipy
http://nipy.org/dipy/examples_built/simulate_multi_tensor.html

Example
-------

>>> import nipype.interfaces.dipy as dipy
>>> sim = dipy.SimulateMultiTensor()
>>> sim.inputs.in_dirs = ['fdir00.nii', 'fdir01.nii']
>>> sim.inputs.in_frac = ['ffra00.nii', 'ffra01.nii']
>>> sim.inputs.in_vfms = ['tpm_00.nii.gz', 'tpm_01.nii.gz',
... 'tpm_02.nii.gz']
>>> sim.inputs.baseline = 'b0.nii'
>>> sim.inputs.in_bvec = 'bvecs'
>>> sim.inputs.in_bval = 'bvals'
>>> sim.run() # doctest: +SKIP
"""
input_spec = SimulateMultiTensorInputSpec
output_spec = SimulateMultiTensorOutputSpec

def _run_interface(self, runtime):
# Load the baseline b0 signal
b0_im = nb.load(self.inputs.baseline)
hdr = b0_im.get_header()
shape = b0_im.get_shape()
aff = b0_im.get_affine()

ffsim = nb.concat_images([nb.load(f) for f in self.inputs.in_frac])
ffs = np.squeeze(ffsim.get_data()) # fiber fractions

vfsim = nb.concat_images([nb.load(f) for f in self.inputs.in_vfms])
vfs = np.squeeze(vfsim.get_data()) # volume fractions

total_ff = np.sum(ffs, axis=3)
total_vf = np.sum(vfs, axis=3)

msk = np.zeros(shape, dtype=np.uint8)
msk[(total_vf > 0.0) & (total_ff > 0.0)] = 1

if isdefined(self.inputs.in_mask):
msk = nb.load(self.inputs.in_mask).get_data()
msk[msk > 0.0] = 1.0
msk[msk < 1.0] = 0.0

mhdr = hdr.copy()
mhdr.set_data_dtype(np.uint8)
mhdr.set_xyzt_units('mm', 'sec')
nb.Nifti1Image(msk, aff, mhdr).to_filename(
op.abspath(self.inputs.out_mask))

args = np.hstack((vfs[msk > 0], ffs[msk > 0]))

for f in self.inputs.in_dirs:
fd = nb.load(f).get_data()
args = np.hstack((args, fd[msk > 0]))

b0 = np.array([b0_im.get_data()[msk > 0]]).T
args = np.hstack((args, b0))

if isdefined(self.inputs.bval) and isdefined(self.inputs.bvec):
# Load the gradient strengths and directions
bvals = np.loadtxt(self.inputs.bval)
bvecs = np.loadtxt(self.inputs.bvec).T

# Place in Dipy's preferred format
gtab = gradient_table(bvals, bvecs)
else:
gtab = _generate_gradients(self.inputs.num_dirs,
self.inputs.bvalues)

np.savetxt(op.abspath(self.inputs.out_bvec), gtab.bvecs.T)
np.savetxt(op.abspath(self.inputs.out_bval), gtab.bvals.T)

snr = self.inputs.snr
args = [tuple(np.hstack((r, gtab, snr))) for r in args]

n_proc = self.inputs.n_proc
if n_proc == 0:
n_proc = cpu_count()

try:
pool = Pool(processes=n_proc, maxtasksperchild=50)
except TypeError:
pool = Pool(processes=n_proc)

iflogger.info(('Starting simulation of %d voxels, %d diffusion'
' directions.') % (len(args), len(gtab.bvals)))
result = pool.map(_compute_voxel, args)
ndirs = np.shape(result)[1]

simulated = np.zeros((shape[0], shape[1], shape[2], ndirs))
simulated[msk > 0] = result

simhdr = hdr.copy()
simhdr.set_data_dtype(np.float32)
simhdr.set_xyzt_units('mm', 'sec')
nb.Nifti1Image(simulated.astype(np.float32), aff,
simhdr).to_filename(op.abspath(self.inputs.out_file))

return runtime

def _list_outputs(self):
outputs = self._outputs().get()
outputs['out_file'] = op.abspath(self.inputs.out_file)
outputs['out_mask'] = op.abspath(self.inputs.out_mask)
outputs['out_bvec'] = op.abspath(self.inputs.out_bvec)
outputs['out_bval'] = op.abspath(self.inputs.out_bval)

return outputs


def _compute_voxel(args):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know how this usually works in nipype, but it would be good to have clear documentation for what goes into args. There's lots of them!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use args to make the method 'pickleable' by multiprocessing

"""
Simulate DW signal for one voxel. Uses the multi-tensor model and
three isotropic compartments.

Apparent diffusivity tensors are taken from [Alexander2002]_
and [Pierpaoli1996]_.

.. [Alexander2002] Alexander et al., Detection and modeling of non-Gaussian
apparent diffusion coefficient profiles in human brain data, MRM
48(2):331-340, 2002, doi: `10.1002/mrm.10209
<http://dx.doi.org/10.1002/mrm.10209>`_.
.. [Pierpaoli1996] Pierpaoli et al., Diffusion tensor MR imaging
of the human brain, Radiology 201:637-648. 1996.
"""
D_ball = [3000e-6, 960e-6, 680e-6]
sf_evals = [1700e-6, 200e-6, 200e-6]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the tensor evals are always this value? Seems limiting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, we could easily add them in args. I wanted to start with a proof of concept.


vfs = [args[0], args[1], args[2]]
ffs = [args[3], args[4], args[5]] # single fiber fractions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always exactly three? Why not take lists as arguments and allow users to determine how many fibers they want?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As before, we could easily extend this, but for a first implementation I think three is a good tradeoff

sticks = [(args[6], args[7], args[8]),
(args[8], args[10], args[11]),
(args[12], args[13], args[14])]

S0 = args[15]
gtab = args[16]

nf = len(ffs)
mevals = [sf_evals] * nf
sf_vf = np.sum(ffs)
ffs = ((np.array(ffs) / sf_vf) * 100)

# Simulate sticks
signal, _ = multi_tensor(gtab, np.array(mevals), S0=1,
angles=sticks, fractions=ffs, snr=None)
signal *= sf_vf

# Simulate balls
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Should we also enable this functionality in the simulations in dipy (adding isotropic components)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @oesteban and @arokem. Having isotropic components is already supported (maybe it needs more documentation in the dipy docs).
For example here I am adding one anisotropic component and then an isotropic
mevals = np.array(([0.0015, 0.0003, 0.0003], [0.0020, 0.0020, 0.0020])
S = multi_tensor(gtab, mevals)

The trick is just to put the eigen-values equal and that makes a perfectly valid isotropic component.

The fractions should also add to 1 (or 100%) which is important. I am not sure if you are doing that correctly here.

So, I think the lines below are not necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Garyfallidis I started out doing that, but I got a better performance by simulating the isotropic compartments outside multi_tensor

r = 1.0 - sf_vf
if r > 1.0e-3:
for vf, d in zip(vfs, D_ball):
f0 = vf * r
signal += f0 * np.exp(-gtab.bvals * d)

snr = None
try:
snr = args[17]
except IndexError:
pass

if snr is not None and snr >= 0:
signal[1:] = add_noise(signal[1:], snr, 1)

return signal * S0


def _generate_gradients(ndirs=64, values=[1000, 3000], nb0s=1):
"""
Automatically generate a `gradient table
<http://nipy.org/dipy/examples_built/gradients_spheres.html#example-gradients-spheres>`_

"""
import numpy as np
from dipy.core.sphere import (disperse_charges, Sphere, HemiSphere)
from dipy.core.gradients import gradient_table

theta = np.pi * np.random.rand(ndirs)
phi = 2 * np.pi * np.random.rand(ndirs)
hsph_initial = HemiSphere(theta=theta, phi=phi)
hsph_updated, potential = disperse_charges(hsph_initial, 5000)

values = np.atleast_1d(values).tolist()
vertices = hsph_updated.vertices
bvecs = vertices.copy()
bvals = np.ones(vertices.shape[0]) * values[0]

for v in values[1:]:
bvecs = np.vstack((bvecs, vertices))
bvals = np.hstack((bvals, v * np.ones(vertices.shape[0])))

for i in xrange(0, nb0s):
bvals = bvals.tolist()
bvals.insert(0, 0)

bvecs = bvecs.tolist()
bvecs.insert(0, np.zeros(3))

return gradient_table(bvals, bvecs)
55 changes: 55 additions & 0 deletions nipype/interfaces/dipy/tests/test_auto_SimulateMultiTensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
from nipype.testing import assert_equal
from nipype.interfaces.dipy.simulate import SimulateMultiTensor

def test_SimulateMultiTensor_inputs():
input_map = dict(baseline=dict(mandatory=True,
),
bvalues=dict(usedefault=True,
),
gradients=dict(),
ignore_exception=dict(nohash=True,
usedefault=True,
),
in_bval=dict(),
in_bvec=dict(),
in_dirs=dict(mandatory=True,
),
in_frac=dict(mandatory=True,
),
in_mask=dict(),
in_vfms=dict(mandatory=True,
),
n_proc=dict(usedefault=True,
),
num_dirs=dict(usedefault=True,
),
out_bval=dict(usedefault=True,
),
out_bvec=dict(usedefault=True,
),
out_file=dict(usedefault=True,
),
out_mask=dict(usedefault=True,
),
snr=dict(usedefault=True,
),
)
inputs = SimulateMultiTensor.input_spec()

for key, metadata in input_map.items():
for metakey, value in metadata.items():
yield assert_equal, getattr(inputs.traits()[key], metakey), value

def test_SimulateMultiTensor_outputs():
output_map = dict(out_bval=dict(),
out_bvec=dict(),
out_file=dict(),
out_mask=dict(),
)
outputs = SimulateMultiTensor.output_spec()

for key, metadata in output_map.items():
for metakey, value in metadata.items():
yield assert_equal, getattr(outputs.traits()[key], metakey), value

Empty file added nipype/testing/data/b0.nii
Empty file.
Empty file added nipype/testing/data/fdir00.nii
Empty file.
Empty file added nipype/testing/data/fdir01.nii
Empty file.
Empty file added nipype/testing/data/ffra00.nii
Empty file.
Empty file added nipype/testing/data/ffra01.nii
Empty file.
Binary file modified nipype/testing/data/von-ray_errmap.nii.gz
Binary file not shown.
Binary file modified nipype/testing/data/von_errmap.nii.gz
Binary file not shown.
Loading