Skip to content

Commit

Permalink
TEST: Add smoke-tests for bold_fit_wf (nipreps#3152)
Browse files Browse the repository at this point in the history
Turns out testing's a good idea. Starting out doing something similar to
nipreps/smriprep#390. Will try to expand the smoke tests upward to
`workflows.bold.base` and `workflows.base`.
  • Loading branch information
effigies authored Nov 21, 2023
1 parent 7a6eb9c commit dfa59db
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 33 deletions.
17 changes: 1 addition & 16 deletions fmriprep/workflows/bold/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,21 +192,6 @@ def init_bold_wf(
mem_gb["largemem"],
)

functional_cache = {}
if config.execution.derivatives:
from fmriprep.utils.bids import collect_derivatives, extract_entities

entities = extract_entities(bold_series)

for deriv_dir in config.execution.derivatives:
functional_cache.update(
collect_derivatives(
derivatives_dir=deriv_dir,
entities=entities,
fieldmap_id=fieldmap_id,
)
)

workflow = Workflow(name=_get_wf_name(bold_file, "bold"))
workflow.__postdesc__ = """\
All resamplings can be performed with *a single interpolation
Expand Down Expand Up @@ -266,7 +251,7 @@ def init_bold_wf(

bold_fit_wf = init_bold_fit_wf(
bold_series=bold_series,
precomputed=functional_cache,
precomputed=precomputed,
fieldmap_id=fieldmap_id,
omp_nthreads=omp_nthreads,
)
Expand Down
4 changes: 3 additions & 1 deletion fmriprep/workflows/bold/fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,9 @@ def init_bold_fit_wf(
name="hmcref_buffer",
)
fmapref_buffer = pe.Node(niu.Function(function=_select_ref), name="fmapref_buffer")
hmc_buffer = pe.Node(niu.IdentityInterface(fields=["hmc_xforms"]), name="hmc_buffer")
hmc_buffer = pe.Node(
niu.IdentityInterface(fields=["hmc_xforms", "movpar_file", "rmsd_file"]), name="hmc_buffer"
)
fmapreg_buffer = pe.Node(
niu.IdentityInterface(fields=["boldref2fmap_xfm"]), name="fmapreg_buffer"
)
Expand Down
4 changes: 2 additions & 2 deletions fmriprep/workflows/bold/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,8 @@ def init_fsl_bbr_wf(
)

FSLDIR = os.getenv('FSLDIR')
if FSLDIR:
flt_bbr.inputs.schedule = op.join(FSLDIR, 'etc/flirtsch/bbr.sch')
if FSLDIR and os.path.exists(schedule := op.join(FSLDIR, 'etc/flirtsch/bbr.sch')):
flt_bbr.inputs.schedule = schedule
else:
# Should mostly be hit while building docs
LOGGER.warning("FSLDIR unset - using packaged BBR schedule")
Expand Down
77 changes: 77 additions & 0 deletions fmriprep/workflows/bold/tests/test_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from pathlib import Path

import nibabel as nb
import numpy as np
import pytest
from nipype.pipeline.engine.utils import generate_expanded_graph
from niworkflows.utils.testing import generate_bids_skeleton

from .... import config
from ...tests import mock_config
from ...tests.test_base import BASE_LAYOUT
from ..base import init_bold_wf


@pytest.fixture(scope="module", autouse=True)
def _quiet_logger():
import logging

logger = logging.getLogger("nipype.workflow")
old_level = logger.getEffectiveLevel()
logger.setLevel(logging.ERROR)
yield
logger.setLevel(old_level)


@pytest.fixture(scope="module")
def bids_root(tmp_path_factory):
base = tmp_path_factory.mktemp("boldbase")
bids_dir = base / "bids"
generate_bids_skeleton(bids_dir, BASE_LAYOUT)
yield bids_dir


@pytest.mark.parametrize("task", ["rest", "nback"])
@pytest.mark.parametrize("fieldmap_id", ["phasediff", None])
@pytest.mark.parametrize("freesurfer", [False, True])
@pytest.mark.parametrize("level", ["minimal", "resampling", "full"])
def test_bold_wf(
bids_root: Path,
tmp_path: Path,
task: str,
fieldmap_id: str | None,
freesurfer: bool,
level: str,
):
"""Test as many combinations of precomputed files and input
configurations as possible."""
output_dir = tmp_path / 'output'
output_dir.mkdir()

img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4))

if task == 'rest':
bold_series = [
str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'),
]
elif task == 'nback':
bold_series = [
str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz')
for i in range(1, 4)
]

# The workflow will attempt to read file headers
for path in bold_series:
img.to_filename(path)

with mock_config(bids_dir=bids_root):
config.workflow.level = level
config.workflow.run_reconall = freesurfer
wf = init_bold_wf(
bold_series=bold_series,
fieldmap_id=fieldmap_id,
precomputed={},
)

flatgraph = wf._create_flat_graph()
generate_expanded_graph(flatgraph)
172 changes: 172 additions & 0 deletions fmriprep/workflows/bold/tests/test_fit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
from pathlib import Path

import nibabel as nb
import numpy as np
import pytest
from nipype.pipeline.engine.utils import generate_expanded_graph
from niworkflows.utils.testing import generate_bids_skeleton

from .... import config
from ...tests import mock_config
from ...tests.test_base import BASE_LAYOUT
from ..fit import init_bold_fit_wf, init_bold_native_wf


@pytest.fixture(scope="module", autouse=True)
def _quiet_logger():
import logging

logger = logging.getLogger("nipype.workflow")
old_level = logger.getEffectiveLevel()
logger.setLevel(logging.ERROR)
yield
logger.setLevel(old_level)


@pytest.fixture(scope="module")
def bids_root(tmp_path_factory):
base = tmp_path_factory.mktemp("boldfit")
bids_dir = base / "bids"
generate_bids_skeleton(bids_dir, BASE_LAYOUT)
yield bids_dir


def _make_params(
have_hmcref: bool = True,
have_coregref: bool = True,
have_hmc_xfms: bool = True,
have_boldref2fmap_xfm: bool = True,
have_boldref2anat_xfm: bool = True,
):
return (
have_hmcref,
have_coregref,
have_hmc_xfms,
have_boldref2anat_xfm,
have_boldref2fmap_xfm,
)


@pytest.mark.parametrize("task", ["rest", "nback"])
@pytest.mark.parametrize("fieldmap_id", ["phasediff", None])
@pytest.mark.parametrize(
(
'have_hmcref',
'have_coregref',
'have_hmc_xfms',
'have_boldref2fmap_xfm',
'have_boldref2anat_xfm',
),
[
(True, True, True, True, True),
(False, False, False, False, False),
_make_params(have_hmcref=False),
_make_params(have_hmc_xfms=False),
_make_params(have_coregref=False),
_make_params(have_coregref=False, have_boldref2fmap_xfm=False),
_make_params(have_boldref2anat_xfm=False),
],
)
def test_bold_fit_precomputes(
bids_root: Path,
tmp_path: Path,
task: str,
fieldmap_id: str | None,
have_hmcref: bool,
have_coregref: bool,
have_hmc_xfms: bool,
have_boldref2fmap_xfm: bool,
have_boldref2anat_xfm: bool,
):
"""Test as many combinations of precomputed files and input
configurations as possible."""
output_dir = tmp_path / 'output'
output_dir.mkdir()

img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4))

if task == 'rest':
bold_series = [
str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'),
]
elif task == 'nback':
bold_series = [
str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz')
for i in range(1, 4)
]

# The workflow will attempt to read file headers
for path in bold_series:
img.to_filename(path)

dummy_nifti = str(tmp_path / 'dummy.nii')
dummy_affine = str(tmp_path / 'dummy.txt')
img.to_filename(dummy_nifti)
np.savetxt(dummy_affine, np.eye(4))

# Construct precomputed files
precomputed = {'transforms': {}}
if have_hmcref:
precomputed['hmc_boldref'] = dummy_nifti
if have_coregref:
precomputed['coreg_boldref'] = dummy_nifti
if have_hmc_xfms:
precomputed['transforms']['hmc'] = dummy_affine
if have_boldref2anat_xfm:
precomputed['transforms']['boldref2anat'] = dummy_affine
if have_boldref2fmap_xfm:
precomputed['transforms']['boldref2fmap'] = dummy_affine

with mock_config(bids_dir=bids_root):
wf = init_bold_fit_wf(
bold_series=bold_series,
precomputed=precomputed,
fieldmap_id=fieldmap_id,
omp_nthreads=1,
)

flatgraph = wf._create_flat_graph()
generate_expanded_graph(flatgraph)


@pytest.mark.parametrize("task", ["rest", "nback"])
@pytest.mark.parametrize("fieldmap_id", ["phasediff", None])
@pytest.mark.parametrize("run_stc", [True, False])
def test_bold_native_precomputes(
bids_root: Path,
tmp_path: Path,
task: str,
fieldmap_id: str | None,
run_stc: bool,
):
"""Test as many combinations of precomputed files and input
configurations as possible."""
output_dir = tmp_path / 'output'
output_dir.mkdir()

img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4))

if task == 'rest':
bold_series = [
str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'),
]
elif task == 'nback':
bold_series = [
str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz')
for i in range(1, 4)
]

# The workflow will attempt to read file headers
for path in bold_series:
img.to_filename(path)

with mock_config(bids_dir=bids_root):
config.workflow.ignore = ['slicetiming'] if not run_stc else []
wf = init_bold_native_wf(
bold_series=bold_series,
fieldmap_id=fieldmap_id,
omp_nthreads=1,
)

flatgraph = wf._create_flat_graph()
generate_expanded_graph(flatgraph)
8 changes: 6 additions & 2 deletions fmriprep/workflows/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@


@contextmanager
def mock_config():
def mock_config(bids_dir=None):
"""Create a mock config for documentation and testing purposes."""
from ... import config

Expand All @@ -51,9 +51,13 @@ def mock_config():
config.loggers.init()
config.init_spaces()

bids_dir = bids_dir or data.load('tests/ds000005').absolute()

config.execution.work_dir = Path(mkdtemp())
config.execution.bids_dir = data.load('tests/ds000005').absolute()
config.execution.bids_dir = bids_dir
config.execution.fmriprep_dir = Path(mkdtemp())
config.execution.bids_database_dir = None
config.execution._layout = None
config.execution.init()

yield
Expand Down
Loading

0 comments on commit dfa59db

Please sign in to comment.