Skip to content

ENH: BIDS Data Grabber #2174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Sep 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ before_install:
conda install python=${TRAVIS_PYTHON_VERSION} &&
conda config --add channels conda-forge &&
conda install -y nipype icu &&
rm -r ${CONDA_HOME}/lib/python${TRAVIS_PYTHON_VERSION}/site-packages/nipype*; }
rm -r ${CONDA_HOME}/lib/python${TRAVIS_PYTHON_VERSION}/site-packages/nipype*;
pushd $HOME;
git clone https://github.com/INCF/pybids.git;
cd pybids;
pip install -e .;
popd; }
# Add install of vtk and mayavi to test mesh (disabled): conda install -y vtk mayavi
- travis_retry apt_inst
- travis_retry conda_inst
Expand Down
5 changes: 5 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,11 @@
"affiliation": "Vrije Universiteit, Amsterdam",
"name": "Gilles de Hollander",
"orcid": "0000-0003-1988-5091"
},
{
"affiliation": "University of Texas at Austin",
"name": "De La Vega, Alejandro",
"orcid": "0000-0001-9062-3778"
}
],
"keywords": [
Expand Down
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt && \
rm -rf ~/.cache/pip

RUN git clone https://github.com/INCF/pybids.git && \
cd pybids && python setup.py develop

# Installing nipype
COPY . /src/nipype
RUN cd /src/nipype && \
Expand Down
1 change: 0 additions & 1 deletion docker/base.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,3 @@ ENV MATLABCMD="/opt/mcr/v85/toolbox/matlab" \
FORCE_SPMMCR=1

WORKDIR /work

1 change: 1 addition & 0 deletions nipype/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def get_nipype_gitversion():
'profiler': ['psutil'],
'duecredit': ['duecredit'],
'xvfbwrapper': ['xvfbwrapper'],
'pybids' : ['pybids']
# 'mesh': ['mayavi'] # Enable when it works
}

Expand Down
169 changes: 169 additions & 0 deletions nipype/interfaces/bids_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
# vi: set ft=python sts=4 ts=4 sw=4 et:
""" Set of interfaces that allow interaction with BIDS data. Currently
available interfaces are:

BIDSDataGrabber: Query data from BIDS dataset using pybids grabbids.

Change directory to provide relative paths for doctests
>>> import os
>>> import bids
>>> filepath = os.path.realpath(os.path.dirname(bids.__file__))
>>> datadir = os.path.realpath(os.path.join(filepath, 'grabbids/tests/data/'))
>>> os.chdir(datadir)

"""
from os.path import join, dirname
from .. import logging
from .base import (traits,
DynamicTraitedSpec,
Directory,
BaseInterface,
isdefined,
Str,
Undefined)

try:
from bids import grabbids as gb
import json
except ImportError:
have_pybids = False
else:
have_pybids = True

LOGGER = logging.getLogger('workflows')

class BIDSDataGrabberInputSpec(DynamicTraitedSpec):
base_dir = Directory(exists=True,
desc='Path to BIDS Directory.',
mandatory=True)
output_query = traits.Dict(key_trait=Str,
value_trait=traits.Dict,
desc='Queries for outfield outputs')
raise_on_empty = traits.Bool(True, usedefault=True,
desc='Generate exception if list is empty '
'for a given field')
return_type = traits.Enum('file', 'namedtuple', usedefault=True)


class BIDSDataGrabber(BaseInterface):

""" BIDS datagrabber module that wraps around pybids to allow arbitrary
querying of BIDS datasets.

Examples
--------

>>> from nipype.interfaces.bids_utils import BIDSDataGrabber
>>> from os.path import basename

By default, the BIDSDataGrabber fetches anatomical and functional images
from a project, and makes BIDS entities (e.g. subject) available for
filtering outputs.

>>> bg = BIDSDataGrabber()
>>> bg.inputs.base_dir = 'ds005/'
>>> bg.inputs.subject = '01'
>>> results = bg.run()
>>> basename(results.outputs.anat[0]) # doctest: +ALLOW_UNICODE
'sub-01_T1w.nii.gz'

>>> basename(results.outputs.func[0]) # doctest: +ALLOW_UNICODE
'sub-01_task-mixedgamblestask_run-01_bold.nii.gz'


Dynamically created, user-defined output fields can also be defined to
return different types of outputs from the same project. All outputs
are filtered on common entities, which can be explicitly defined as
infields.

>>> bg = BIDSDataGrabber(infields = ['subject'], outfields = ['dwi'])
>>> bg.inputs.base_dir = 'ds005/'
>>> bg.inputs.subject = '01'
>>> bg.inputs.output_query['dwi'] = dict(modality='dwi')
>>> results = bg.run()
>>> basename(results.outputs.dwi[0]) # doctest: +ALLOW_UNICODE
'sub-01_dwi.nii.gz'

"""
input_spec = BIDSDataGrabberInputSpec
output_spec = DynamicTraitedSpec
_always_run = True

def __init__(self, infields=None, outfields=None, **kwargs):
"""
Parameters
----------
infields : list of str
Indicates the input fields to be dynamically created

outfields: list of str
Indicates output fields to be dynamically created.
If no matching items, returns Undefined.
"""
super(BIDSDataGrabber, self).__init__(**kwargs)
if not have_pybids:
raise ImportError("The BIDSEventsGrabber interface requires pybids."
" Please make sure it is installed.")

# If outfields is None use anat and func as default
if outfields is None:
outfields = ['func', 'anat']
self.inputs.output_query = {
"func": {"modality": "func"},
"anat": {"modality": "anat"}}
else:
self.inputs.output_query = {}

# If infields is None, use all BIDS entities
if infields is None:
bids_config = join(dirname(gb.__file__), 'config', 'bids.json')
Copy link
Member

Choose a reason for hiding this comment

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

as a future improvement to a GrabbitIT interface that could serve as a base class, could this be passed along as a parameter?

this would also allow arbitrary and newer layouts to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. The issue here is that I cannot get the entities of a project (or BIDS entities in general), without initializing a BIDSLayout with a specific dataset. So this was the only way I could think of to access the entities definition.

bids_config = json.load(open(bids_config, 'r'))
infields = [i['name'] for i in bids_config['entities']]

self._infields = infields
self._outfields = outfields

# used for mandatory inputs check
undefined_traits = {}
for key in infields:
self.inputs.add_trait(key, traits.Any)
undefined_traits[key] = Undefined

self.inputs.trait_set(trait_change_notify=False, **undefined_traits)

def _run_interface(self, runtime):
return runtime

def _list_outputs(self):
layout = gb.BIDSLayout(self.inputs.base_dir)

for key in self._outfields:
if key not in self.inputs.output_query:
raise ValueError("Define query for all outputs")

# If infield is not given nm input value, silently ignore
filters = {}
for key in self._infields:
value = getattr(self.inputs, key)
if isdefined(value):
filters[key] = value

outputs = {}
for key, query in self.inputs.output_query.items():
args = query.copy()
args.update(filters)
filelist = layout.get(return_type=self.inputs.return_type,
**args)
if len(filelist) == 0:
msg = 'Output key: %s returned no files' % (
key)
if self.inputs.raise_on_empty:
raise IOError(msg)
else:
LOGGER.warning(msg)
filelist = Undefined

outputs[key] = filelist
return outputs
1 change: 1 addition & 0 deletions nipype/interfaces/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .base import (
TraitedSpec, traits, Str, File, Directory, BaseInterface, InputMultiPath,
isdefined, OutputMultiPath, DynamicTraitedSpec, Undefined, BaseInterfaceInputSpec)
from .bids_utils import BIDSDataGrabber

try:
import pyxnat
Expand Down