Skip to content

ENH: initial connectome workbench support #2594

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 9 commits into from
May 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions nipype/interfaces/workbench/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# -*- 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:

from .metric import MetricResample
69 changes: 69 additions & 0 deletions nipype/interfaces/workbench/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- 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:
"""
The workbench module provides classes for interfacing with `connectome workbench
<https://www.humanconnectome.org/software/connectome-workbench>`_ tools.

`Connectome Workbench is an open source, freely available visualization and
discovery tool used to map neuroimaging data, especially data generated by the
Human Connectome Project.
"""

from __future__ import (print_function, division, unicode_literals,
absolute_import)
import os
import re

from ... import logging
from ...utils.filemanip import split_filename
from ..base import CommandLine, PackageInfo

iflogger = logging.getLogger('interface')


class Info(PackageInfo):
"""
Handle `wb_command` version information.
"""

version_cmd = 'wb_command -version'

@staticmethod
def parse_version(raw_info):
m = re.search(r'\nVersion (\S+)', raw_info)
return m.groups()[0] if m else None


class WBCommand(CommandLine):
"""Base support for workbench commands."""

@property
def version(self):
return Info.version()

def _gen_filename(self, name, outdir=None, suffix='', ext=None):
"""Generate a filename based on the given parameters.
The filename will take the form: <basename><suffix><ext>.
Parameters
----------
name : str
Filename to base the new filename on.
suffix : str
Suffix to add to the `basename`. (defaults is '' )
ext : str
Extension to use for the new filename.
Returns
-------
fname : str
New filename based on given parameters.
"""
if not name:
raise ValueError("Cannot generate filename - filename not set")

_, fname, fext = split_filename(name)
if ext is None:
ext = fext
if outdir is None:
outdir = '.'
return os.path.join(outdir, fname + suffix + ext)
161 changes: 161 additions & 0 deletions nipype/interfaces/workbench/metric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# -*- 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:
"""This module provides interfaces for workbench surface commands"""
from __future__ import (print_function, division, unicode_literals,
absolute_import)
import os

from ..base import (TraitedSpec, File, traits, CommandLineInputSpec)
from .base import WBCommand
from ... import logging

iflogger = logging.getLogger('interface')


class MetricResampleInputSpec(CommandLineInputSpec):
in_file = File(
exists=True,
mandatory=True,
argstr="%s",
position=0,
desc="The metric file to resample")
current_sphere = File(
exists=True,
mandatory=True,
argstr="%s",
position=1,
desc="A sphere surface with the mesh that the metric is currently on")
new_sphere = File(
exists=True,
mandatory=True,
argstr="%s",
position=2,
desc="A sphere surface that is in register with <current-sphere> and"
" has the desired output mesh")
method = traits.Enum(
"ADAP_BARY_AREA",
"BARYCENTRIC",
argstr="%s",
mandatory=True,
position=3,
desc="The method name - ADAP_BARY_AREA method is recommended for"
" ordinary metric data, because it should use all data while"
" downsampling, unlike BARYCENTRIC. If ADAP_BARY_AREA is used,"
" exactly one of area_surfs or area_metrics must be specified")
out_file = File(
name_source=["new_sphere"],
name_template="%s.out",
keep_extension=True,
argstr="%s",
position=4,
desc="The output metric")
Copy link
Member

Choose a reason for hiding this comment

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

Maybe rename this to out_file (or rename both here and outputs.out_file to metric_file)? In addition to resolving your trait weirdness, I think it'll be less confusing for people for the connected traits to have the same name.

area_surfs = traits.Bool(
position=5,
argstr="-area-surfs",
xor=["area_metrics"],
desc="Specify surfaces to do vertex area correction based on")
area_metrics = traits.Bool(
position=5,
argstr="-area-metrics",
xor=["area_surfs"],
desc="Specify vertex area metrics to do area correction based on")
current_area = File(
exists=True,
position=6,
argstr="%s",
desc="A relevant anatomical surface with <current-sphere> mesh OR"
" a metric file with vertex areas for <current-sphere> mesh")
new_area = File(
exists=True,
position=7,
argstr="%s",
desc="A relevant anatomical surface with <current-sphere> mesh OR"
" a metric file with vertex areas for <current-sphere> mesh")
roi_metric = File(
exists=True,
position=8,
argstr="-current-roi %s",
desc="Input roi on the current mesh used to exclude non-data vertices")
valid_roi_out = traits.Bool(
position=9,
argstr="-valid-roi-out",
desc="Output the ROI of vertices that got data from valid source vertices")
largest = traits.Bool(
position=10,
argstr="-largest",
desc="Use only the value of the vertex with the largest weight")


class MetricResampleOutputSpec(TraitedSpec):
out_file = File(exists=True, desc="the output metric")
roi_file = File(desc="ROI of vertices that got data from valid source vertices")


class MetricResample(WBCommand):
"""
Resample a metric file to a different mesh

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would help documentation to copy a bit more from their docs:

      Resample a metric file to a different mesh

      Resamples a metric file, given two spherical surfaces that are in
      register.  If ``ADAP_BARY_AREA`` is used, exactly one of -area-surfs or
      ``-area-metrics`` must be specified.

      The ``ADAP_BARY_AREA`` method is recommended for ordinary metric data,
      because it should use all data while downsampling, unlike ``BARYCENTRIC``.
      The recommended areas option for most data is individual midthicknesses
      for individual data, and averaged vertex area metrics from individual
      midthicknesses for group average data.

      The ``-current-roi`` option only masks the input, the output may be slightly
      dilated in comparison, consider using ``-metric-mask`` on the output when
      using ``-current-roi``.

      The ``-largest option`` results in nearest vertex behavior when used with
      ``BARYCENTRIC``.  When resampling a binary metric, consider thresholding at
      0.5 after resampling rather than using ``-largest``.

Copy link
Member

Choose a reason for hiding this comment

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

@oesteban Want to just push this change? It's after 5pm here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure

Resamples a metric file, given two spherical surfaces that are in
register. If ``ADAP_BARY_AREA`` is used, exactly one of -area-surfs or
``-area-metrics`` must be specified.

The ``ADAP_BARY_AREA`` method is recommended for ordinary metric data,
because it should use all data while downsampling, unlike ``BARYCENTRIC``.
The recommended areas option for most data is individual midthicknesses
for individual data, and averaged vertex area metrics from individual
midthicknesses for group average data.

The ``-current-roi`` option only masks the input, the output may be slightly
dilated in comparison, consider using ``-metric-mask`` on the output when
using ``-current-roi``.

The ``-largest option`` results in nearest vertex behavior when used with
``BARYCENTRIC``. When resampling a binary metric, consider thresholding at
0.5 after resampling rather than using ``-largest``.

>>> from nipype.interfaces.workbench import MetricResample
>>> metres = MetricResample()
>>> metres.inputs.in_file = 'sub-01_task-rest_bold_space-fsaverage5.L.func.gii'
>>> metres.inputs.method = 'ADAP_BARY_AREA'
>>> metres.inputs.current_sphere = 'fsaverage5_std_sphere.L.10k_fsavg_L.surf.gii'
>>> metres.inputs.new_sphere = 'fs_LR-deformed_to-fsaverage.L.sphere.32k_fs_LR.surf.gii'
>>> metres.inputs.area_metrics = True
>>> metres.inputs.current_area = 'fsaverage5.L.midthickness_va_avg.10k_fsavg_L.shape.gii'
>>> metres.inputs.new_area = 'fs_LR.L.midthickness_va_avg.32k_fs_LR.shape.gii'
>>> metres.cmdline
'wb_command -metric-resample sub-01_task-rest_bold_space-fsaverage5.L.func.gii \
fsaverage5_std_sphere.L.10k_fsavg_L.surf.gii \
fs_LR-deformed_to-fsaverage.L.sphere.32k_fs_LR.surf.gii \
ADAP_BARY_AREA fs_LR-deformed_to-fsaverage.L.sphere.32k_fs_LR.surf.out \
-area-metrics fsaverage5.L.midthickness_va_avg.10k_fsavg_L.shape.gii \
fs_LR.L.midthickness_va_avg.32k_fs_LR.shape.gii'
"""
input_spec = MetricResampleInputSpec
output_spec = MetricResampleOutputSpec
_cmd = 'wb_command -metric-resample'

def _format_arg(self, opt, spec, val):
if opt in ['current_area', 'new_area']:
if not self.inputs.area_surfs and not self.inputs.area_metrics:
raise ValueError("{} was set but neither area_surfs or"
" area_metrics were set".format(opt))
if opt == "method":
if (val == "ADAP_BARY_AREA" and
not self.inputs.area_surfs and
not self.inputs.area_metrics):
raise ValueError("Exactly one of area_surfs or area_metrics"
" must be specified")
if opt == "valid_roi_out" and val:
# generate a filename and add it to argstr
roi_out = self._gen_filename(self.inputs.in_file, suffix='_roi')
iflogger.info("Setting roi output file as", roi_out)
spec.argstr += " " + roi_out
return super(MetricResample, self)._format_arg(opt, spec, val)

def _list_outputs(self):
outputs = super(MetricResample, self)._list_outputs()
if self.inputs.valid_roi_out:
roi_file = self._gen_filename(self.inputs.in_file, suffix='_roi')
outputs['roi_file'] = os.path.abspath(roi_file)
return outputs
Empty file.
94 changes: 94 additions & 0 deletions nipype/interfaces/workbench/tests/test_auto_MetricResample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
from __future__ import unicode_literals
from ..metric import MetricResample


def test_MetricResample_inputs():
input_map = dict(
area_metrics=dict(
argstr='-area-metrics',
position=5,
xor=['area_surfs'],
),
area_surfs=dict(
argstr='-area-surfs',
position=5,
xor=['area_metrics'],
),
args=dict(argstr='%s', ),
current_area=dict(
argstr='%s',
position=6,
),
current_sphere=dict(
argstr='%s',
mandatory=True,
position=1,
),
environ=dict(
nohash=True,
usedefault=True,
),
ignore_exception=dict(
deprecated='1.0.0',
nohash=True,
usedefault=True,
),
in_file=dict(
argstr='%s',
mandatory=True,
position=0,
),
largest=dict(
argstr='-largest',
position=10,
),
method=dict(
argstr='%s',
mandatory=True,
position=3,
),
new_area=dict(
argstr='%s',
position=7,
),
new_sphere=dict(
argstr='%s',
mandatory=True,
position=2,
),
out_file=dict(
argstr='%s',
keep_extension=True,
name_source=['new_sphere'],
name_template='%s.out',
position=4,
),
roi_metric=dict(
argstr='-current-roi %s',
position=8,
),
terminal_output=dict(
deprecated='1.0.0',
nohash=True,
),
valid_roi_out=dict(
argstr='-valid-roi-out',
position=9,
),
)
inputs = MetricResample.input_spec()

for key, metadata in list(input_map.items()):
for metakey, value in list(metadata.items()):
assert getattr(inputs.traits()[key], metakey) == value
def test_MetricResample_outputs():
output_map = dict(
out_file=dict(),
roi_file=dict(),
)
outputs = MetricResample.output_spec()

for key, metadata in list(output_map.items()):
for metakey, value in list(metadata.items()):
assert getattr(outputs.traits()[key], metakey) == value
27 changes: 27 additions & 0 deletions nipype/interfaces/workbench/tests/test_auto_WBCommand.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
from __future__ import unicode_literals
from ..base import WBCommand


def test_WBCommand_inputs():
input_map = dict(
args=dict(argstr='%s', ),
environ=dict(
nohash=True,
usedefault=True,
),
ignore_exception=dict(
deprecated='1.0.0',
nohash=True,
usedefault=True,
),
terminal_output=dict(
deprecated='1.0.0',
nohash=True,
),
)
inputs = WBCommand.input_spec()

for key, metadata in list(input_map.items()):
for metakey, value in list(metadata.items()):
assert getattr(inputs.traits()[key], metakey) == value
Empty file.
Empty file.
Empty file.