diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 956b527caed80d3f6bc1bba6d84ed188fe6bf232..a443c7b579be2a7906cbe37c215145d2ebac220f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ Added * Added more functions to the :class:`.fslmaths` wrapper (!431). +* New :func:`.smoothest` wrapper function (!432). 3.15.4 (Monday 27th November 2023) diff --git a/doc/conf.py b/doc/conf.py index 1e5e868f22fdc3ac48ad0fa58649f6225b32927b..073f4d5d29eb2d502bf516d2e81ed0764e24a7df 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -12,12 +12,70 @@ # All configuration values have a default; values that are commented out # serve to show the default. +import glob +import itertools as it import os +import os.path as op import sys import datetime date = datetime.date.today() + +def check_for_missing_stubs(): + + docdir = op.dirname(__file__) + basedir = op.join(docdir, '..') + modules = [] + + def tomodname(f): + if f.endswith('.py'): + f = f[:-3] + return op.relpath(op.join(dirpath, f), basedir).replace(op.sep, '.') + + for dirpath, dirnames, filenames in os.walk(op.join(basedir, 'fsl')): + for d in dirnames: + if d == '__pycache__': + continue + if len(glob.glob(op.join(dirpath, d, '**', '*.py'), recursive=True)) == 0: + continue + modules.append(tomodname(d)) + + for f in filenames: + if not f.endswith('.py'): + continue + if f in ('__init__.py', '__main__.py'): + continue + modules.append(tomodname(f)) + + modules = [m for m in modules if not m.startswith('fsl.tests')] + + # import fsl + # modules = recurse(fsl) + # modules = [m.name for m in modules] + + # print() + # print() + # print() + + for mod in modules: + + docfile = op.join(docdir, f'{mod}.rst') + + if not op.exists(docfile): + print(f'No doc file found for module: {mod}') + + for docfile in glob.glob(op.join(docdir, '*.rst')): + docfile = op.relpath(docfile, basedir) + mod = op.splitext(op.basename(docfile))[0] + if mod not in modules: + print(f'No module found for doc file: {docfile}') + + +if __name__ == '__main__': + check_for_missing_stubs() + + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. diff --git a/doc/fsl.wrappers.cluster_commands.rst b/doc/fsl.wrappers.cluster_commands.rst new file mode 100644 index 0000000000000000000000000000000000000000..6749dc5be4c4ac104e436eaf39df919ac100f658 --- /dev/null +++ b/doc/fsl.wrappers.cluster_commands.rst @@ -0,0 +1,7 @@ +``fsl.wrappers.cluster_commands`` +================================= + +.. automodule:: fsl.wrappers.cluster_commands + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/fsl.wrappers.oxford_asl.rst b/doc/fsl.wrappers.oxford_asl.rst new file mode 100644 index 0000000000000000000000000000000000000000..cfdbdccb7b2792c27b5c5029ac3ce481c0a2a5eb --- /dev/null +++ b/doc/fsl.wrappers.oxford_asl.rst @@ -0,0 +1,7 @@ +``fsl.wrappers.oxford_asl`` +=========================== + +.. automodule:: fsl.wrappers.oxford_asl + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/fsl.wrappers.rst b/doc/fsl.wrappers.rst index 6277046d469cf4e0ce0cc6bd070ac286ab28b47e..792a188f11e2dab51ea062dc3ceff9a30a8c3dd7 100644 --- a/doc/fsl.wrappers.rst +++ b/doc/fsl.wrappers.rst @@ -8,6 +8,7 @@ fsl.wrappers.bedpostx fsl.wrappers.bet fsl.wrappers.bianca + fsl.wrappers.cluster_commands fsl.wrappers.dtifit fsl.wrappers.eddy fsl.wrappers.epi_reg @@ -23,6 +24,7 @@ fsl.wrappers.fugue fsl.wrappers.melodic fsl.wrappers.misc + fsl.wrappers.oxford_asl fsl.wrappers.randomise fsl.wrappers.tbss fsl.wrappers.wrapperutils diff --git a/fsl/tests/test_wrappers/test_cluster.py b/fsl/tests/test_wrappers/test_cluster.py index f6b92c58e16bafe17a3ce0a5e2d3849a8c9d41cc..6e0cfb8763b73922634ef4d1a0963f31435a6f14 100644 --- a/fsl/tests/test_wrappers/test_cluster.py +++ b/fsl/tests/test_wrappers/test_cluster.py @@ -9,10 +9,17 @@ import contextlib import os import os.path as op import sys +import textwrap as tw + +from unittest import mock import numpy as np -from fsl.wrappers.cluster import cluster, _cluster +from fsl.wrappers import wrapperutils as wutils +from fsl.wrappers.cluster_commands import (cluster, + _cluster, + smoothest, + _smoothest) from . import testenv from .. import mockFSLDIR @@ -67,3 +74,56 @@ def test_cluster(): assert np.all(np.isclose(data, expected)) assert ''.join(titles) == mock_titles assert result1.stdout == result2.stdout + + +def test_smoothest_wrapper(): + with testenv('smoothest') as smoothest_exe: + result = _smoothest(res='res', zstat='zstat', d=5, V=True) + expected = f'{smoothest_exe} --res=res --zstat=zstat -d 5 -V' + assert result.stdout[0] == expected + + # auto detect residuals vs zstat + result = _smoothest('res4d.nii.gz', d=5, V=True) + expected = f'{smoothest_exe} -d 5 -V --res=res4d.nii.gz' + assert result.stdout[0] == expected + + result = _smoothest('zstat1.nii.gz', d=5, V=True) + expected = f'{smoothest_exe} -d 5 -V --zstat=zstat1.nii.gz' + assert result.stdout[0] == expected + + +def test_smoothest(): + + result = tw.dedent(""" + FWHMx = 4.763 mm, FWHMy = 5.06668 mm, FWHMz = 4.71527 mm + DLH 0.324569 voxels^-3 + VOLUME 244531 voxels + RESELS 14.224 voxels per resel + DLH 0.324569 + VOLUME 244531 + RESELS 14.224 + FWHMvoxel 2.3815 2.53334 2.35763 + FWHMmm 4.763 5.06668 4.71527 + """) + + result = wutils.FileOrThing.Results((result, '')) + + expect = { + 'DLH' : 0.324569, + 'VOLUME' : 244531, + 'RESELS' : 14.224, + 'FWHMvoxel' : [2.3815, 2.53334, 2.35763], + 'FWHMmm' : [4.763, 5.06668, 4.71527] + } + + with mock.patch('fsl.wrappers.cluster_commands._smoothest', + return_value=result): + + result = smoothest('inimage') + + assert result.keys() == expect.keys() + assert np.isclose(result['DLH'], expect['DLH']) + assert np.isclose(result['VOLUME'], expect['VOLUME']) + assert np.isclose(result['RESELS'], expect['RESELS']) + assert np.all(np.isclose(result['FWHMvoxel'], expect['FWHMvoxel'])) + assert np.all(np.isclose(result['FWHMmm'], expect['FWHMmm'])) diff --git a/fsl/wrappers/__init__.py b/fsl/wrappers/__init__.py index aa90bb4566e7f7c8487d4344f8294bf05a9bc2ef..124c2115037eda8bcb23628f24ebe1261b45808c 100755 --- a/fsl/wrappers/__init__.py +++ b/fsl/wrappers/__init__.py @@ -101,7 +101,8 @@ from fsl.wrappers.wrapperutils import (LOAD, fslwrapper, funcwrapper) from fsl.wrappers import (tbss,) -from fsl.wrappers.cluster import (cluster,) +from fsl.wrappers.cluster_commands import (cluster, + smoothest) from fsl.wrappers.bet import (bet, robustfov) from fsl.wrappers.eddy import (eddy, diff --git a/fsl/wrappers/cluster.py b/fsl/wrappers/cluster_commands.py similarity index 55% rename from fsl/wrappers/cluster.py rename to fsl/wrappers/cluster_commands.py index 046a28ef597feeca52ec386a1956615acb5eb635..c3b28e96f1f91cd0b5c519afb6ac02505bcce7c0 100644 --- a/fsl/wrappers/cluster.py +++ b/fsl/wrappers/cluster_commands.py @@ -66,3 +66,59 @@ def _cluster(infile, thresh, **kwargs): cmd += wutils.applyArgStyle('--=', valmap=valmap, **kwargs) return cmd + + +def smoothest(inimg=None, **kwargs): + """Wrapper for the ``smoothest`` command. + + The residual or zstatistic image may be passed as the first positional + argument (``inimg``) - its type is inferred from the image file name if + possible. If this is not possible (e.g. non-standard file names or + in-memory images), you must specify residual images via ``res``, or + zstatistic images via ``zstat``. + + Returns a dictionary containing the parameters estimated by ``smoothest``, + e.g.:: + + { + 'DLH' : 1.25903, + 'VOLUME' : 239991, + 'RESELS' : 3.69574, + 'FWHMvoxel' : [1.57816, 1.64219, 1.42603], + 'FWHMmm' : [3.15631, 3.28437, 2.85206] + } + """ + result = _smoothest(**kwargs) + result = result.stdout[0] + result = result.strip().split('\n')[-5:] + values = {} + + for line in result: + key, vals = line.split(maxsplit=1) + vals = [float(v) for v in vals.split()] + + if len(vals) == 1: + vals = vals[0] + + values[key] = vals + + return values + + +@wutils.fileOrImage('inimg', 'r', 'res', 'z', 'zstat', 'm', 'mask') +@wutils.fslwrapper +def _smoothest(inimg=None, **kwargs): + """Actual wrapper for the ``smoothest`` command.""" + + if inimg is not None: + if 'res4d' in inimg: kwargs['res'] = inimg + elif 'zstat' in inimg: kwargs['zstat'] = inimg + else: raise RuntimeError('Cannot infer type of input ' + f'image {inimg.name}') + + valmap = { + 'V' : wutils.SHOW_IF_TRUE, + 'verbose' : wutils.SHOW_IF_TRUE, + } + + return ['smoothest'] + wutils.applyArgStyle('--=', valmap=valmap, **kwargs) diff --git a/fsl/wrappers/fnirt.py b/fsl/wrappers/fnirt.py index f4ca120fceebed4cdd97310087a1f816e544a979..d5c5bd21a02603522e74b810152ed0c9ea665e50 100644 --- a/fsl/wrappers/fnirt.py +++ b/fsl/wrappers/fnirt.py @@ -32,8 +32,13 @@ def fnirt(src, **kwargs): asrt.assertIsNifti(src) + valmap = { + 'v' : wutils.SHOW_IF_TRUE, + 'verbose' : wutils.SHOW_IF_TRUE, + } + cmd = ['fnirt', '--in={}'.format(src)] - cmd += wutils.applyArgStyle('--=', **kwargs) + cmd += wutils.applyArgStyle('--=', valmap=valmap, **kwargs) return cmd diff --git a/fsl/wrappers/fslstats.py b/fsl/wrappers/fslstats.py index 67942fba59f1b79f34d5a18a93bcd34778a2fc35..c25dc238d55169962b2f0776f4ab472ee14ac788 100644 --- a/fsl/wrappers/fslstats.py +++ b/fsl/wrappers/fslstats.py @@ -31,20 +31,28 @@ class fslstats: present in older versions. + This ``fslstats`` command:: + + fslstats image -r -p 95 -R + + + is equivalent to this function call:: + + fslstats('image').r.p(95).R.run() + + Any ``fslstats`` command-line option which does not require any arguments (e.g. ``-r``) can be set by accessing an attribute on a ``fslstats`` object, e.g.:: - stats = fslstats('image.nii.gz') - stats.r + fslstats('image.nii.gz').r.run() ``fslstats`` command-line options which do require additional arguments (e.g. ``-k``) can be set by calling a method on an ``fslstats`` object, e.g.:: - stats = fslstats('image.nii.gz') - stats.k('mask.nii.gz') + stats = fslstats('image.nii.gz').k('mask.nii.gz').run() The ``fslstats`` command can be executed via the :meth:`run` method.