Skip to content

Commit 44fec84

Browse files
authored
Merge pull request #140 from josephmje/enh/apply_topup
ENH: Connect fieldmap estimation to preprocessing pipeline of individual DWI runs
2 parents aa16cd9 + 9371c49 commit 44fec84

File tree

3 files changed

+155
-51
lines changed

3 files changed

+155
-51
lines changed

dmriprep/workflows/base.py

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def init_single_subject_wf(subject_id):
287287
return workflow
288288

289289
from .dwi.base import init_dwi_preproc_wf
290+
290291
# Append the dMRI section to the existing anatomical excerpt
291292
# That way we do not need to stream down the number of DWI datasets
292293
anat_preproc_wf.__postdesc__ = (
@@ -301,61 +302,86 @@ def init_single_subject_wf(subject_id):
301302
"""
302303
)
303304

305+
# SDC Step 0: Determine whether fieldmaps can/should be estimated
306+
fmap_estimators = None
307+
if "fieldmap" not in config.workflow.ignore:
308+
from sdcflows import fieldmaps as fm
309+
from sdcflows.utils.wrangler import find_estimators
310+
from sdcflows.workflows.base import init_fmap_preproc_wf
311+
312+
# SDC Step 1: Run basic heuristics to identify available data for fieldmap estimation
313+
fmap_estimators = find_estimators(config.execution.layout)
314+
315+
# Add fieldmap-less estimators
316+
if not fmap_estimators and config.workflow.use_syn:
317+
# estimators = [fm.FieldmapEstimation()]
318+
raise NotImplementedError
319+
320+
# Nuts and bolts: initialize individual run's pipeline
321+
dwi_preproc_list = []
304322
for dwi_file in subject_data["dwi"]:
305-
dwi_preproc_wf = init_dwi_preproc_wf(dwi_file)
323+
dwi_preproc_wf = init_dwi_preproc_wf(
324+
dwi_file,
325+
has_fieldmap=bool(fmap_estimators),
326+
)
306327

307328
# fmt: off
308329
workflow.connect([
309-
(anat_preproc_wf, dwi_preproc_wf,
310-
[("outputnode.t1w_preproc", "inputnode.t1w_preproc"),
311-
("outputnode.t1w_mask", "inputnode.t1w_mask"),
312-
("outputnode.t1w_dseg", "inputnode.t1w_dseg"),
313-
("outputnode.t1w_aseg", "inputnode.t1w_aseg"),
314-
("outputnode.t1w_aparc", "inputnode.t1w_aparc"),
315-
("outputnode.t1w_tpms", "inputnode.t1w_tpms"),
316-
("outputnode.template", "inputnode.template"),
317-
("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"),
318-
("outputnode.std2anat_xfm", "inputnode.std2anat_xfm"),
319-
# Undefined if --fs-no-reconall, but this is safe
320-
("outputnode.subjects_dir", "inputnode.subjects_dir"),
321-
("outputnode.t1w2fsnative_xfm", "inputnode.t1w2fsnative_xfm"),
322-
("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm")]),
330+
(anat_preproc_wf, dwi_preproc_wf, [
331+
("outputnode.t1w_preproc", "inputnode.t1w_preproc"),
332+
("outputnode.t1w_mask", "inputnode.t1w_mask"),
333+
("outputnode.t1w_dseg", "inputnode.t1w_dseg"),
334+
("outputnode.t1w_aseg", "inputnode.t1w_aseg"),
335+
("outputnode.t1w_aparc", "inputnode.t1w_aparc"),
336+
("outputnode.t1w_tpms", "inputnode.t1w_tpms"),
337+
("outputnode.template", "inputnode.template"),
338+
("outputnode.anat2std_xfm", "inputnode.anat2std_xfm"),
339+
("outputnode.std2anat_xfm", "inputnode.std2anat_xfm"),
340+
# Undefined if --fs-no-reconall, but this is safe
341+
("outputnode.subjects_dir", "inputnode.subjects_dir"),
342+
("outputnode.t1w2fsnative_xfm", "inputnode.t1w2fsnative_xfm"),
343+
("outputnode.fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm"),
344+
]),
323345
(bids_info, dwi_preproc_wf, [("subject", "inputnode.subject_id")]),
324346
])
325347
# fmt: on
326348

327-
if "fieldmap" in config.workflow.ignore:
328-
return workflow
329-
330-
from sdcflows import fieldmaps as fm
331-
from sdcflows.utils.wrangler import find_estimators
332-
from sdcflows.workflows.base import init_fmap_preproc_wf
349+
# Keep a handle to each workflow
350+
dwi_preproc_list.append(dwi_preproc_wf)
333351

334-
# SDCFlows connection
335-
# Step 1: Run basic heuristics to identify available data for fieldmap estimation
336-
estimators = find_estimators(config.execution.layout)
337-
338-
if not estimators and config.workflow.use_syn: # Add fieldmap-less estimators
339-
# estimators = [fm.FieldmapEstimation()]
340-
raise NotImplementedError
352+
if not fmap_estimators:
353+
return workflow
341354

342-
# Step 2: Manually add further estimators (e.g., fieldmap-less)
355+
# SDC Step 2: Manually add further estimators (e.g., fieldmap-less)
343356
fmap_wf = init_fmap_preproc_wf(
344357
debug=config.execution.debug,
345-
estimators=estimators,
358+
estimators=fmap_estimators,
346359
omp_nthreads=config.nipype.omp_nthreads,
347360
output_dir=str(output_dir),
348361
subject=subject_id,
349362
)
350363

364+
# TODO: Requires nipreps/sdcflows#147
365+
for dwi_preproc_wf in dwi_preproc_list:
366+
# fmt: off
367+
workflow.connect([
368+
(fmap_wf, dwi_preproc_wf, [
369+
("outputnode.fmap", "inputnode.fmap"),
370+
("outputnode.fmap_ref", "inputnode.fmap_ref"),
371+
("outputnode.fmap_coeff", "inputnode.fmap_coeff"),
372+
("outputnode.fmap_mask", "inputnode.fmap_mask"),
373+
("outputnode.fmap_id", "inputnode.fmap_id"),
374+
]),
375+
])
376+
# fmt: on
377+
351378
# Overwrite ``out_path_base`` of sdcflows's DataSinks
352379
for node in fmap_wf.list_node_names():
353380
if node.split(".")[-1].startswith("ds_"):
354381
fmap_wf.get_node(node).interface.out_path_base = "dmriprep"
355-
workflow.add_nodes([fmap_wf])
356382

357383
# Step 3: Manually connect PEPOLAR
358-
for estimator in estimators:
384+
for estimator in fmap_estimators:
359385
if estimator.method != fm.EstimatorType.PEPOLAR:
360386
continue
361387

@@ -372,18 +398,18 @@ def init_single_subject_wf(subject_id):
372398
raise NotImplementedError
373399
# from niworkflows.interfaces.utility import KeySelect
374400
# est_id = estimator.bids_id
375-
# fmap_select = pe.MapNode(
401+
# estim_select = pe.MapNode(
376402
# KeySelect(fields=["metadata", "dwi_reference", "dwi_mask", "gradients_rasb",]),
377403
# name=f"fmap_select_{est_id}",
378404
# run_without_submitting=True,
379405
# iterfields=["key"]
380406
# )
381-
# fmap_select.inputs.key = [
407+
# estim_select.inputs.key = [
382408
# str(s.path) for s in estimator.sources if s.suffix in ("epi", "dwi", "sbref")
383409
# ]
384410
# # fmt:off
385411
# workflow.connect([
386-
# (referencenode, fmap_select, [("dwi_file", "keys"),
412+
# (referencenode, estim_select, [("dwi_file", "keys"),
387413
# ("metadata", "metadata"),
388414
# ("dwi_reference", "dwi_reference"),
389415
# ("gradients_rasb", "gradients_rasb")]),

dmriprep/workflows/dwi/base.py

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from ...interfaces import DerivativesDataSink
99

1010

11-
def init_dwi_preproc_wf(dwi_file):
11+
def init_dwi_preproc_wf(dwi_file, has_fieldmap=False):
1212
"""
1313
Build a preprocessing workflow for one DWI run.
1414
@@ -30,6 +30,8 @@ def init_dwi_preproc_wf(dwi_file):
3030
----------
3131
dwi_file : :obj:`os.PathLike`
3232
One diffusion MRI dataset to be processed.
33+
has_fieldmap : :obj:`bool`
34+
Build the workflow with a path to register a fieldmap to the DWI.
3335
3436
Inputs
3537
------
@@ -39,6 +41,17 @@ def init_dwi_preproc_wf(dwi_file):
3941
File path of the b-vectors
4042
in_bval
4143
File path of the b-values
44+
fmap
45+
File path of the fieldmap
46+
fmap_ref
47+
File path of the fieldmap reference
48+
fmap_coeff
49+
File path of the fieldmap coefficients
50+
fmap_mask
51+
File path of the fieldmap mask
52+
fmap_id
53+
The BIDS modality label of the fieldmap being used
54+
4255
4356
Outputs
4457
-------
@@ -67,6 +80,16 @@ def init_dwi_preproc_wf(dwi_file):
6780
f"Creating DWI preprocessing workflow for <{dwi_file.name}>"
6881
)
6982

83+
if has_fieldmap:
84+
from sdcflows.fieldmaps import get_identifier
85+
86+
estimator_key = get_identifier(dwi_file)
87+
if not estimator_key:
88+
has_fieldmap = False
89+
config.loggers.workflow.critical(
90+
f"None of the available B0 fieldmaps are associated to <{dwi_file}>"
91+
)
92+
7093
# Build workflow
7194
workflow = Workflow(name=_get_wf_name(dwi_file.name))
7295

@@ -77,6 +100,12 @@ def init_dwi_preproc_wf(dwi_file):
77100
"dwi_file",
78101
"in_bvec",
79102
"in_bval",
103+
# From SDCFlows
104+
"fmap",
105+
"fmap_ref",
106+
"fmap_coeff",
107+
"fmap_mask",
108+
"fmap_id",
80109
# From anatomical
81110
"t1w_preproc",
82111
"t1w_mask",
@@ -111,20 +140,16 @@ def init_dwi_preproc_wf(dwi_file):
111140
)
112141

113142
# MAIN WORKFLOW STRUCTURE
114-
# fmt:off
143+
# fmt: off
115144
workflow.connect([
116145
(inputnode, gradient_table, [("dwi_file", "dwi_file"),
117146
("in_bvec", "in_bvec"),
118147
("in_bval", "in_bval")]),
119148
(inputnode, dwi_reference_wf, [("dwi_file", "inputnode.dwi_file")]),
120149
(gradient_table, dwi_reference_wf, [("b0_ixs", "inputnode.b0_ixs")]),
121-
(dwi_reference_wf, outputnode, [
122-
("outputnode.ref_image", "dwi_reference"),
123-
("outputnode.dwi_mask", "dwi_mask"),
124-
]),
125150
(gradient_table, outputnode, [("out_rasb", "gradients_rasb")]),
126151
])
127-
# fmt:on
152+
# fmt: on
128153

129154
if config.workflow.run_reconall:
130155
from niworkflows.interfaces.nibabel import ApplyMask
@@ -142,8 +167,7 @@ def init_dwi_preproc_wf(dwi_file):
142167

143168
ds_report_reg = pe.Node(
144169
DerivativesDataSink(
145-
base_directory=str(config.execution.output_dir),
146-
datatype="figures",
170+
base_directory=str(config.execution.output_dir), datatype="figures",
147171
),
148172
name="ds_report_reg",
149173
run_without_submitting=True,
@@ -152,7 +176,7 @@ def init_dwi_preproc_wf(dwi_file):
152176
def _bold_reg_suffix(fallback):
153177
return "coreg" if fallback else "bbregister"
154178

155-
# fmt:off
179+
# fmt: off
156180
workflow.connect([
157181
(inputnode, bbr_wf, [
158182
("fsnative2t1w_xfm", "inputnode.fsnative2t1w_xfm"),
@@ -171,20 +195,74 @@ def _bold_reg_suffix(fallback):
171195
('outputnode.out_report', 'in_file'),
172196
(('outputnode.fallback', _bold_reg_suffix), 'desc')]),
173197
])
174-
# fmt:on
198+
# fmt: on
175199

176200
# REPORTING ############################################################
177201
reportlets_wf = init_reportlets_wf(str(config.execution.output_dir))
178-
# fmt:off
202+
# fmt: off
179203
workflow.connect([
180204
(inputnode, reportlets_wf, [("dwi_file", "inputnode.source_file")]),
181205
(dwi_reference_wf, reportlets_wf, [
182-
("outputnode.ref_image", "inputnode.dwi_ref"),
183-
("outputnode.dwi_mask", "inputnode.dwi_mask"),
184206
("outputnode.validation_report", "inputnode.validation_report"),
185207
]),
208+
(outputnode, reportlets_wf, [
209+
("dwi_reference", "inputnode.dwi_ref"),
210+
("dwi_mask", "inputnode.dwi_mask"),
211+
]),
212+
])
213+
# fmt: on
214+
215+
if not has_fieldmap:
216+
# fmt: off
217+
workflow.connect([
218+
(dwi_reference_wf, outputnode, [("outputnode.ref_image", "dwi_reference"),
219+
("outputnode.dwi_mask", "dwi_mask")]),
220+
])
221+
# fmt: on
222+
return workflow
223+
224+
from niworkflows.interfaces.utility import KeySelect
225+
from sdcflows.workflows.apply.registration import init_coeff2epi_wf
226+
from sdcflows.workflows.apply.correction import init_unwarp_wf
227+
228+
coeff2epi_wf = init_coeff2epi_wf(
229+
omp_nthreads=config.nipype.omp_nthreads, write_coeff=True
230+
)
231+
unwarp_wf = init_unwarp_wf(omp_nthreads=config.nipype.omp_nthreads)
232+
unwarp_wf.inputs.inputnode.metadata = layout.get_metadata(str(dwi_file))
233+
234+
output_select = pe.Node(
235+
KeySelect(fields=["fmap", "fmap_ref", "fmap_coeff", "fmap_mask"]),
236+
name="output_select",
237+
run_without_submitting=True,
238+
)
239+
output_select.inputs.key = estimator_key[0]
240+
if len(estimator_key) > 1:
241+
config.loggers.workflow.warning(
242+
f"Several fieldmaps <{', '.join(estimator_key)}> are "
243+
f"'IntendedFor' <{dwi_file}>, using {estimator_key[0]}"
244+
)
245+
246+
# fmt: off
247+
workflow.connect([
248+
(inputnode, output_select, [("fmap", "fmap"),
249+
("fmap_ref", "fmap_ref"),
250+
("fmap_coeff", "fmap_coeff"),
251+
("fmap_mask", "fmap_mask"),
252+
("fmap_id", "keys")]),
253+
(output_select, coeff2epi_wf, [
254+
("fmap_ref", "inputnode.fmap_ref"),
255+
("fmap_coeff", "inputnode.fmap_coeff"),
256+
("fmap_mask", "inputnode.fmap_mask")]),
257+
(dwi_reference_wf, coeff2epi_wf, [
258+
("outputnode.ref_image", "inputnode.target_ref"),
259+
("outputnode.dwi_mask", "inputnode.target_mask")]),
260+
(dwi_reference_wf, unwarp_wf, [("outputnode.ref_image", "inputnode.distorted")]),
261+
(coeff2epi_wf, unwarp_wf, [
262+
("outputnode.fmap_coeff", "inputnode.fmap_coeff")]),
263+
(unwarp_wf, outputnode, [("outputnode.corrected", "dwi_reference")]),
186264
])
187-
# fmt:on
265+
# fmt: on
188266

189267
return workflow
190268

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ install_requires =
2929
numpy
3030
pybids >= 0.11.1
3131
pyyaml
32-
sdcflows >= 2.0.0rc2
32+
sdcflows @ git+https://github.com/nipreps/sdcflows.git@9b5c167d81e7670ef2121151b2d59fe190c0167c
3333
smriprep >= 0.8.0rc0
3434
templateflow ~= 0.6
3535
toml

0 commit comments

Comments
 (0)