Skip to content

Commit e83051a

Browse files
committed
enh: Add a minimal infrastructure to run sMRIPrep (and FreeSurfer)
Ref. #2
1 parent dc9b5d6 commit e83051a

File tree

5 files changed

+276
-213
lines changed

5 files changed

+276
-213
lines changed

.circleci/config.yml

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ jobs:
7373
-t nipreps/dmriprep_devel \
7474
-f Dockerfile_devel .
7575
fi
76+
77+
- run:
78+
name: Docker save
79+
no_output_timeout: 40m
80+
command: |
81+
mkdir -p /tmp/cache
82+
docker save ubuntu:xenial-20161213 nipreps/dmriprep:latest \
83+
| pigz -3 > /tmp/cache/docker.tar.gz
84+
- save_cache:
85+
key: docker-v0-{{ .Branch }}-{{ .Revision }}-{{ epoch }}
86+
paths:
87+
- /tmp/cache/docker.tar.gz
88+
7689
- run:
7790
name: Smoke test Docker image
7891
command: |
@@ -85,22 +98,11 @@ jobs:
8598
echo "VERSION: \"$THISVERSION\""
8699
echo "DOCKERVERSION: \"${DOCKERVERSION}\""
87100
test "$DOCKERVERSION" = "$THISVERSION"
88-
- run:
89-
name: Docker save
90-
no_output_timeout: 40m
91-
command: |
92-
mkdir -p /tmp/cache
93-
docker save ubuntu:xenial-20161213 nipreps/dmriprep:latest \
94-
| pigz -3 > /tmp/cache/docker.tar.gz
95101
96102
- persist_to_workspace:
97103
root: /tmp
98104
paths:
99105
- src/dmriprep
100-
- save_cache:
101-
key: docker-v0-{{ .Branch }}-{{ .Revision }}-{{ epoch }}
102-
paths:
103-
- /tmp/cache/docker.tar.gz
104106

105107
get_data:
106108
machine:

dmriprep/cli/run.py

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,7 @@ def get_parser():
6464
parser.add_argument('--version', action='version', version=verstr)
6565

6666
g_bids = parser.add_argument_group('Options for filtering BIDS queries')
67-
g_bids.add_argument('--skip_bids_validation', '--skip-bids-validation', action='store_true',
68-
default=False,
67+
g_bids.add_argument('--skip-bids-validation', action='store_true', default=False,
6968
help='assume the input dataset is BIDS compliant and skip the validation')
7069
g_bids.add_argument('--participant_label', '--participant-label', action='store', nargs='+',
7170
help='a space delimited list of participant identifiers or a single '
@@ -187,7 +186,7 @@ def get_parser():
187186
'the dMRIPrep developers. This information helps to '
188187
'improve dMRIPrep and provides an indicator of real '
189188
'world usage crucial for obtaining funding.')
190-
g_other.add_argument('--sloppy', action='store_true', default=False,
189+
g_other.add_argument('--sloppy', action='store_true', default=False, dest='debug',
191190
help='Use low-quality tools for speed - TESTING ONLY')
192191

193192
latest = check_latest()
@@ -548,36 +547,21 @@ def build_workflow(opts, retval):
548547

549548
retval['workflow'] = init_dmriprep_wf(
550549
anat_only=opts.anat_only,
551-
aroma_melodic_dim=opts.aroma_melodic_dimensionality,
552-
bold2t1w_dof=opts.bold2t1w_dof,
553-
cifti_output=opts.cifti_output,
554-
debug=opts.sloppy,
555-
dummy_scans=opts.dummy_scans,
556-
echo_idx=opts.echo_idx,
557-
err_on_aroma_warn=opts.error_on_aroma_warnings,
558-
fmap_bspline=opts.fmap_bspline,
559-
fmap_demean=opts.fmap_no_demean,
550+
debug=opts.debug,
560551
force_syn=opts.force_syn,
561552
freesurfer=opts.run_reconall,
562553
hires=opts.hires,
563554
ignore=opts.ignore,
564555
layout=layout,
565556
longitudinal=opts.longitudinal,
566557
low_mem=opts.low_mem,
567-
medial_surface_nan=opts.medial_surface_nan,
568558
omp_nthreads=omp_nthreads,
569559
output_dir=str(output_dir),
570560
output_spaces=output_spaces,
571561
run_uuid=run_uuid,
572-
regressors_all_comps=opts.return_all_components,
573-
regressors_fd_th=opts.fd_spike_threshold,
574-
regressors_dvars_th=opts.dvars_spike_threshold,
575562
skull_strip_fixed_seed=opts.skull_strip_fixed_seed,
576563
skull_strip_template=opts.skull_strip_template,
577564
subject_list=subject_list,
578-
t2s_coreg=opts.t2s_coreg,
579-
task_id=opts.task_id,
580-
use_aroma=opts.use_aroma,
581565
use_bbr=opts.use_bbr,
582566
use_syn=opts.use_syn_sdc,
583567
work_dir=str(work_dir),
@@ -608,75 +592,19 @@ def build_workflow(opts, retval):
608592

609593

610594
def parse_spaces(opts):
611-
"""Ensures the spaces are correctly parsed"""
595+
"""Ensure the spaces are correctly parsed."""
612596
from sys import stderr
613597
from collections import OrderedDict
614-
from templateflow.api import templates as get_templates
615598
# Set the default template to 'MNI152NLin2009cAsym'
616599
output_spaces = opts.output_spaces or OrderedDict([('MNI152NLin2009cAsym', {})])
617600

618-
if opts.template:
619-
print("""\
620-
The ``--template`` option has been deprecated in version 1.4.0. Your selected template \
621-
"%s" will be inserted at the front of the ``--output-spaces`` argument list. Please update \
622-
your scripts to use ``--output-spaces``.""" % opts.template, file=stderr)
623-
deprecated_tpl_arg = [(opts.template, {})]
624-
# If output_spaces is not set, just replate the default - append otherwise
625-
if opts.output_spaces is not None:
626-
deprecated_tpl_arg += list(output_spaces.items())
627-
output_spaces = OrderedDict(deprecated_tpl_arg)
628-
629-
if opts.output_space:
630-
print("""\
631-
The ``--output_space`` option has been deprecated in version 1.4.0. Your selection of spaces \
632-
"%s" will be inserted at the front of the ``--output-spaces`` argument list. Please update \
633-
your scripts to use ``--output-spaces``.""" % ', '.join(opts.output_space), file=stderr)
634-
missing = set(opts.output_space)
635-
if 'template' in missing:
636-
missing.remove('template')
637-
if not opts.template:
638-
missing.add('MNI152NLin2009cAsym')
639-
missing = missing - set(output_spaces.keys())
640-
output_spaces.update({tpl: {} for tpl in missing})
641-
642601
FS_SPACES = set(['fsnative', 'fsaverage', 'fsaverage6', 'fsaverage5'])
643602
if opts.run_reconall and not list(FS_SPACES.intersection(output_spaces.keys())):
644603
print("""\
645604
Although ``--fs-no-reconall`` was not set (i.e., FreeSurfer is to be run), no FreeSurfer \
646605
output space (valid values are: %s) was selected. Adding default "fsaverage5" to the \
647606
list of output spaces.""" % ', '.join(FS_SPACES), file=stderr)
648607
output_spaces['fsaverage5'] = {}
649-
650-
# Validity of some inputs
651-
# ERROR check if use_aroma was specified, but the correct template was not
652-
if opts.use_aroma and 'MNI152NLin6Asym' not in output_spaces:
653-
output_spaces['MNI152NLin6Asym'] = {'res': 2}
654-
print("""\
655-
Option "--use-aroma" requires functional images to be resampled to MNI152NLin6Asym space. \
656-
The argument "MNI152NLin6Asym:res-2" has been automatically added to the list of output spaces \
657-
(option ``--output-spaces``).""", file=stderr)
658-
659-
if opts.cifti_output and 'MNI152NLin2009cAsym' not in output_spaces:
660-
if 'MNI152NLin2009cAsym' not in output_spaces:
661-
output_spaces['MNI152NLin2009cAsym'] = {'res': 2}
662-
print("""Option ``--cifti-output`` requires functional images to be resampled to \
663-
``MNI152NLin2009cAsym`` space. Such template identifier has been automatically added to the \
664-
list of output spaces (option "--output-space").""", file=stderr)
665-
if not [s for s in output_spaces if s in ('fsaverage5', 'fsaverage6')]:
666-
output_spaces['fsaverage5'] = {}
667-
print("""Option ``--cifti-output`` requires functional images to be resampled to \
668-
``fsaverage`` space. The argument ``fsaverage:den-10k`` (a.k.a ``fsaverage5``) has been \
669-
automatically added to the list of output spaces (option ``--output-space``).""", file=stderr)
670-
671-
if opts.template_resampling_grid is not None:
672-
print("""Option ``--template-resampling-grid`` is deprecated, please specify \
673-
resampling grid options as modifiers to templates listed in ``--output-spaces``. \
674-
The configuration value will be applied to ALL output standard spaces.""")
675-
if opts.template_resampling_grid != 'native':
676-
for key in output_spaces.keys():
677-
if key in get_templates():
678-
output_spaces[key]['res'] = opts.template_resampling_grid[0]
679-
680608
return output_spaces
681609

682610

dmriprep/interfaces/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Custom Nipype interfaces for dMRIPrep."""
4+
from niworkflows.interfaces.bids import DerivativesDataSink as _DDS
5+
6+
7+
class DerivativesDataSink(_DDS):
8+
"""A patched DataSink."""
9+
10+
out_path_base = 'dmriprep'

dmriprep/interfaces/reports.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Interfaces to generate reportlets."""
4+
5+
import os
6+
import time
7+
8+
from collections import Counter
9+
from nipype.interfaces.base import (
10+
traits, TraitedSpec, BaseInterfaceInputSpec,
11+
File, Directory, InputMultiObject, Str, isdefined,
12+
SimpleInterface)
13+
from nipype.interfaces import freesurfer as fs
14+
from niworkflows.utils.bids import BIDS_NAME
15+
16+
17+
SUBJECT_TEMPLATE = """\
18+
\t<ul class="elem-desc">
19+
\t\t<li>Subject ID: {subject_id}</li>
20+
\t\t<li>Structural images: {n_t1s:d} T1-weighted {t2w}</li>
21+
\t\t<li>Diffusion Weighted Images: {n_dwi:d}</li>
22+
{tasks}
23+
\t\t<li>Standard output spaces: {std_spaces}</li>
24+
\t\t<li>Non-standard output spaces: {nstd_spaces}</li>
25+
\t\t<li>FreeSurfer reconstruction: {freesurfer_status}</li>
26+
\t</ul>
27+
"""
28+
29+
DWI_TEMPLATE = """\t\t<h3 class="elem-title">Summary</h3>
30+
\t\t<ul class="elem-desc">
31+
\t\t\t<li>Susceptibility distortion correction: {sdc}</li>
32+
\t\t\t<li>Registration: {registration}</li>
33+
\t\t</ul>
34+
"""
35+
36+
ABOUT_TEMPLATE = """\t<ul>
37+
\t\t<li>dMRIPrep version: {version}</li>
38+
\t\t<li>dMRIPrep command: <code>{command}</code></li>
39+
\t\t<li>Date preprocessed: {date}</li>
40+
\t</ul>
41+
</div>
42+
"""
43+
44+
45+
class SummaryOutputSpec(TraitedSpec):
46+
out_report = File(exists=True, desc='HTML segment containing summary')
47+
48+
49+
class SummaryInterface(SimpleInterface):
50+
output_spec = SummaryOutputSpec
51+
52+
def _run_interface(self, runtime):
53+
segment = self._generate_segment()
54+
fname = os.path.join(runtime.cwd, 'report.html')
55+
with open(fname, 'w') as fobj:
56+
fobj.write(segment)
57+
self._results['out_report'] = fname
58+
return runtime
59+
60+
def _generate_segment(self):
61+
raise NotImplementedError
62+
63+
64+
class SubjectSummaryInputSpec(BaseInterfaceInputSpec):
65+
t1w = InputMultiObject(File(exists=True), desc='T1w structural images')
66+
t2w = InputMultiObject(File(exists=True), desc='T2w structural images')
67+
subjects_dir = Directory(desc='FreeSurfer subjects directory')
68+
subject_id = Str(desc='Subject ID')
69+
dwi = InputMultiObject(traits.Either(
70+
File(exists=True), traits.List(File(exists=True))),
71+
desc='DWI files')
72+
std_spaces = traits.List(Str, desc='list of standard spaces')
73+
nstd_spaces = traits.List(Str, desc='list of non-standard spaces')
74+
75+
76+
class SubjectSummaryOutputSpec(SummaryOutputSpec):
77+
# This exists to ensure that the summary is run prior to the first ReconAll
78+
# call, allowing a determination whether there is a pre-existing directory
79+
subject_id = Str(desc='FreeSurfer subject ID')
80+
81+
82+
class SubjectSummary(SummaryInterface):
83+
input_spec = SubjectSummaryInputSpec
84+
output_spec = SubjectSummaryOutputSpec
85+
86+
def _run_interface(self, runtime):
87+
if isdefined(self.inputs.subject_id):
88+
self._results['subject_id'] = self.inputs.subject_id
89+
return super(SubjectSummary, self)._run_interface(runtime)
90+
91+
def _generate_segment(self):
92+
if not isdefined(self.inputs.subjects_dir):
93+
freesurfer_status = 'Not run'
94+
else:
95+
recon = fs.ReconAll(subjects_dir=self.inputs.subjects_dir,
96+
subject_id=self.inputs.subject_id,
97+
T1_files=self.inputs.t1w,
98+
flags='-noskullstrip')
99+
if recon.cmdline.startswith('echo'):
100+
freesurfer_status = 'Pre-existing directory'
101+
else:
102+
freesurfer_status = 'Run by dMRIPrep'
103+
104+
t2w_seg = ''
105+
if self.inputs.t2w:
106+
t2w_seg = '(+ {:d} T2-weighted)'.format(len(self.inputs.t2w))
107+
108+
# Add list of tasks with number of runs
109+
dwi_files = self.inputs.dwi if isdefined(self.inputs.dwi) else []
110+
dwi_files = [s[0] if isinstance(s, list) else s for s in dwi_files]
111+
112+
counts = Counter(BIDS_NAME.search(series).groupdict()['task_id'][5:]
113+
for series in dwi_files)
114+
115+
tasks = ''
116+
if counts:
117+
header = '\t\t<ul class="elem-desc">'
118+
footer = '\t\t</ul>'
119+
lines = ['\t\t\t<li>Task: {task_id} ({n_runs:d} run{s})</li>'.format(
120+
task_id=task_id, n_runs=n_runs, s='' if n_runs == 1 else 's')
121+
for task_id, n_runs in sorted(counts.items())]
122+
tasks = '\n'.join([header] + lines + [footer])
123+
124+
return SUBJECT_TEMPLATE.format(
125+
subject_id=self.inputs.subject_id,
126+
n_t1s=len(self.inputs.t1w),
127+
t2w=t2w_seg,
128+
n_dwi=len(dwi_files),
129+
tasks=tasks,
130+
std_spaces=', '.join(self.inputs.std_spaces),
131+
nstd_spaces=', '.join(self.inputs.nstd_spaces),
132+
freesurfer_status=freesurfer_status)
133+
134+
135+
class AboutSummaryInputSpec(BaseInterfaceInputSpec):
136+
version = Str(desc='dMRIPrep version')
137+
command = Str(desc='dMRIPrep command')
138+
# Date not included - update timestamp only if version or command changes
139+
140+
141+
class AboutSummary(SummaryInterface):
142+
input_spec = AboutSummaryInputSpec
143+
144+
def _generate_segment(self):
145+
return ABOUT_TEMPLATE.format(version=self.inputs.version,
146+
command=self.inputs.command,
147+
date=time.strftime("%Y-%m-%d %H:%M:%S %z"))
148+
149+
150+
class DiffusionSummaryInputSpec(BaseInterfaceInputSpec):
151+
distortion_correction = traits.Str(desc='Susceptibility distortion correction method',
152+
mandatory=True)
153+
pe_direction = traits.Enum(None, 'i', 'i-', 'j', 'j-', mandatory=True,
154+
desc='Phase-encoding direction detected')
155+
registration = traits.Enum('FSL', 'FreeSurfer', mandatory=True,
156+
desc='Diffusion/anatomical registration method')
157+
fallback = traits.Bool(desc='Boundary-based registration rejected')
158+
registration_dof = traits.Enum(6, 9, 12, desc='Registration degrees of freedom',
159+
mandatory=True)
160+
161+
162+
class DiffusionSummary(SummaryInterface):
163+
input_spec = DiffusionSummaryInputSpec
164+
165+
def _generate_segment(self):
166+
dof = self.inputs.registration_dof
167+
reg = {
168+
'FSL': [
169+
'FSL <code>flirt</code> with boundary-based registration'
170+
' (BBR) metric - %d dof' % dof,
171+
'FSL <code>flirt</code> rigid registration - 6 dof'],
172+
'FreeSurfer': [
173+
'FreeSurfer <code>bbregister</code> '
174+
'(boundary-based registration, BBR) - %d dof' % dof,
175+
'FreeSurfer <code>mri_coreg</code> - %d dof' % dof],
176+
}[self.inputs.registration][self.inputs.fallback]
177+
if self.inputs.pe_direction is None:
178+
pedir = 'MISSING - Assuming Anterior-Posterior'
179+
else:
180+
pedir = {'i': 'Left-Right', 'j': 'Anterior-Posterior'}[self.inputs.pe_direction[0]]
181+
182+
return DWI_TEMPLATE.format(
183+
pedir=pedir, sdc=self.inputs.distortion_correction, registration=reg)

0 commit comments

Comments
 (0)