Skip to content

Commit 4f9a1f2

Browse files
authored
RF: Filter fieldmaps based on whether they will be used to correct a BOLD series (#3025)
This PR takes a chunk out of the `init_func_preproc_wf` function and pre-runs it in `init_single_subject_wf` to filter out fieldmaps that are not needed to correct BOLD files. Closes #2968. Closes nipreps/sdcflows#359.
2 parents c56bec6 + b1fcd2f commit 4f9a1f2

File tree

3 files changed

+156
-17
lines changed

3 files changed

+156
-17
lines changed

fmriprep/workflows/base.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@
3636

3737
from nipype.interfaces import utility as niu
3838
from nipype.pipeline import engine as pe
39+
from niworkflows.utils.connections import listify
3940
from packaging.version import Version
4041

4142
from .. import config
4243
from ..interfaces import DerivativesDataSink
4344
from ..interfaces.reports import AboutSummary, SubjectSummary
44-
from .bold import init_func_preproc_wf
45+
from .bold.base import get_estimator, init_func_preproc_wf
4546

4647

4748
def init_fmriprep_wf():
@@ -418,6 +419,16 @@ def init_single_subject_wf(subject_id: str):
418419
)
419420
fmap_estimators = [f for f in fmap_estimators if f.method == fm.EstimatorType.ANAT]
420421

422+
# Do not calculate fieldmaps that we will not use
423+
if fmap_estimators:
424+
used_estimators = {
425+
key
426+
for bold_file in subject_data['bold']
427+
for key in get_estimator(config.execution.layout, listify(bold_file)[0])
428+
}
429+
430+
fmap_estimators = [fmap for fmap in fmap_estimators if fmap.bids_id in used_estimators]
431+
421432
if fmap_estimators:
422433
config.loggers.workflow.info(
423434
"B0 field inhomogeneity map will be estimated with "

fmriprep/workflows/bold/base.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -283,22 +283,7 @@ def init_func_preproc_wf(bold_file, has_fieldmap=False):
283283
config.loggers.workflow.info(sbref_msg)
284284

285285
if has_fieldmap:
286-
# First check if specified via B0FieldSource
287-
estimator_key = listify(metadata.get("B0FieldSource"))
288-
289-
if not estimator_key:
290-
import re
291-
from pathlib import Path
292-
293-
from sdcflows.fieldmaps import get_identifier
294-
295-
# Fallback to IntendedFor
296-
intended_rel = re.sub(
297-
r"^sub-[a-zA-Z0-9]*/",
298-
"",
299-
str(Path(bold_file if not multiecho else bold_file[0]).relative_to(layout.root)),
300-
)
301-
estimator_key = get_identifier(intended_rel)
286+
estimator_key = get_estimator(layout, bold_file if not multiecho else bold_file[0])
302287

303288
if not estimator_key:
304289
has_fieldmap = False
@@ -1356,3 +1341,21 @@ def get_img_orientation(imgf):
13561341
"""Return the image orientation as a string"""
13571342
img = nb.load(imgf)
13581343
return "".join(nb.aff2axcodes(img.affine))
1344+
1345+
1346+
def get_estimator(layout, fname):
1347+
field_source = layout.get_metadata(fname).get("B0FieldSource")
1348+
if isinstance(field_source, str):
1349+
field_source = (field_source,)
1350+
1351+
if field_source is None:
1352+
import re
1353+
from pathlib import Path
1354+
1355+
from sdcflows.fieldmaps import get_identifier
1356+
1357+
# Fallback to IntendedFor
1358+
intended_rel = re.sub(r"^sub-[a-zA-Z0-9]*/", "", str(Path(fname).relative_to(layout.root)))
1359+
field_source = get_identifier(intended_rel)
1360+
1361+
return field_source
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
from copy import deepcopy
2+
3+
import bids
4+
import pytest
5+
from niworkflows.utils.testing import generate_bids_skeleton
6+
from sdcflows.fieldmaps import clear_registry
7+
from sdcflows.utils.wrangler import find_estimators
8+
9+
from ..base import get_estimator
10+
11+
BASE_LAYOUT = {
12+
"01": {
13+
"anat": [{"suffix": "T1w"}],
14+
"func": [
15+
{
16+
"task": "rest",
17+
"run": i,
18+
"suffix": "bold",
19+
"metadata": {"PhaseEncodingDirection": "j", "TotalReadoutTime": 0.6},
20+
}
21+
for i in range(1, 3)
22+
],
23+
"fmap": [
24+
{"suffix": "phasediff", "metadata": {"EchoTime1": 0.005, "EchoTime2": 0.007}},
25+
{"suffix": "magnitude1", "metadata": {"EchoTime": 0.005}},
26+
{
27+
"suffix": "epi",
28+
"direction": "PA",
29+
"metadata": {"PhaseEncodingDirection": "j", "TotalReadoutTime": 0.6},
30+
},
31+
{
32+
"suffix": "epi",
33+
"direction": "AP",
34+
"metadata": {"PhaseEncodingDirection": "j-", "TotalReadoutTime": 0.6},
35+
},
36+
],
37+
},
38+
}
39+
40+
41+
def test_get_estimator_none(tmp_path):
42+
bids_dir = tmp_path / "bids"
43+
44+
# No IntendedFors/B0Fields
45+
generate_bids_skeleton(bids_dir, BASE_LAYOUT)
46+
layout = bids.BIDSLayout(bids_dir)
47+
bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file'))
48+
49+
assert get_estimator(layout, bold_files[0]) == ()
50+
assert get_estimator(layout, bold_files[1]) == ()
51+
52+
53+
def test_get_estimator_b0field_and_intendedfor(tmp_path):
54+
bids_dir = tmp_path / "bids"
55+
56+
# Set B0FieldSource for run 1
57+
spec = deepcopy(BASE_LAYOUT)
58+
spec['01']['func'][0]['metadata']['B0FieldSource'] = 'epi'
59+
spec['01']['fmap'][2]['metadata']['B0FieldIdentifier'] = 'epi'
60+
spec['01']['fmap'][3]['metadata']['B0FieldIdentifier'] = 'epi'
61+
62+
# Set IntendedFor for run 2
63+
spec['01']['fmap'][0]['metadata']['IntendedFor'] = 'func/sub-01_task-rest_run-2_bold.nii.gz'
64+
65+
generate_bids_skeleton(bids_dir, spec)
66+
layout = bids.BIDSLayout(bids_dir)
67+
estimators = find_estimators(layout=layout, subject='01')
68+
69+
bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file'))
70+
71+
assert get_estimator(layout, bold_files[0]) == ('epi',)
72+
assert get_estimator(layout, bold_files[1]) == ('auto_00000',)
73+
clear_registry()
74+
75+
76+
def test_get_estimator_overlapping_specs(tmp_path):
77+
bids_dir = tmp_path / "bids"
78+
79+
# Set B0FieldSource for both runs
80+
spec = deepcopy(BASE_LAYOUT)
81+
spec['01']['func'][0]['metadata']['B0FieldSource'] = 'epi'
82+
spec['01']['func'][1]['metadata']['B0FieldSource'] = 'epi'
83+
spec['01']['fmap'][2]['metadata']['B0FieldIdentifier'] = 'epi'
84+
spec['01']['fmap'][3]['metadata']['B0FieldIdentifier'] = 'epi'
85+
86+
# Set IntendedFor for both runs
87+
spec['01']['fmap'][0]['metadata']['IntendedFor'] = [
88+
'func/sub-01_task-rest_run-1_bold.nii.gz',
89+
'func/sub-01_task-rest_run-2_bold.nii.gz',
90+
]
91+
92+
generate_bids_skeleton(bids_dir, spec)
93+
layout = bids.BIDSLayout(bids_dir)
94+
estimators = find_estimators(layout=layout, subject='01')
95+
96+
bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file'))
97+
98+
# B0Fields take precedence
99+
assert get_estimator(layout, bold_files[0]) == ('epi',)
100+
assert get_estimator(layout, bold_files[1]) == ('epi',)
101+
clear_registry()
102+
103+
104+
def test_get_estimator_multiple_b0fields(tmp_path):
105+
bids_dir = tmp_path / "bids"
106+
107+
# Set B0FieldSource for both runs
108+
spec = deepcopy(BASE_LAYOUT)
109+
spec['01']['func'][0]['metadata']['B0FieldSource'] = ('epi', 'phasediff')
110+
spec['01']['func'][1]['metadata']['B0FieldSource'] = 'epi'
111+
spec['01']['fmap'][0]['metadata']['B0FieldIdentifier'] = 'phasediff'
112+
spec['01']['fmap'][1]['metadata']['B0FieldIdentifier'] = 'phasediff'
113+
spec['01']['fmap'][2]['metadata']['B0FieldIdentifier'] = 'epi'
114+
spec['01']['fmap'][3]['metadata']['B0FieldIdentifier'] = 'epi'
115+
116+
generate_bids_skeleton(bids_dir, spec)
117+
layout = bids.BIDSLayout(bids_dir)
118+
estimators = find_estimators(layout=layout, subject='01')
119+
120+
bold_files = sorted(layout.get(suffix='bold', extension='.nii.gz', return_type='file'))
121+
122+
# Always get an iterable; don't care if it's a list or tuple
123+
assert get_estimator(layout, bold_files[0]) == ['epi', 'phasediff']
124+
assert get_estimator(layout, bold_files[1]) == ('epi',)
125+
clear_registry()

0 commit comments

Comments
 (0)