Skip to content

Commit 8ef4d64

Browse files
committed
Merge remote-tracking branch 'upstream/master' into enh/InterfaceLoadSettings
2 parents 8958e9b + 20c40ae commit 8ef4d64

File tree

9 files changed

+960
-9
lines changed

9 files changed

+960
-9
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ Upcoming release 0.13
22
=====================
33

44
* ENH: Convenient load/save of interface inputs (https://github.com/nipy/nipype/pull/1591)
5+
* ENH: Add a Framewise Displacement calculation interface (https://github.com/nipy/nipype/pull/1604)
56
* FIX: Use builtins open and unicode literals for py3 compatibility (https://github.com/nipy/nipype/pull/1572)
67
* TST: reduce the size of docker images & use tags for images (https://github.com/nipy/nipype/pull/1564)
78
* ENH: Implement missing inputs/outputs in FSL AvScale (https://github.com/nipy/nipype/pull/1563)

nipype/algorithms/confounds.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
'''
5+
Algorithms to compute confounds in :abbr:`fMRI (functional MRI)`
6+
7+
Change directory to provide relative paths for doctests
8+
>>> import os
9+
>>> filepath = os.path.dirname(os.path.realpath(__file__))
10+
>>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data'))
11+
>>> os.chdir(datadir)
12+
13+
'''
14+
from __future__ import print_function, division, unicode_literals, absolute_import
15+
from builtins import str, zip, range, open
16+
17+
import os.path as op
18+
import numpy as np
19+
20+
from .. import logging
21+
from ..external.due import due, BibTeX
22+
from ..interfaces.base import (traits, TraitedSpec, BaseInterface,
23+
BaseInterfaceInputSpec, File, isdefined)
24+
IFLOG = logging.getLogger('interface')
25+
26+
27+
class FramewiseDisplacementInputSpec(BaseInterfaceInputSpec):
28+
in_plots = File(exists=True, desc='motion parameters as written by FSL MCFLIRT')
29+
radius = traits.Float(50, usedefault=True,
30+
desc='radius in mm to calculate angular FDs, 50mm is the '
31+
'default since it is used in Power et al. 2012')
32+
out_file = File('fd_power_2012.txt', usedefault=True, desc='output file name')
33+
out_figure = File('fd_power_2012.pdf', usedefault=True, desc='output figure name')
34+
series_tr = traits.Float(desc='repetition time in sec.')
35+
save_plot = traits.Bool(False, usedefault=True, desc='write FD plot')
36+
normalize = traits.Bool(False, usedefault=True, desc='calculate FD in mm/s')
37+
figdpi = traits.Int(100, usedefault=True, desc='output dpi for the FD plot')
38+
figsize = traits.Tuple(traits.Float(11.7), traits.Float(2.3), usedefault=True,
39+
desc='output figure size')
40+
41+
class FramewiseDisplacementOutputSpec(TraitedSpec):
42+
out_file = File(desc='calculated FD per timestep')
43+
out_figure = File(desc='output image file')
44+
fd_average = traits.Float(desc='average FD')
45+
46+
class FramewiseDisplacement(BaseInterface):
47+
"""
48+
Calculate the :abbr:`FD (framewise displacement)` as in [Power2012]_.
49+
This implementation reproduces the calculation in fsl_motion_outliers
50+
51+
.. [Power2012] Power et al., Spurious but systematic correlations in functional
52+
connectivity MRI networks arise from subject motion, NeuroImage 59(3),
53+
2012. doi:`10.1016/j.neuroimage.2011.10.018
54+
<http://dx.doi.org/10.1016/j.neuroimage.2011.10.018>`_.
55+
56+
57+
"""
58+
59+
input_spec = FramewiseDisplacementInputSpec
60+
output_spec = FramewiseDisplacementOutputSpec
61+
62+
references_ = [{
63+
'entry': BibTeX("""\
64+
@article{power_spurious_2012,
65+
title = {Spurious but systematic correlations in functional connectivity {MRI} networks \
66+
arise from subject motion},
67+
volume = {59},
68+
doi = {10.1016/j.neuroimage.2011.10.018},
69+
number = {3},
70+
urldate = {2016-08-16},
71+
journal = {NeuroImage},
72+
author = {Power, Jonathan D. and Barnes, Kelly A. and Snyder, Abraham Z. and Schlaggar, \
73+
Bradley L. and Petersen, Steven E.},
74+
year = {2012},
75+
pages = {2142--2154},
76+
}
77+
"""),
78+
'tags': ['method']
79+
}]
80+
81+
def _run_interface(self, runtime):
82+
mpars = np.loadtxt(self.inputs.in_plots) # mpars is N_t x 6
83+
diff = mpars[:-1, :] - mpars[1:, :]
84+
diff[:, :3] *= self.inputs.radius
85+
fd_res = np.abs(diff).sum(axis=1)
86+
87+
self._results = {
88+
'out_file': op.abspath(self.inputs.out_file),
89+
'fd_average': float(fd_res.mean())
90+
}
91+
np.savetxt(self.inputs.out_file, fd_res)
92+
93+
94+
if self.inputs.save_plot:
95+
tr = None
96+
if isdefined(self.inputs.series_tr):
97+
tr = self.inputs.series_tr
98+
99+
if self.inputs.normalize and tr is None:
100+
IFLOG.warn('FD plot cannot be normalized if TR is not set')
101+
102+
self._results['out_figure'] = op.abspath(self.inputs.out_figure)
103+
fig = plot_confound(fd_res, self.inputs.figsize, 'FD', units='mm',
104+
series_tr=tr, normalize=self.inputs.normalize)
105+
fig.savefig(self._results['out_figure'], dpi=float(self.inputs.figdpi),
106+
format=self.inputs.out_figure[-3:],
107+
bbox_inches='tight')
108+
fig.clf()
109+
return runtime
110+
111+
def _list_outputs(self):
112+
return self._results
113+
114+
115+
def plot_confound(tseries, figsize, name, units=None,
116+
series_tr=None, normalize=False):
117+
"""
118+
A helper function to plot :abbr:`fMRI (functional MRI)` confounds.
119+
"""
120+
import matplotlib
121+
matplotlib.use('Agg')
122+
import matplotlib.pyplot as plt
123+
from matplotlib.gridspec import GridSpec
124+
from matplotlib.backends.backend_pdf import FigureCanvasPdf as FigureCanvas
125+
import seaborn as sns
126+
127+
fig = plt.Figure(figsize=figsize)
128+
FigureCanvas(fig)
129+
grid = GridSpec(1, 2, width_ratios=[3, 1], wspace=0.025)
130+
grid.update(hspace=1.0, right=0.95, left=0.1, bottom=0.2)
131+
132+
ax = fig.add_subplot(grid[0, :-1])
133+
if normalize and series_tr is not None:
134+
tseries /= series_tr
135+
136+
ax.plot(tseries)
137+
ax.set_xlim((0, len(tseries)))
138+
ylabel = name
139+
if units is not None:
140+
ylabel += (' speed [{}/s]' if normalize else ' [{}]').format(units)
141+
ax.set_ylabel(ylabel)
142+
143+
xlabel = 'Frame #'
144+
if series_tr is not None:
145+
xlabel = 'Frame # ({} sec TR)'.format(series_tr)
146+
ax.set_xlabel(xlabel)
147+
ylim = ax.get_ylim()
148+
149+
ax = fig.add_subplot(grid[0, -1])
150+
sns.distplot(tseries, vertical=True, ax=ax)
151+
ax.set_xlabel('Frames')
152+
ax.set_ylim(ylim)
153+
ax.set_yticklabels([])
154+
return fig

nipype/algorithms/misc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1157,6 +1157,10 @@ class SplitROIs(BaseInterface):
11571157
"""
11581158
Splits a 3D image in small chunks to enable parallel processing.
11591159
ROIs keep time series structure in 4D images.
1160+
1161+
Example
1162+
-------
1163+
11601164
>>> from nipype.algorithms import misc
11611165
>>> rois = misc.SplitROIs()
11621166
>>> rois.inputs.in_file = 'diffusion.nii'
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from ...testing import assert_equal
3+
from ..confounds import FramewiseDisplacement
4+
5+
6+
def test_FramewiseDisplacement_inputs():
7+
input_map = dict(figdpi=dict(usedefault=True,
8+
),
9+
figsize=dict(usedefault=True,
10+
),
11+
ignore_exception=dict(nohash=True,
12+
usedefault=True,
13+
),
14+
in_plots=dict(),
15+
normalize=dict(usedefault=True,
16+
),
17+
out_figure=dict(usedefault=True,
18+
),
19+
out_file=dict(usedefault=True,
20+
),
21+
radius=dict(usedefault=True,
22+
),
23+
save_plot=dict(usedefault=True,
24+
),
25+
series_tr=dict(),
26+
)
27+
inputs = FramewiseDisplacement.input_spec()
28+
29+
for key, metadata in list(input_map.items()):
30+
for metakey, value in list(metadata.items()):
31+
yield assert_equal, getattr(inputs.traits()[key], metakey), value
32+
33+
34+
def test_FramewiseDisplacement_outputs():
35+
output_map = dict(fd_average=dict(),
36+
out_figure=dict(),
37+
out_file=dict(),
38+
)
39+
outputs = FramewiseDisplacement.output_spec()
40+
41+
for key, metadata in list(output_map.items()):
42+
for metakey, value in list(metadata.items()):
43+
yield assert_equal, getattr(outputs.traits()[key], metakey), value
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
from nipype.testing import (assert_equal, example_data)
5+
from nipype.algorithms.confounds import FramewiseDisplacement
6+
import numpy as np
7+
from tempfile import mkdtemp
8+
from shutil import rmtree
9+
10+
def test_fd():
11+
tempdir = mkdtemp()
12+
ground_truth = np.loadtxt(example_data('fsl_motion_outliers_fd.txt'))
13+
fd = FramewiseDisplacement(in_plots=example_data('fsl_mcflirt_movpar.txt'),
14+
out_file=tempdir + '/fd.txt')
15+
res = fd.run()
16+
yield assert_equal, np.allclose(ground_truth, np.loadtxt(res.outputs.out_file)), True
17+
yield assert_equal, np.abs(ground_truth.mean() - res.outputs.fd_average) < 1e-4, True
18+
rmtree(tempdir)

nipype/pipeline/plugins/multiproc.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from copy import deepcopy
1818
import numpy as np
1919

20-
from ... import logging
20+
from ... import logging, config
2121
from ...utils.misc import str2bool
2222
from ..engine import MapNode
2323
from ..plugins import semaphore_singleton
@@ -223,15 +223,18 @@ def _send_procs_to_workers(self, updatehash=False, graph=None):
223223
key=lambda item: (self.procs[item]._interface.estimated_memory_gb,
224224
self.procs[item]._interface.num_threads))
225225

226-
logger.debug('Free memory (GB): %d, Free processors: %d',
227-
free_memory_gb, free_processors)
226+
if str2bool(config.get('execution', 'profile_runtime')):
227+
logger.debug('Free memory (GB): %d, Free processors: %d',
228+
free_memory_gb, free_processors)
228229

229230
# While have enough memory and processors for first job
230231
# Submit first job on the list
231232
for jobid in jobids:
232-
logger.debug('Next Job: %d, memory (GB): %d, threads: %d' \
233-
% (jobid, self.procs[jobid]._interface.estimated_memory_gb,
234-
self.procs[jobid]._interface.num_threads))
233+
if str2bool(config.get('execution', 'profile_runtime')):
234+
logger.debug('Next Job: %d, memory (GB): %d, threads: %d' \
235+
% (jobid,
236+
self.procs[jobid]._interface.estimated_memory_gb,
237+
self.procs[jobid]._interface.num_threads))
235238

236239
if self.procs[jobid]._interface.estimated_memory_gb <= free_memory_gb and \
237240
self.procs[jobid]._interface.num_threads <= free_processors:
@@ -307,5 +310,3 @@ def _send_procs_to_workers(self, updatehash=False, graph=None):
307310
self.pending_tasks.insert(0, (tid, jobid))
308311
else:
309312
break
310-
311-
logger.debug('No jobs waiting to execute')

0 commit comments

Comments
 (0)