Skip to content

Commit 278580c

Browse files
authored
Merge pull request #1803 from tsalo/multi-input-reference
FIX: Revise multi-echo reference generation, permitting using SBRefs too
2 parents 6c21510 + b4f9ed3 commit 278580c

File tree

2 files changed

+88
-45
lines changed

2 files changed

+88
-45
lines changed

fmriprep/workflows/bold/base.py

Lines changed: 83 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from nipype.pipeline import engine as pe
1818
from nipype.interfaces import utility as niu
1919

20+
from niworkflows.utils.connections import pop_file, listify
21+
22+
2023
from ...utils.meepi import combine_meepi_source
2124

2225
from ...interfaces import DerivativesDataSink
@@ -141,59 +144,66 @@ def init_func_preproc_wf(bold_file):
141144
from niworkflows.interfaces.utils import DictMerge
142145
from sdcflows.workflows.base import init_sdc_estimate_wf, fieldmap_wrangler
143146

144-
ref_file = bold_file
145147
mem_gb = {'filesize': 1, 'resampled': 1, 'largemem': 1}
146148
bold_tlen = 10
147-
multiecho = isinstance(bold_file, list)
148149

149150
# Have some options handy
150-
layout = config.execution.layout
151151
omp_nthreads = config.nipype.omp_nthreads
152152
freesurfer = config.workflow.run_reconall
153153
spaces = config.workflow.spaces
154154
output_dir = str(config.execution.output_dir)
155155

156+
# Extract BIDS entities and metadata from BOLD file(s)
157+
entities = extract_entities(bold_file)
158+
layout = config.execution.layout
159+
160+
# Take first file as reference
161+
ref_file = pop_file(bold_file)
162+
metadata = layout.get_metadata(ref_file)
163+
164+
echo_idxs = listify(entities.get("echo", []))
165+
multiecho = len(echo_idxs) > 2
166+
if len(echo_idxs) == 1:
167+
config.loggers.warning(
168+
f"Running a single echo <{ref_file}> from a seemingly multi-echo dataset."
169+
)
170+
bold_file = ref_file # Just in case - drop the list
171+
172+
if len(echo_idxs) == 2:
173+
raise RuntimeError(
174+
"Multi-echo processing requires at least three different echos (found two)."
175+
)
176+
156177
if multiecho:
157-
tes = [layout.get_metadata(echo)['EchoTime'] for echo in bold_file]
158-
ref_file = dict(zip(tes, bold_file))[min(tes)]
178+
# Drop echo entity for future queries, have a boolean shorthand
179+
entities.pop("echo", None)
180+
# reorder echoes from shortest to largest
181+
tes, bold_file = zip(*sorted([
182+
(layout.get_metadata(bf)["EchoTime"], bf) for bf in bold_file
183+
]))
184+
ref_file = bold_file[0] # Reset reference to be the shortest TE
159185

160186
if os.path.isfile(ref_file):
161187
bold_tlen, mem_gb = _create_mem_gb(ref_file)
162188

163189
wf_name = _get_wf_name(ref_file)
164190
config.loggers.workflow.debug(
165-
'Creating bold processing workflow for "%s" (%.2f GB / %d TRs). '
191+
'Creating bold processing workflow for <%s> (%.2f GB / %d TRs). '
166192
'Memory resampled/largemem=%.2f/%.2f GB.',
167193
ref_file, mem_gb['filesize'], bold_tlen, mem_gb['resampled'], mem_gb['largemem'])
168194

169-
sbref_file = None
170195
# Find associated sbref, if possible
171-
entities = layout.parse_file_entities(ref_file)
172196
entities['suffix'] = 'sbref'
173197
entities['extension'] = ['nii', 'nii.gz'] # Overwrite extensions
174-
files = layout.get(return_type='file', **entities)
175-
refbase = os.path.basename(ref_file)
176-
if 'sbref' in config.workflow.ignore:
177-
config.loggers.workflow.info("Single-band reference files ignored.")
178-
elif files and multiecho:
179-
config.loggers.workflow.warning(
180-
"Single-band reference found, but not supported in "
181-
"multi-echo workflows at this time. Ignoring.")
182-
elif files:
183-
sbref_file = files[0]
184-
sbbase = os.path.basename(sbref_file)
185-
if len(files) > 1:
186-
config.loggers.workflow.warning(
187-
"Multiple single-band reference files found for {}; using "
188-
"{}".format(refbase, sbbase))
189-
else:
190-
config.loggers.workflow.info("Using single-band reference file %s.",
191-
sbbase)
192-
else:
193-
config.loggers.workflow.info("No single-band-reference found for %s.",
194-
refbase)
198+
sbref_files = layout.get(return_type='file', **entities)
195199

196-
metadata = layout.get_metadata(ref_file)
200+
sbref_msg = f"No single-band-reference found for {os.path.basename(ref_file)}."
201+
if sbref_files and 'sbref' in config.workflow.ignore:
202+
sbref_msg = "Single-band reference file(s) found and ignored."
203+
elif sbref_files:
204+
sbref_msg = "Using single-band reference file(s) {}.".format(
205+
','.join([os.path.basename(sbf) for sbf in sbref_files]))
206+
config.loggers.workflow.info(sbref_msg)
197207

198208
# Find fieldmaps. Options: (phase1|phase2|phasediff|epi|fieldmap|syn)
199209
fmaps = None
@@ -234,9 +244,6 @@ def init_func_preproc_wf(bold_file):
234244
't1w2fsnative_xfm', 'fsnative2t1w_xfm']),
235245
name='inputnode')
236246
inputnode.inputs.bold_file = bold_file
237-
if sbref_file is not None:
238-
from niworkflows.interfaces.images import ValidateImage
239-
val_sbref = pe.Node(ValidateImage(in_file=sbref_file), name='val_sbref')
240247

241248
outputnode = pe.Node(niu.IdentityInterface(
242249
fields=['bold_t1', 'bold_t1_ref', 'bold_mask_t1', 'bold_aseg_t1', 'bold_aparc_t1',
@@ -296,12 +303,13 @@ def init_func_preproc_wf(bold_file):
296303
])
297304

298305
# Generate a tentative boldref
299-
bold_reference_wf = init_bold_reference_wf(omp_nthreads=omp_nthreads)
306+
bold_reference_wf = init_bold_reference_wf(
307+
omp_nthreads=omp_nthreads,
308+
bold_file=bold_file,
309+
sbref_files=sbref_files,
310+
multiecho=multiecho,
311+
)
300312
bold_reference_wf.inputs.inputnode.dummy_scans = config.workflow.dummy_scans
301-
if sbref_file is not None:
302-
workflow.connect([
303-
(val_sbref, bold_reference_wf, [('out_file', 'inputnode.sbref_file')]),
304-
])
305313

306314
# Top-level BOLD splitter
307315
bold_split = pe.Node(FSLSplit(dimension='t'), name='bold_split',
@@ -414,8 +422,6 @@ def init_func_preproc_wf(bold_file):
414422
workflow.connect([
415423
(inputnode, t1w_brain, [('t1w_preproc', 'in_file'),
416424
('t1w_mask', 'in_mask')]),
417-
# Generate early reference
418-
(inputnode, bold_reference_wf, [('bold_file', 'inputnode.bold_file')]),
419425
# BOLD buffer has slice-time corrected if it was run, original otherwise
420426
(boldbuffer, bold_split, [('bold_file', 'in_file')]),
421427
# HMC
@@ -889,3 +895,40 @@ def _to_join(in_file, join_file):
889895
return in_file
890896
res = JoinTSVColumns(in_file=in_file, join_file=join_file).run()
891897
return res.outputs.out_file
898+
899+
900+
def extract_entities(file_list):
901+
"""
902+
Return a dictionary of common entities given a list of files.
903+
904+
Examples
905+
--------
906+
>>> extract_entities('sub-01/anat/sub-01_T1w.nii.gz')
907+
{'subject': '01', 'suffix': 'T1w', 'datatype': 'anat', 'extension': 'nii.gz'}
908+
>>> extract_entities(['sub-01/anat/sub-01_T1w.nii.gz'] * 2)
909+
{'subject': '01', 'suffix': 'T1w', 'datatype': 'anat', 'extension': 'nii.gz'}
910+
>>> extract_entities(['sub-01/anat/sub-01_run-1_T1w.nii.gz',
911+
... 'sub-01/anat/sub-01_run-2_T1w.nii.gz'])
912+
{'subject': '01', 'run': [1, 2], 'suffix': 'T1w', 'datatype': 'anat',
913+
'extension': 'nii.gz'}
914+
915+
"""
916+
from collections import defaultdict
917+
from bids.layout import parse_file_entities
918+
919+
entities = defaultdict(list)
920+
for e, v in [
921+
ev_pair
922+
for f in listify(file_list)
923+
for ev_pair in parse_file_entities(f).items()
924+
]:
925+
entities[e].append(v)
926+
927+
def _unique(inlist):
928+
inlist = sorted(set(inlist))
929+
if len(inlist) == 1:
930+
return inlist[0]
931+
return inlist
932+
return {
933+
k: _unique(v) for k, v in entities.items()
934+
}

fmriprep/workflows/bold/t2s.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def init_bold_t2s_wf(echo_times, mem_gb, omp_nthreads,
3636
3737
Parameters
3838
----------
39-
echo_times : :obj:`list`
39+
echo_times : :obj:`list` or :obj:`tuple`
4040
list of TEs associated with each echo
4141
mem_gb : :obj:`float`
4242
Size of BOLD file in GB
@@ -60,12 +60,12 @@ def init_bold_t2s_wf(echo_times, mem_gb, omp_nthreads,
6060

6161
workflow = Workflow(name=name)
6262
workflow.__desc__ = """\
63-
A T2* map was estimated from the preprocessed BOLD by fitting to a monoexponential signal
64-
decay model with nonlinear regression, using T2*/S0 estimates from a log-linear
63+
A T2\\* map was estimated from the preprocessed BOLD by fitting to a monoexponential signal
64+
decay model with nonlinear regression, using T2\\*/S0 estimates from a log-linear
6565
regression fit as initial values.
6666
For each voxel, the maximal number of echoes with reliable signal in that voxel were
6767
used to fit the model.
68-
The calculated T2* map was then used to optimally combine preprocessed BOLD across
68+
The calculated T2\\* map was then used to optimally combine preprocessed BOLD across
6969
echoes following the method described in [@posse_t2s].
7070
The optimally combined time series was carried forward as the *preprocessed BOLD*.
7171
"""
@@ -76,7 +76,7 @@ def init_bold_t2s_wf(echo_times, mem_gb, omp_nthreads,
7676

7777
LOGGER.log(25, 'Generating T2* map and optimally combined ME-EPI time series.')
7878

79-
t2smap_node = pe.Node(T2SMap(echo_times=echo_times), name='t2smap_node')
79+
t2smap_node = pe.Node(T2SMap(echo_times=list(echo_times)), name='t2smap_node')
8080

8181
workflow.connect([
8282
(inputnode, t2smap_node, [('bold_file', 'in_files')]),

0 commit comments

Comments
 (0)