Skip to content
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

TEST: Add smoke-tests for bold_fit_wf #3152

Merged
merged 9 commits into from
Nov 21, 2023
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 @@
)

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

Check warning on line 586 in fmriprep/workflows/bold/registration.py

View check run for this annotation

Codecov / codecov/patch

fmriprep/workflows/bold/registration.py#L586

Added line #L586 was not covered by tests
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