Skip to content

Commit 47fe0e9

Browse files
author
dPys
committed
Merge remote-tracking branch 'upstream/master'
2 parents e4e8f82 + 127a65d commit 47fe0e9

File tree

17 files changed

+716
-141
lines changed

17 files changed

+716
-141
lines changed

.circleci/config.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,24 @@ jobs:
251251
key: THP002-anat-v00-{{ .Branch }}-{{ .Revision }}-{{ epoch }}
252252
paths:
253253
- /tmp/THP002/work
254+
- run:
255+
name: Run full diffusion workflow on THP002
256+
no_output_timeout: 2h
257+
command: |
258+
mkdir -p /tmp/THP002/work /tmp/THP002/derivatives
259+
sudo setfacl -d -m group:$(id -gn):rwx /tmp/THP002/derivatives && \
260+
sudo setfacl -m group:$(id -gn):rwx /tmp/THP002/derivatives
261+
sudo setfacl -d -m group:$(id -gn):rwx /tmp/THP002/work && \
262+
sudo setfacl -m group:$(id -gn):rwx /tmp/THP002/work
263+
docker run -e FS_LICENSE=$FS_LICENSE --rm \
264+
-v /tmp/data/THP002:/data \
265+
-v /tmp/THP002/derivatives:/out \
266+
-v /tmp/fslicense/license.txt:/tmp/fslicense/license.txt:ro \
267+
-v /tmp/config/nipype.cfg:/home/dmriprep/.nipype/nipype.cfg \
268+
-v /tmp/THP002/work:/work \
269+
--user $(id -u):$(id -g) \
270+
nipreps/dmriprep:latest /data /out participant -vv --sloppy \
271+
--notrack --skip-bids-validation -w /work --omp-nthreads 2 --nprocs 2
254272
- store_artifacts:
255273
path: /tmp/THP002/derivatives/dmriprep
256274
- run:

CHANGES.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
0.2.2 (January 15, 2020)
2+
========================
3+
A release to show the deployment process on the Sprint.
4+
5+
* ENH: b0 reference and skullstrip workflow (#50)
6+
* FIX: Version on docker target of ``Makefile`` (#54)
7+
* FIX/ENH: Remove sentry tracking and add popylar tracking. (#51)
8+
* MAINT: Some small changes to the Dockerfile. (#53)
9+
* ENH: Set up customized report specification (#34)
10+
* MAINT: Use a local docker registry instead of load/save (#46)
11+
12+
113
0.2.1 (December 12, 2019)
214
=========================
315
A bugfix release to test that versioned documentation is working.

Dockerfile

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ RUN python -c "from matplotlib import font_manager" && \
154154
sed -i 's/\(backend *: \).*$/\1Agg/g' $( python -c "import matplotlib; print(matplotlib.matplotlib_fname())" )
155155

156156
# Precaching atlases
157-
COPY setup.cfg fmriprep-setup.cfg
158-
RUN pip install --no-cache-dir "$( grep templateflow fmriprep-setup.cfg | xargs )" && \
157+
COPY setup.cfg dmriprep-setup.cfg
158+
RUN pip install --no-cache-dir "$( grep templateflow dmriprep-setup.cfg | xargs )" && \
159159
python -c "from templateflow import api as tfapi; \
160160
tfapi.get('MNI152NLin6Asym', atlas=None, resolution=[1, 2], extension=['.nii', '.nii.gz']); \
161161
tfapi.get('MNI152NLin2009cAsym', atlas=None, extension=['.nii', '.nii.gz']); \
@@ -169,13 +169,12 @@ ARG VERSION
169169
# Force static versioning within container
170170
RUN echo "${VERSION}" > /src/dmriprep/dmriprep/VERSION && \
171171
echo "include dmriprep/VERSION" >> /src/dmriprep/MANIFEST.in && \
172-
pip install --no-cache-dir "/src/dmriprep[all]"
172+
cd /src/dmriprep && \
173+
pip install --no-cache-dir .[all]
173174

174175
RUN find $HOME -type d -exec chmod go=u {} + && \
175176
find $HOME -type f -exec chmod go=u {} +
176177

177-
ENV IS_DOCKER_8395080871=1
178-
179178
RUN ldconfig
180179
WORKDIR /tmp/
181180
ENTRYPOINT ["/usr/local/miniconda/bin/dmriprep"]

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ dist: clean ## builds source and wheel package
8787
install: clean ## install the package to the active Python's site-packages
8888
python setup.py install
8989

90-
docker: docker
90+
docker:
9191
docker build --rm -t nipreps/dmriprep:latest \
9292
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
9393
--build-arg VCS_REF=`git rev-parse --short HEAD` \
94-
--build-arg VERSION=$( python get_version.py ) .
94+
--build-arg VERSION=`python get_version.py` .

dmriprep/__about__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@
1414
DOWNLOAD_URL = (
1515
'https://github.com/nipreps/{name}/archive/{ver}.tar.gz'.format(
1616
name=__packagename__, ver=__version__))
17+
18+
__ga_id__ = "UA-156165436-1"

dmriprep/cli/run.py

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def main():
214214
from nipype import logging as nlogging
215215
from multiprocessing import set_start_method, Process, Manager
216216
from ..utils.bids import write_derivative_description, validate_input_dir
217+
from ..__about__ import __ga_id__
217218
set_start_method('forkserver')
218219
warnings.showwarning = _warn_redirect
219220
opts = get_parser().parse_args()
@@ -229,11 +230,9 @@ def main():
229230
if os.getenv('DOCKER_VERSION_8395080871'):
230231
exec_env = 'dmriprep-docker'
231232

232-
sentry_sdk = None
233233
if not opts.notrack:
234-
import sentry_sdk
235-
from ..utils.sentry import sentry_setup
236-
sentry_setup(opts, exec_env)
234+
import popylar
235+
popylar.track_event(__ga_id__, 'run', 'cli_run')
237236

238237
# Validate inputs
239238
if not opts.skip_bids_validation:
@@ -301,27 +300,14 @@ def main():
301300
# Clean up master process before running workflow, which may create forks
302301
gc.collect()
303302

304-
# Sentry tracking
305-
if not opts.notrack:
306-
from ..utils.sentry import start_ping
307-
start_ping(run_uuid, len(subject_list))
308-
309303
errno = 1 # Default is error exit unless otherwise set
310304
try:
311305
dmriprep_wf.run(**plugin_settings)
312306
except Exception as e:
313307
if not opts.notrack:
314-
from ..utils.sentry import process_crashfile
315-
crashfolders = [output_dir / 'dmriprep' / 'sub-{}'.format(s) / 'log' / run_uuid
316-
for s in subject_list]
317-
for crashfolder in crashfolders:
318-
for crashfile in crashfolder.glob('crash*.*'):
319-
process_crashfile(crashfile)
320-
321-
if "Workflow did not execute cleanly" not in str(e):
322-
sentry_sdk.capture_exception(e)
308+
popylar.track_event(__ga_id__, 'run', 'cli_error')
323309
logger.critical('dMRIPrep failed: %s', e)
324-
raise
310+
raise e
325311
else:
326312
if opts.run_reconall:
327313
from templateflow import api
@@ -334,8 +320,8 @@ def main():
334320
errno = 0
335321
logger.log(25, 'dMRIPrep finished without errors')
336322
if not opts.notrack:
337-
sentry_sdk.capture_message('dMRIPrep finished without errors',
338-
level='info')
323+
popylar.track_event(__ga_id__, 'run', 'cli_finished')
324+
339325
finally:
340326
from niworkflows.reports import generate_reports
341327
from subprocess import check_call, CalledProcessError, TimeoutExpired
@@ -389,10 +375,6 @@ def main():
389375
packagename='dmriprep')
390376
write_derivative_description(bids_dir, output_dir / 'dmriprep')
391377

392-
if failed_reports and not opts.notrack:
393-
sentry_sdk.capture_message(
394-
'Report generation failed for %d subjects' % failed_reports,
395-
level='error')
396378
sys.exit(int((errno + failed_reports) > 0))
397379

398380

dmriprep/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
"""Settings."""
44

5+
DEFAULT_MEMORY_MIN_GB = 0.01
56
NONSTANDARD_REFERENCES = ['anat', 'T1w', 'dwi', 'fsnative']

dmriprep/config/reports-spec.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ sections:
2626
caption: Surfaces (white and pial) reconstructed with FreeSurfer (<code>recon-all</code>)
2727
overlaid on the participant's T1w template.
2828
subtitle: Surface reconstruction
29+
- name: Diffusion
30+
ordering: session,acquisition,run
31+
reportlets:
32+
- bids: {datatype: dwi, desc: validation, suffix: dwi}
33+
- bids: {datatype: dwi, desc: brain, suffix: mask}
34+
caption: Average b=0 that serves for reference in early preprocessing steps.
35+
descriptions: The reference b=0 is obtained as the voxel-wise median across
36+
all b=0 found in the dataset, after accounting for signal drift.
37+
The red contour shows the brain mask calculated using this reference b=0.
38+
subtitle: Reference b=0 and brain mask
2939
- name: About
3040
reportlets:
3141
- bids: {datatype: anat, desc: about, suffix: T1w}

dmriprep/data/tests/dwi_mask.nii.gz

Whitespace-only changes.

dmriprep/interfaces/images.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Image tools interfaces."""
2+
import numpy as np
3+
import nibabel as nb
4+
from nipype.utils.filemanip import fname_presuffix
5+
from nipype import logging
6+
from nipype.interfaces.base import (
7+
traits, TraitedSpec, BaseInterfaceInputSpec, SimpleInterface, File
8+
)
9+
10+
LOGGER = logging.getLogger('nipype.interface')
11+
12+
13+
class _ExtractB0InputSpec(BaseInterfaceInputSpec):
14+
in_file = File(exists=True, mandatory=True, desc='dwi file')
15+
b0_ixs = traits.List(traits.Int, mandatory=True,
16+
desc='Index of b0s')
17+
18+
19+
class _ExtractB0OutputSpec(TraitedSpec):
20+
out_file = File(exists=True, desc='b0 file')
21+
22+
23+
class ExtractB0(SimpleInterface):
24+
"""
25+
Extract all b=0 volumes from a dwi series.
26+
27+
Example
28+
-------
29+
>>> os.chdir(tmpdir)
30+
>>> extract_b0 = ExtractB0()
31+
>>> extract_b0.inputs.in_file = str(data_dir / 'dwi.nii.gz')
32+
>>> extract_b0.inputs.b0_ixs = [0, 1, 2]
33+
>>> res = extract_b0.run() # doctest: +SKIP
34+
35+
"""
36+
37+
input_spec = _ExtractB0InputSpec
38+
output_spec = _ExtractB0OutputSpec
39+
40+
def _run_interface(self, runtime):
41+
self._results['out_file'] = extract_b0(
42+
self.inputs.in_file,
43+
self.inputs.b0_ixs,
44+
newpath=runtime.cwd)
45+
return runtime
46+
47+
48+
def extract_b0(in_file, b0_ixs, newpath=None):
49+
"""Extract the *b0* volumes from a DWI dataset."""
50+
out_file = fname_presuffix(
51+
in_file, suffix='_b0', newpath=newpath)
52+
53+
img = nb.load(in_file)
54+
data = img.get_fdata(dtype='float32')
55+
56+
b0 = data[..., b0_ixs]
57+
58+
hdr = img.header.copy()
59+
hdr.set_data_shape(b0.shape)
60+
hdr.set_xyzt_units('mm')
61+
hdr.set_data_dtype(np.float32)
62+
nb.Nifti1Image(b0, img.affine, hdr).to_filename(out_file)
63+
return out_file
64+
65+
66+
class _RescaleB0InputSpec(BaseInterfaceInputSpec):
67+
in_file = File(exists=True, mandatory=True, desc='b0s file')
68+
mask_file = File(exists=True, mandatory=True, desc='mask file')
69+
70+
71+
class _RescaleB0OutputSpec(TraitedSpec):
72+
out_ref = File(exists=True, desc='One average b0 file')
73+
out_b0s = File(exists=True, desc='series of rescaled b0 volumes')
74+
75+
76+
class RescaleB0(SimpleInterface):
77+
"""
78+
Rescale the b0 volumes to deal with average signal decay over time.
79+
80+
Example
81+
-------
82+
>>> os.chdir(tmpdir)
83+
>>> rescale_b0 = RescaleB0()
84+
>>> rescale_b0.inputs.in_file = str(data_dir / 'dwi.nii.gz')
85+
>>> rescale_b0.inputs.mask_file = str(data_dir / 'dwi_mask.nii.gz')
86+
>>> res = rescale_b0.run() # doctest: +SKIP
87+
88+
"""
89+
90+
input_spec = _RescaleB0InputSpec
91+
output_spec = _RescaleB0OutputSpec
92+
93+
def _run_interface(self, runtime):
94+
self._results['out_b0s'] = rescale_b0(
95+
self.inputs.in_file,
96+
self.inputs.mask_file,
97+
newpath=runtime.cwd
98+
)
99+
self._results['out_ref'] = median(
100+
self._results['out_b0s'],
101+
newpath=runtime.cwd
102+
)
103+
return runtime
104+
105+
106+
def rescale_b0(in_file, mask_file, newpath=None):
107+
"""Rescale the input volumes using the median signal intensity."""
108+
out_file = fname_presuffix(
109+
in_file, suffix='_rescaled_b0', newpath=newpath)
110+
111+
img = nb.load(in_file)
112+
if img.dataobj.ndim == 3:
113+
return in_file
114+
115+
data = img.get_fdata(dtype='float32')
116+
mask_img = nb.load(mask_file)
117+
mask_data = mask_img.get_fdata(dtype='float32')
118+
119+
median_signal = np.median(data[mask_data > 0, ...], axis=0)
120+
rescaled_data = 1000 * data / median_signal
121+
hdr = img.header.copy()
122+
nb.Nifti1Image(rescaled_data, img.affine, hdr).to_filename(out_file)
123+
return out_file
124+
125+
126+
def median(in_file, newpath=None):
127+
"""Average a 4D dataset across the last dimension using median."""
128+
out_file = fname_presuffix(
129+
in_file, suffix='_b0ref', newpath=newpath)
130+
131+
img = nb.load(in_file)
132+
if img.dataobj.ndim == 3:
133+
return in_file
134+
if img.shape[-1] == 1:
135+
nb.squeeze_image(img).to_filename(out_file)
136+
return out_file
137+
138+
median_data = np.median(img.get_fdata(dtype='float32'),
139+
axis=-1)
140+
141+
hdr = img.header.copy()
142+
hdr.set_xyzt_units('mm')
143+
hdr.set_data_dtype(np.float32)
144+
nb.Nifti1Image(median_data, img.affine, hdr).to_filename(out_file)
145+
return out_file

0 commit comments

Comments
 (0)