Skip to content

Commit dfa59db

Browse files
authored
TEST: Add smoke-tests for bold_fit_wf (#3152)
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`.
1 parent 7a6eb9c commit dfa59db

File tree

7 files changed

+308
-33
lines changed

7 files changed

+308
-33
lines changed

fmriprep/workflows/bold/base.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,21 +192,6 @@ def init_bold_wf(
192192
mem_gb["largemem"],
193193
)
194194

195-
functional_cache = {}
196-
if config.execution.derivatives:
197-
from fmriprep.utils.bids import collect_derivatives, extract_entities
198-
199-
entities = extract_entities(bold_series)
200-
201-
for deriv_dir in config.execution.derivatives:
202-
functional_cache.update(
203-
collect_derivatives(
204-
derivatives_dir=deriv_dir,
205-
entities=entities,
206-
fieldmap_id=fieldmap_id,
207-
)
208-
)
209-
210195
workflow = Workflow(name=_get_wf_name(bold_file, "bold"))
211196
workflow.__postdesc__ = """\
212197
All resamplings can be performed with *a single interpolation
@@ -266,7 +251,7 @@ def init_bold_wf(
266251

267252
bold_fit_wf = init_bold_fit_wf(
268253
bold_series=bold_series,
269-
precomputed=functional_cache,
254+
precomputed=precomputed,
270255
fieldmap_id=fieldmap_id,
271256
omp_nthreads=omp_nthreads,
272257
)

fmriprep/workflows/bold/fit.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,9 @@ def init_bold_fit_wf(
287287
name="hmcref_buffer",
288288
)
289289
fmapref_buffer = pe.Node(niu.Function(function=_select_ref), name="fmapref_buffer")
290-
hmc_buffer = pe.Node(niu.IdentityInterface(fields=["hmc_xforms"]), name="hmc_buffer")
290+
hmc_buffer = pe.Node(
291+
niu.IdentityInterface(fields=["hmc_xforms", "movpar_file", "rmsd_file"]), name="hmc_buffer"
292+
)
291293
fmapreg_buffer = pe.Node(
292294
niu.IdentityInterface(fields=["boldref2fmap_xfm"]), name="fmapreg_buffer"
293295
)

fmriprep/workflows/bold/registration.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,8 +582,8 @@ def init_fsl_bbr_wf(
582582
)
583583

584584
FSLDIR = os.getenv('FSLDIR')
585-
if FSLDIR:
586-
flt_bbr.inputs.schedule = op.join(FSLDIR, 'etc/flirtsch/bbr.sch')
585+
if FSLDIR and os.path.exists(schedule := op.join(FSLDIR, 'etc/flirtsch/bbr.sch')):
586+
flt_bbr.inputs.schedule = schedule
587587
else:
588588
# Should mostly be hit while building docs
589589
LOGGER.warning("FSLDIR unset - using packaged BBR schedule")
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from pathlib import Path
2+
3+
import nibabel as nb
4+
import numpy as np
5+
import pytest
6+
from nipype.pipeline.engine.utils import generate_expanded_graph
7+
from niworkflows.utils.testing import generate_bids_skeleton
8+
9+
from .... import config
10+
from ...tests import mock_config
11+
from ...tests.test_base import BASE_LAYOUT
12+
from ..base import init_bold_wf
13+
14+
15+
@pytest.fixture(scope="module", autouse=True)
16+
def _quiet_logger():
17+
import logging
18+
19+
logger = logging.getLogger("nipype.workflow")
20+
old_level = logger.getEffectiveLevel()
21+
logger.setLevel(logging.ERROR)
22+
yield
23+
logger.setLevel(old_level)
24+
25+
26+
@pytest.fixture(scope="module")
27+
def bids_root(tmp_path_factory):
28+
base = tmp_path_factory.mktemp("boldbase")
29+
bids_dir = base / "bids"
30+
generate_bids_skeleton(bids_dir, BASE_LAYOUT)
31+
yield bids_dir
32+
33+
34+
@pytest.mark.parametrize("task", ["rest", "nback"])
35+
@pytest.mark.parametrize("fieldmap_id", ["phasediff", None])
36+
@pytest.mark.parametrize("freesurfer", [False, True])
37+
@pytest.mark.parametrize("level", ["minimal", "resampling", "full"])
38+
def test_bold_wf(
39+
bids_root: Path,
40+
tmp_path: Path,
41+
task: str,
42+
fieldmap_id: str | None,
43+
freesurfer: bool,
44+
level: str,
45+
):
46+
"""Test as many combinations of precomputed files and input
47+
configurations as possible."""
48+
output_dir = tmp_path / 'output'
49+
output_dir.mkdir()
50+
51+
img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4))
52+
53+
if task == 'rest':
54+
bold_series = [
55+
str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'),
56+
]
57+
elif task == 'nback':
58+
bold_series = [
59+
str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz')
60+
for i in range(1, 4)
61+
]
62+
63+
# The workflow will attempt to read file headers
64+
for path in bold_series:
65+
img.to_filename(path)
66+
67+
with mock_config(bids_dir=bids_root):
68+
config.workflow.level = level
69+
config.workflow.run_reconall = freesurfer
70+
wf = init_bold_wf(
71+
bold_series=bold_series,
72+
fieldmap_id=fieldmap_id,
73+
precomputed={},
74+
)
75+
76+
flatgraph = wf._create_flat_graph()
77+
generate_expanded_graph(flatgraph)
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
from pathlib import Path
2+
3+
import nibabel as nb
4+
import numpy as np
5+
import pytest
6+
from nipype.pipeline.engine.utils import generate_expanded_graph
7+
from niworkflows.utils.testing import generate_bids_skeleton
8+
9+
from .... import config
10+
from ...tests import mock_config
11+
from ...tests.test_base import BASE_LAYOUT
12+
from ..fit import init_bold_fit_wf, init_bold_native_wf
13+
14+
15+
@pytest.fixture(scope="module", autouse=True)
16+
def _quiet_logger():
17+
import logging
18+
19+
logger = logging.getLogger("nipype.workflow")
20+
old_level = logger.getEffectiveLevel()
21+
logger.setLevel(logging.ERROR)
22+
yield
23+
logger.setLevel(old_level)
24+
25+
26+
@pytest.fixture(scope="module")
27+
def bids_root(tmp_path_factory):
28+
base = tmp_path_factory.mktemp("boldfit")
29+
bids_dir = base / "bids"
30+
generate_bids_skeleton(bids_dir, BASE_LAYOUT)
31+
yield bids_dir
32+
33+
34+
def _make_params(
35+
have_hmcref: bool = True,
36+
have_coregref: bool = True,
37+
have_hmc_xfms: bool = True,
38+
have_boldref2fmap_xfm: bool = True,
39+
have_boldref2anat_xfm: bool = True,
40+
):
41+
return (
42+
have_hmcref,
43+
have_coregref,
44+
have_hmc_xfms,
45+
have_boldref2anat_xfm,
46+
have_boldref2fmap_xfm,
47+
)
48+
49+
50+
@pytest.mark.parametrize("task", ["rest", "nback"])
51+
@pytest.mark.parametrize("fieldmap_id", ["phasediff", None])
52+
@pytest.mark.parametrize(
53+
(
54+
'have_hmcref',
55+
'have_coregref',
56+
'have_hmc_xfms',
57+
'have_boldref2fmap_xfm',
58+
'have_boldref2anat_xfm',
59+
),
60+
[
61+
(True, True, True, True, True),
62+
(False, False, False, False, False),
63+
_make_params(have_hmcref=False),
64+
_make_params(have_hmc_xfms=False),
65+
_make_params(have_coregref=False),
66+
_make_params(have_coregref=False, have_boldref2fmap_xfm=False),
67+
_make_params(have_boldref2anat_xfm=False),
68+
],
69+
)
70+
def test_bold_fit_precomputes(
71+
bids_root: Path,
72+
tmp_path: Path,
73+
task: str,
74+
fieldmap_id: str | None,
75+
have_hmcref: bool,
76+
have_coregref: bool,
77+
have_hmc_xfms: bool,
78+
have_boldref2fmap_xfm: bool,
79+
have_boldref2anat_xfm: bool,
80+
):
81+
"""Test as many combinations of precomputed files and input
82+
configurations as possible."""
83+
output_dir = tmp_path / 'output'
84+
output_dir.mkdir()
85+
86+
img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4))
87+
88+
if task == 'rest':
89+
bold_series = [
90+
str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'),
91+
]
92+
elif task == 'nback':
93+
bold_series = [
94+
str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz')
95+
for i in range(1, 4)
96+
]
97+
98+
# The workflow will attempt to read file headers
99+
for path in bold_series:
100+
img.to_filename(path)
101+
102+
dummy_nifti = str(tmp_path / 'dummy.nii')
103+
dummy_affine = str(tmp_path / 'dummy.txt')
104+
img.to_filename(dummy_nifti)
105+
np.savetxt(dummy_affine, np.eye(4))
106+
107+
# Construct precomputed files
108+
precomputed = {'transforms': {}}
109+
if have_hmcref:
110+
precomputed['hmc_boldref'] = dummy_nifti
111+
if have_coregref:
112+
precomputed['coreg_boldref'] = dummy_nifti
113+
if have_hmc_xfms:
114+
precomputed['transforms']['hmc'] = dummy_affine
115+
if have_boldref2anat_xfm:
116+
precomputed['transforms']['boldref2anat'] = dummy_affine
117+
if have_boldref2fmap_xfm:
118+
precomputed['transforms']['boldref2fmap'] = dummy_affine
119+
120+
with mock_config(bids_dir=bids_root):
121+
wf = init_bold_fit_wf(
122+
bold_series=bold_series,
123+
precomputed=precomputed,
124+
fieldmap_id=fieldmap_id,
125+
omp_nthreads=1,
126+
)
127+
128+
flatgraph = wf._create_flat_graph()
129+
generate_expanded_graph(flatgraph)
130+
131+
132+
@pytest.mark.parametrize("task", ["rest", "nback"])
133+
@pytest.mark.parametrize("fieldmap_id", ["phasediff", None])
134+
@pytest.mark.parametrize("run_stc", [True, False])
135+
def test_bold_native_precomputes(
136+
bids_root: Path,
137+
tmp_path: Path,
138+
task: str,
139+
fieldmap_id: str | None,
140+
run_stc: bool,
141+
):
142+
"""Test as many combinations of precomputed files and input
143+
configurations as possible."""
144+
output_dir = tmp_path / 'output'
145+
output_dir.mkdir()
146+
147+
img = nb.Nifti1Image(np.zeros((10, 10, 10, 10)), np.eye(4))
148+
149+
if task == 'rest':
150+
bold_series = [
151+
str(bids_root / 'sub-01' / 'func' / 'sub-01_task-rest_run-1_bold.nii.gz'),
152+
]
153+
elif task == 'nback':
154+
bold_series = [
155+
str(bids_root / 'sub-01' / 'func' / f'sub-01_task-nback_echo-{i}_bold.nii.gz')
156+
for i in range(1, 4)
157+
]
158+
159+
# The workflow will attempt to read file headers
160+
for path in bold_series:
161+
img.to_filename(path)
162+
163+
with mock_config(bids_dir=bids_root):
164+
config.workflow.ignore = ['slicetiming'] if not run_stc else []
165+
wf = init_bold_native_wf(
166+
bold_series=bold_series,
167+
fieldmap_id=fieldmap_id,
168+
omp_nthreads=1,
169+
)
170+
171+
flatgraph = wf._create_flat_graph()
172+
generate_expanded_graph(flatgraph)

fmriprep/workflows/tests/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434

3535
@contextmanager
36-
def mock_config():
36+
def mock_config(bids_dir=None):
3737
"""Create a mock config for documentation and testing purposes."""
3838
from ... import config
3939

@@ -51,9 +51,13 @@ def mock_config():
5151
config.loggers.init()
5252
config.init_spaces()
5353

54+
bids_dir = bids_dir or data.load('tests/ds000005').absolute()
55+
5456
config.execution.work_dir = Path(mkdtemp())
55-
config.execution.bids_dir = data.load('tests/ds000005').absolute()
57+
config.execution.bids_dir = bids_dir
5658
config.execution.fmriprep_dir = Path(mkdtemp())
59+
config.execution.bids_database_dir = None
60+
config.execution._layout = None
5761
config.execution.init()
5862

5963
yield

0 commit comments

Comments
 (0)