Skip to content

Commit 19677a1

Browse files
committed
Repair 3dQwarp workflow
1 parent 08e198f commit 19677a1

File tree

2 files changed

+118
-43
lines changed

2 files changed

+118
-43
lines changed

sdcflows/workflows/fit/pepolar.py

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@
2121
# https://www.nipreps.org/community/licensing/
2222
#
2323
"""Datasets with multiple phase encoded directions."""
24-
from nipype.pipeline import engine as pe
2524
from nipype.interfaces import utility as niu
26-
25+
from nipype.pipeline import engine as pe
2726
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
2827

2928
from ... import data
@@ -32,7 +31,7 @@
3231
_PEPOLAR_DESC = """\
3332
A *B<sub>0</sub>*-nonuniformity map (or *fieldmap*) was estimated based on two (or more)
3433
echo-planar imaging (EPI) references """
35-
34+
_PEPOLAR_METHOD = "PEB/PEPOLAR (phase-encoding based / PE-POLARity)"
3635

3736
def init_topup_wf(
3837
grid_reference=0,
@@ -88,13 +87,13 @@ def init_topup_wf(
8887
8988
"""
9089
from nipype.interfaces.fsl.epi import TOPUP
91-
from niworkflows.interfaces.nibabel import MergeSeries, ReorientImage
9290
from niworkflows.interfaces.images import RobustAverage
91+
from niworkflows.interfaces.nibabel import MergeSeries, ReorientImage
9392

94-
from ...utils.misc import front as _front
95-
from ...interfaces.epi import GetReadoutTime, SortPEBlips
96-
from ...interfaces.utils import UniformGrid, PadSlices, ReorientImageAndMetadata
9793
from ...interfaces.bspline import TOPUPCoeffReorient
94+
from ...interfaces.epi import GetReadoutTime, SortPEBlips
95+
from ...interfaces.utils import PadSlices, ReorientImageAndMetadata, UniformGrid
96+
from ...utils.misc import front as _front
9897
from ..ancillary import init_brainextraction_wf
9998

10099
workflow = Workflow(name=name)
@@ -118,7 +117,7 @@ def init_topup_wf(
118117
),
119118
name="outputnode",
120119
)
121-
outputnode.inputs.method = "PEB/PEPOLAR (phase-encoding based / PE-POLARity)"
120+
outputnode.inputs.method = _PEPOLAR_METHOD
122121

123122
# Calculate the total readout time of each run
124123
readout_time = pe.MapNode(
@@ -238,13 +237,23 @@ def init_topup_wf(
238237
return workflow
239238

240239

241-
def init_3dQwarp_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
240+
def init_3dQwarp_wf(
241+
omp_nthreads=1,
242+
debug=False,
243+
sloppy=False,
244+
name="pepolar_estimate_wf"):
242245
"""
243246
Create the PEPOLAR field estimation workflow based on AFNI's ``3dQwarp``.
244247
245248
This workflow takes in two EPI files that MUST have opposed
246249
:abbr:`PE (phase-encoding)` direction.
247250
Therefore, EPIs with orthogonal PE directions are not supported.
251+
``3dQwarp`` is used to generate a displacement field and correct
252+
the reference image. The workflow also returns an estimated fieldmap,
253+
which is the result of converting the displacement field to a fieldmap
254+
and then regularizing it with a bspline field. This means that the unwarped
255+
image is in general not what one would get by reconstructing the fieldmap
256+
from fmap_coeff and warping the in_data.
248257
249258
Workflow Graph
250259
.. workflow ::
@@ -267,25 +276,46 @@ def init_3dQwarp_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
267276
------
268277
in_data : :obj:`list` of :obj:`str`
269278
A list of two EPI files, the first of which will be taken as reference.
279+
The reference PhaseEncodingDirection should match the value for
280+
in_reference.
281+
metadata : :obj:`list` of :obj:`dict`
282+
A list with length matching the length of in_data. Each element should be a
283+
dict with keys that are strings and values of any type. One key should be
284+
PhaseEncodingDirection and the values should be BIDS-valid codings.
270285
271286
Outputs
272287
-------
273288
fmap : :obj:`str`
274289
The path of the estimated fieldmap.
275290
fmap_ref : :obj:`str`
276-
The path of an unwarped conversion of the first element of ``in_data``.
291+
The path of an unwarped conversion of files in ``in_data``.
292+
fmap_mask : :obj:`str`
293+
The path of mask corresponding to the ``fmap_ref`` output.
294+
fmap_coeff : :obj:`str` or :obj:`list` of :obj:`str`
295+
The path(s) of the B-Spline coefficients supporting the fieldmap.
296+
method: :obj:`str`
297+
Short description of the estimation method that was run.
298+
out_warps: :obj:`str`
299+
The displacement field from 3dQwarp, in ANTS format.
277300
278301
"""
279302
from nipype.interfaces import afni
303+
from niworkflows.func.util import init_enhance_and_skullstrip_bold_wf
304+
from niworkflows.interfaces.fixes import FixHeaderRegistration as Registration
305+
from niworkflows.interfaces.freesurfer import StructuralReference
280306
from niworkflows.interfaces.header import CopyHeader
281-
from niworkflows.interfaces.fixes import (
282-
FixHeaderRegistration as Registration,
283-
FixHeaderApplyTransforms as ApplyTransforms,
307+
308+
from ...interfaces.bspline import (
309+
DEFAULT_HF_ZOOMS_MM,
310+
DEFAULT_ZOOMS_MM,
311+
ApplyCoeffsField,
312+
BSplineApprox,
284313
)
285-
from niworkflows.interfaces.freesurfer import StructuralReference
286-
from niworkflows.func.util import init_enhance_and_skullstrip_bold_wf
287-
from ...utils.misc import front as _front, last as _last
288-
from ...interfaces.utils import Flatten, ConvertWarp
314+
from ...interfaces.epi import GetReadoutTime
315+
from ...interfaces.fmap import DisplacementsField2Fieldmap
316+
from ...interfaces.utils import ConvertWarp, Flatten
317+
from ...utils.misc import front, last
318+
289319

290320
workflow = Workflow(name=name)
291321
workflow.__desc__ = f"""{_PEPOLAR_DESC} \
@@ -297,7 +327,22 @@ def init_3dQwarp_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
297327
)
298328

299329
outputnode = pe.Node(
300-
niu.IdentityInterface(fields=["fmap", "fmap_ref"]), name="outputnode"
330+
niu.IdentityInterface(
331+
fields=[
332+
"fmap",
333+
"fmap_ref",
334+
"fmap_mask",
335+
"fmap_coeff",
336+
"method",
337+
"out_warps"]),
338+
name="outputnode"
339+
)
340+
outputnode.inputs.method = _PEPOLAR_METHOD
341+
342+
readout_time = pe.Node(
343+
GetReadoutTime(),
344+
name="readout_time",
345+
run_without_submitting=True,
301346
)
302347

303348
flatten = pe.Node(Flatten(), name="flatten")
@@ -355,14 +400,24 @@ def init_3dQwarp_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
355400

356401
cphdr_warp = pe.Node(CopyHeader(), name="cphdr_warp", mem_gb=0.01)
357402

358-
unwarp_reference = pe.Node(
359-
ApplyTransforms(
360-
dimension=3,
361-
float=True,
362-
interpolation="LanczosWindowedSinc",
363-
),
364-
name="unwarp_reference",
403+
# Extract the corresponding fieldmap in Hz
404+
extract_field = pe.Node(
405+
DisplacementsField2Fieldmap(), name="extract_field"
406+
)
407+
408+
# Regularize with B-Splines
409+
bs_filter = pe.Node(
410+
BSplineApprox(debug=debug, extrapolate=not debug),
411+
name="bs_filter",
365412
)
413+
bs_filter.interface._always_run = debug
414+
bs_filter.inputs.bs_spacing = (
415+
[DEFAULT_HF_ZOOMS_MM] if not sloppy else [DEFAULT_ZOOMS_MM]
416+
)
417+
if sloppy:
418+
bs_filter.inputs.zooms_min = 4.0
419+
420+
unwarp = pe.Node(ApplyCoeffsField(), name="unwarp")
366421

367422
# fmt: off
368423
workflow.connect([
@@ -371,20 +426,32 @@ def init_3dQwarp_wf(omp_nthreads=1, debug=False, name="pepolar_estimate_wf"):
371426
(flatten, sort_pe, [("out_list", "inlist")]),
372427
(sort_pe, qwarp, [("qwarp_args", "args")]),
373428
(sort_pe, merge_pes, [("sorted", "in_files")]),
374-
(merge_pes, pe0_wf, [(("out_file", _front), "inputnode.in_file")]),
375-
(merge_pes, pe1_wf, [(("out_file", _last), "inputnode.in_file")]),
429+
(merge_pes, pe0_wf, [(("out_file", front), "inputnode.in_file")]),
430+
(merge_pes, pe1_wf, [(("out_file", last), "inputnode.in_file")]),
376431
(pe0_wf, align_pes, [("outputnode.skull_stripped_file", "fixed_image")]),
377432
(pe1_wf, align_pes, [("outputnode.skull_stripped_file", "moving_image")]),
378433
(pe0_wf, qwarp, [("outputnode.skull_stripped_file", "in_file")]),
379434
(align_pes, qwarp, [("warped_image", "base_file")]),
380-
(inputnode, cphdr_warp, [(("in_data", _front), "hdr_file")]),
435+
(inputnode, cphdr_warp, [(("in_data", front), "hdr_file")]),
381436
(qwarp, cphdr_warp, [("source_warp", "in_file")]),
382437
(cphdr_warp, to_ants, [("out_file", "in_file")]),
383-
(to_ants, unwarp_reference, [("out_file", "transforms")]),
384-
(inputnode, unwarp_reference, [("in_reference", "reference_image"),
385-
("in_reference", "input_image")]),
386-
(unwarp_reference, outputnode, [("output_image", "fmap_ref")]),
387-
(to_ants, outputnode, [("out_file", "fmap")]),
438+
(pe0_wf, extract_field, [("outputnode.skull_stripped_file", "epi")]),
439+
(to_ants, extract_field, [("out_file", "transform")]),
440+
(inputnode, readout_time, [(("metadata", front), "metadata")]),
441+
(readout_time, extract_field, [("readout_time", "ro_time"),
442+
("pe_direction", "pe_dir")]),
443+
(pe1_wf, unwarp, [("outputnode.skull_stripped_file", "in_data")]),
444+
(pe0_wf, bs_filter, [("outputnode.mask_file", "in_mask")]),
445+
(extract_field, bs_filter, [("out_file", "in_data")]),
446+
(bs_filter, unwarp, [("out_coeff", "in_coeff")]),
447+
(readout_time, unwarp, [("readout_time", "ro_time"),
448+
("pe_direction", "pe_dir")]),
449+
(bs_filter, outputnode, [("out_coeff", "fmap_coeff")]),
450+
(qwarp, outputnode, [("warped_source", "fmap_ref")]),
451+
(unwarp, outputnode, [("out_field", "fmap")]),
452+
(pe0_wf, outputnode, [("outputnode.mask_file", "fmap_mask")]),
453+
(to_ants, outputnode, [("out_file", "out_warps")])
454+
388455
])
389456
# fmt: on
390457
return workflow

sdcflows/workflows/fit/tests/test_pepolar.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
import pytest
2626
from nipype.pipeline import engine as pe
2727

28-
from ..pepolar import init_topup_wf
28+
from ..pepolar import init_3dQwarp_wf, init_topup_wf
2929

3030

3131
@pytest.mark.skipif(os.getenv("TRAVIS") == "true", reason="this is TravisCI")
3232
@pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") == "true", reason="this is GH Actions")
3333
@pytest.mark.parametrize("ds", ("ds001771", "HCP101006"))
34-
def test_topup_wf(tmpdir, bids_layouts, workdir, outdir, ds):
34+
@pytest.mark.parametrize("workflow", ("topup", "3dQwarp"))
35+
def test_pepolar_wf(tmpdir, bids_layouts, workdir, outdir, ds, workflow):
3536
"""Test preparation workflow."""
3637
layout = bids_layouts[ds]
3738
epi_path = sorted(
@@ -40,17 +41,24 @@ def test_topup_wf(tmpdir, bids_layouts, workdir, outdir, ds):
4041
)
4142
in_data = [f.path for f in epi_path]
4243

43-
wf = pe.Workflow(name=f"topup_{ds}")
44-
topup_wf = init_topup_wf(omp_nthreads=2, debug=True, sloppy=True)
44+
wf = pe.Workflow(name=f"{workflow}_{ds}")
45+
if workflow == "topup":
46+
init_pepolar = init_topup_wf
47+
elif workflow == "3dQwarp":
48+
init_pepolar = init_3dQwarp_wf
49+
else:
50+
msg = f"Unknown workflow: {workflow}"
51+
raise ValueError(msg)
52+
pepolar_wf = init_pepolar(omp_nthreads=2, debug=True, sloppy=True)
4553
metadata = [layout.get_metadata(f.path) for f in epi_path]
4654

47-
topup_wf.inputs.inputnode.in_data = in_data
48-
topup_wf.inputs.inputnode.metadata = metadata
55+
pepolar_wf.inputs.inputnode.in_data = in_data
56+
pepolar_wf.inputs.inputnode.metadata = metadata
4957

5058
if outdir:
5159
from ...outputs import init_fmap_derivatives_wf, init_fmap_reports_wf
5260

53-
outdir = outdir / "unittests" / f"topup_{ds}"
61+
outdir = outdir / "unittests" / f"{workflow}_{ds}"
5462
fmap_derivatives_wf = init_fmap_derivatives_wf(
5563
output_dir=str(outdir),
5664
write_coeff=True,
@@ -67,18 +75,18 @@ def test_topup_wf(tmpdir, bids_layouts, workdir, outdir, ds):
6775

6876
# fmt: off
6977
wf.connect([
70-
(topup_wf, fmap_reports_wf, [("outputnode.fmap", "inputnode.fieldmap"),
78+
(pepolar_wf, fmap_reports_wf, [("outputnode.fmap", "inputnode.fieldmap"),
7179
("outputnode.fmap_ref", "inputnode.fmap_ref"),
7280
("outputnode.fmap_mask", "inputnode.fmap_mask")]),
73-
(topup_wf, fmap_derivatives_wf, [
81+
(pepolar_wf, fmap_derivatives_wf, [
7482
("outputnode.fmap", "inputnode.fieldmap"),
7583
("outputnode.fmap_ref", "inputnode.fmap_ref"),
7684
("outputnode.fmap_coeff", "inputnode.fmap_coeff"),
7785
]),
7886
])
7987
# fmt: on
8088
else:
81-
wf.add_nodes([topup_wf])
89+
wf.add_nodes([pepolar_wf])
8290

8391
if workdir:
8492
wf.base_dir = str(workdir)

0 commit comments

Comments
 (0)