diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b9fab5d092b766e33361a50ee96fb52c1f921953..f47e76e9a1ad9d672be3ebc57d57d294038a5662 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,12 @@ order. -------------------------- +Added +^^^^^ + +* New :func:`.cluster` wrapper function for the FSL ``cluster`` / + ``fsl-cluster`` command (!417). + Changed ^^^^^^^ diff --git a/fsl/tests/test_wrappers/__init__.py b/fsl/tests/test_wrappers/__init__.py index e146dbf6aaf91c0091e2eb95206fd45a14093421..4dca534cd4278553f118b3ab4d0c7876735e9976 100644 --- a/fsl/tests/test_wrappers/__init__.py +++ b/fsl/tests/test_wrappers/__init__.py @@ -46,3 +46,4 @@ def testenv(*fslexes): fslexes = [op.join(fsldir, 'bin', e) for e in fslexes] if len(fslexes) == 1: yield fslexes[0] else: yield fslexes +testenv.__test__ = False diff --git a/fsl/tests/test_wrappers/test_cluster.py b/fsl/tests/test_wrappers/test_cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..9475a026e82b5e192d147cc2d4ba9a4c1defa8af --- /dev/null +++ b/fsl/tests/test_wrappers/test_cluster.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# +# test_cluster.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# + +import contextlib +import os +import os.path as op +import sys + +import numpy as np + +from fsl.wrappers.cluster import cluster, _cluster + +from . import testenv +from .. import mockFSLDIR + + +@contextlib.contextmanager +def reseed(seed): + state = np.random.get_state() + np.random.seed(seed) + try: + yield + finally: + np.random.set_state(state) + + +def test_cluster_wrapper(): + with testenv('fsl-cluster') as cluster_exe: + result = _cluster('input', 10, mm=True) + expected = [cluster_exe, '-i', 'input', '-t', '10', '--mm'] + expected = ' '.join(expected) + assert result.stdout[0] == expected + + +mock_titles = 'ABCDEFGHIJ' +mock_cluster = f""" +#!{sys.executable} + +import numpy as np + +np.random.seed(12345) +data = np.random.randint(1, 10, (10, 10)) + +print('\t'.join('{mock_titles}')) +for row in data: + print('\t'.join([str(val) for val in row])) +""".strip() + + +def test_cluster(): + with mockFSLDIR() as fsldir: + cluster_exe = op.join(fsldir, 'bin', 'fsl-cluster') + with open(cluster_exe, 'wt') as f: + f.write(mock_cluster) + os.chmod(cluster_exe, 0o755) + + data, titles, result1 = cluster('input', 3.5) + result2 = cluster('input', 3.5, load=False) + + with reseed(12345): + expected = np.random.randint(1, 10, (10, 10)) + + assert np.all(np.isclose(data, expected)) + assert ''.join(titles) == mock_titles + assert result1.stdout == result2.stdout diff --git a/fsl/wrappers/__init__.py b/fsl/wrappers/__init__.py index 1f9b0a9ae9d7187d180035de77fb37e63c82a409..3cfb92533333193f19abd08586023b0a66487bfa 100755 --- a/fsl/wrappers/__init__.py +++ b/fsl/wrappers/__init__.py @@ -103,6 +103,7 @@ from fsl.wrappers.wrapperutils import (LOAD, fslwrapper, funcwrapper) from fsl.wrappers import (tbss,) +from fsl.wrappers.cluster import (cluster,) from fsl.wrappers.bet import (bet, robustfov) from fsl.wrappers.eddy import (eddy, diff --git a/fsl/wrappers/cluster.py b/fsl/wrappers/cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..6c63e382096c32125c4cd605c1243b814dfa47b7 --- /dev/null +++ b/fsl/wrappers/cluster.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# +# cluster.py - Wrapper for the FSL cluster command. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module contains the :func:`cluster` function, which calls +the FSL ``cluster`` / ``fsl-cluster``command. +""" + + +import io +import numpy as np + +from . import wrapperutils as wutils + + +def cluster(infile, thres, load=True, **kwargs): + """Wrapper function for the FSL ``cluster`/``fsl-cluster``) command. + + If ``load is True`` (the default) a tuple is returned, containing: + - A numpy array containing the cluster table + - A list of column titles + - A dictionary containing references to the standard output as an + attribute called ``stdout``, and to the output images if any, + e.g. ``--oindex``. + + If ``load is False``, only the dictionary is returned. + """ + + result = _cluster(infile, thres, **kwargs) + + if load: + header, data = result.stdout[0].split('\n', 1) + titles = header.split('\t') + data = np.loadtxt(io.StringIO(data), delimiter='\t') + + return data, titles, result + else: + return result + + +@wutils.fileOrImage('infile', 'othresh', 'olmaxim', 'osize', + 'omax', 'omean', 'opvals', 'cope', 'warpvol',) +@wutils.fslwrapper +def _cluster(infile, thresh, **kwargs): + """Actual wrapper function for the FSL ``cluster``/``fsl-cluster`` command. + """ + + valmap = { + 'fractional' : wutils.SHOW_IF_TRUE, + 'mm' : wutils.SHOW_IF_TRUE, + 'min' : wutils.SHOW_IF_TRUE, + 'no_table' : wutils.SHOW_IF_TRUE, + 'minclustersize' : wutils.SHOW_IF_TRUE, + 'scalarname' : wutils.SHOW_IF_TRUE, + 'verbose' : wutils.SHOW_IF_TRUE, + 'voxthresh' : wutils.SHOW_IF_TRUE, + 'voxuncthresh' : wutils.SHOW_IF_TRUE, + } + + cmd = ['fsl-cluster', '-i', infile, '-t', str(thresh)] + cmd += wutils.applyArgStyle('--=', valmap=valmap, **kwargs) + + return cmd