Skip to content

Commit f3939f8

Browse files
committed
ENH: Add flag for STC reference time
This ports over nipreps/fmriprep#2565, allowing for control of the slice timing reference used during correction. Additionally, add SliceTimingCorrected and StartTime fields to the BOLD metadata.
1 parent e53b4e0 commit f3939f8

File tree

4 files changed

+110
-53
lines changed

4 files changed

+110
-53
lines changed

nibabies/cli/parser.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ def _bids_filter(value):
6060
if value and Path(value).exists():
6161
return loads(Path(value).read_text(), object_hook=_filter_pybids_none_any)
6262

63+
def _slice_time_ref(value, parser):
64+
if value == "start":
65+
value = 0
66+
elif value == "middle":
67+
value = 0.5
68+
try:
69+
value = float(value)
70+
except ValueError:
71+
raise parser.error("Slice time reference must be number, 'start', or 'middle'. "
72+
f"Received {value}.")
73+
if not 0 <= value <= 1:
74+
raise parser.error(f"Slice time reference must be in range 0-1. Received {value}.")
75+
return value
76+
6377
verstr = f"NiBabies v{config.environment.version}"
6478
currentv = Version(config.environment.version)
6579
is_release = not any((currentv.is_devrelease, currentv.is_prerelease, currentv.is_postrelease))
@@ -71,6 +85,7 @@ def _bids_filter(value):
7185
PathExists = partial(_path_exists, parser=parser)
7286
IsFile = partial(_is_file, parser=parser)
7387
PositiveInt = partial(_min_one, parser=parser)
88+
SliceTimeRef = partial(_slice_time_ref, parser=parser)
7489

7590
parser.description = f"""
7691
NiBabies: Preprocessing workflows for infants v{config.environment.version}"""
@@ -303,6 +318,17 @@ def _bids_filter(value):
303318
help="Replace medial wall values with NaNs on functional GIFTI files. Only "
304319
"performed for GIFTI files mapped to a freesurfer subject (fsaverage or fsnative).",
305320
)
321+
g_conf.add_argument(
322+
"--slice-time-ref",
323+
required=False,
324+
action="store",
325+
default=None,
326+
type=SliceTimeRef,
327+
help="The time of the reference slice to correct BOLD values to, as a fraction "
328+
"acquisition time. 0 indicates the start, 0.5 the midpoint, and 1 the end "
329+
"of acquisition. The alias `start` corresponds to 0, and `middle` to 0.5. "
330+
"The default value is 0.5.",
331+
)
306332
g_conf.add_argument(
307333
"--dummy-scans",
308334
required=False,

nibabies/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,11 @@ class workflow(_Config):
521521
skull_strip_t1w = "force"
522522
"""Skip brain extraction of the T1w image (default is ``force``, meaning that
523523
*nibabies* will run brain extraction of the T1w)."""
524+
slice_time_ref = 0.5
525+
"""The time of the reference slice to correct BOLD values to, as a fraction
526+
acquisition time. 0 indicates the start, 0.5 the midpoint, and 1 the end
527+
of acquisition. The alias `start` corresponds to 0, and `middle` to 0.5.
528+
The default value is 0.5."""
524529
spaces = None
525530
"""Keeps the :py:class:`~niworkflows.utils.spaces.SpatialReferences`
526531
instance keeping standard and nonstandard spaces."""

nibabies/workflows/bold/outputs.py

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,60 @@
55
from nipype.pipeline import engine as pe
66
from nipype.interfaces import utility as niu
77

8-
from ...config import DEFAULT_MEMORY_MIN_GB
8+
from ... import config
99
from ...interfaces import DerivativesDataSink
1010

1111

1212
def prepare_timing_parameters(metadata):
13-
"""Convert initial timing metadata to post-realignment timing metadata
14-
13+
""" Convert initial timing metadata to post-realignment timing metadata
1514
In particular, SliceTiming metadata is invalid once STC or any realignment is applied,
1615
as a matrix of voxels no longer corresponds to an acquisition slice.
1716
Therefore, if SliceTiming is present in the metadata dictionary, and a sparse
1817
acquisition paradigm is detected, DelayTime or AcquisitionDuration must be derived to
1918
preserve the timing interpretation.
20-
2119
Examples
2220
--------
23-
21+
.. testsetup::
22+
>>> from unittest import mock
23+
If SliceTiming metadata is absent, then the only change is to note that
24+
STC has not been applied:
2425
>>> prepare_timing_parameters(dict(RepetitionTime=2))
25-
{'RepetitionTime': 2}
26+
{'RepetitionTime': 2, 'SliceTimingCorrected': False}
2627
>>> prepare_timing_parameters(dict(RepetitionTime=2, DelayTime=0.5))
27-
{'RepetitionTime': 2, 'DelayTime': 0.5}
28-
>>> prepare_timing_parameters(dict(RepetitionTime=2, SliceTiming=[0.0, 0.2, 0.4, 0.6]))
29-
{'RepetitionTime': 2, 'DelayTime': 1.2}
28+
{'RepetitionTime': 2, 'DelayTime': 0.5, 'SliceTimingCorrected': False}
3029
>>> prepare_timing_parameters(dict(VolumeTiming=[0.0, 1.0, 2.0, 5.0, 6.0, 7.0],
3130
... AcquisitionDuration=1.0))
32-
{'VolumeTiming': [0.0, 1.0, 2.0, 5.0, 6.0, 7.0], 'AcquisitionDuration': 1.0}
33-
>>> prepare_timing_parameters(dict(VolumeTiming=[0.0, 1.0, 2.0, 5.0, 6.0, 7.0],
34-
... SliceTiming=[0.0, 0.2, 0.4, 0.6, 0.8]))
35-
{'VolumeTiming': [0.0, 1.0, 2.0, 5.0, 6.0, 7.0], 'AcquisitionDuration': 1.0}
36-
31+
{'VolumeTiming': [0.0, 1.0, 2.0, 5.0, 6.0, 7.0], 'AcquisitionDuration': 1.0,
32+
'SliceTimingCorrected': False}
33+
When SliceTiming is available and used, then ``SliceTimingCorrected`` is ``True``
34+
and the ``StartTime`` indicates a series offset.
35+
>>> with mock.patch("fmriprep.config.workflow.ignore", []):
36+
... prepare_timing_parameters(dict(RepetitionTime=2, SliceTiming=[0.0, 0.2, 0.4, 0.6]))
37+
{'RepetitionTime': 2, 'SliceTimingCorrected': True, 'DelayTime': 1.2, 'StartTime': 0.3}
38+
>>> with mock.patch("fmriprep.config.workflow.ignore", []):
39+
... prepare_timing_parameters(dict(VolumeTiming=[0.0, 1.0, 2.0, 5.0, 6.0, 7.0],
40+
... SliceTiming=[0.0, 0.2, 0.4, 0.6, 0.8]))
41+
{'VolumeTiming': [0.0, 1.0, 2.0, 5.0, 6.0, 7.0], 'SliceTimingCorrected': True,
42+
'AcquisitionDuration': 1.0, 'StartTime': 0.4}
43+
When SliceTiming is available and not used, then ``SliceTimingCorrected`` is ``False``
44+
and TA is indicated with ``DelayTime`` or ``AcquisitionDuration``.
45+
>>> with mock.patch("fmriprep.config.workflow.ignore", ["slicetiming"]):
46+
... prepare_timing_parameters(dict(RepetitionTime=2, SliceTiming=[0.0, 0.2, 0.4, 0.6]))
47+
{'RepetitionTime': 2, 'SliceTimingCorrected': False, 'DelayTime': 1.2}
48+
>>> with mock.patch("fmriprep.config.workflow.ignore", ["slicetiming"]):
49+
... prepare_timing_parameters(dict(VolumeTiming=[0.0, 1.0, 2.0, 5.0, 6.0, 7.0],
50+
... SliceTiming=[0.0, 0.2, 0.4, 0.6, 0.8]))
51+
{'VolumeTiming': [0.0, 1.0, 2.0, 5.0, 6.0, 7.0], 'SliceTimingCorrected': False,
52+
'AcquisitionDuration': 1.0}
3753
"""
3854
timing_parameters = {
3955
key: metadata[key]
40-
for key in (
41-
"RepetitionTime",
42-
"VolumeTiming",
43-
"DelayTime",
44-
"AcquisitionDuration",
45-
"SliceTiming",
46-
)
47-
if key in metadata
48-
}
56+
for key in ("RepetitionTime", "VolumeTiming", "DelayTime",
57+
"AcquisitionDuration", "SliceTiming")
58+
if key in metadata}
59+
60+
run_stc = "SliceTiming" in metadata and 'slicetiming' not in config.workflow.ignore
61+
timing_parameters["SliceTimingCorrected"] = run_stc
4962

5063
if "SliceTiming" in timing_parameters:
5164
st = sorted(timing_parameters.pop("SliceTiming"))
@@ -58,6 +71,13 @@ def prepare_timing_parameters(metadata):
5871
# For variable TR paradigms, use AcquisitionDuration
5972
elif "VolumeTiming" in timing_parameters:
6073
timing_parameters["AcquisitionDuration"] = TA
74+
75+
if run_stc:
76+
first, last = st[0], st[-1]
77+
frac = config.workflow.slice_time_ref
78+
tzero = np.round(first + frac * (last - first), 3)
79+
timing_parameters["StartTime"] = tzero
80+
6181
return timing_parameters
6282

6383

@@ -162,7 +182,7 @@ def init_func_derivatives_wf(
162182
),
163183
name="ds_confounds",
164184
run_without_submitting=True,
165-
mem_gb=DEFAULT_MEMORY_MIN_GB,
185+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
166186
)
167187
ds_ref_t1w_xfm = pe.Node(
168188
DerivativesDataSink(
@@ -216,7 +236,7 @@ def init_func_derivatives_wf(
216236
),
217237
name="ds_bold_native",
218238
run_without_submitting=True,
219-
mem_gb=DEFAULT_MEMORY_MIN_GB,
239+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
220240
)
221241
ds_bold_native_ref = pe.Node(
222242
DerivativesDataSink(
@@ -227,7 +247,7 @@ def init_func_derivatives_wf(
227247
),
228248
name="ds_bold_native_ref",
229249
run_without_submitting=True,
230-
mem_gb=DEFAULT_MEMORY_MIN_GB,
250+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
231251
)
232252
ds_bold_mask_native = pe.Node(
233253
DerivativesDataSink(
@@ -239,7 +259,7 @@ def init_func_derivatives_wf(
239259
),
240260
name="ds_bold_mask_native",
241261
run_without_submitting=True,
242-
mem_gb=DEFAULT_MEMORY_MIN_GB,
262+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
243263
)
244264

245265
# fmt: off
@@ -268,7 +288,7 @@ def init_func_derivatives_wf(
268288
),
269289
name="ds_bold_t1",
270290
run_without_submitting=True,
271-
mem_gb=DEFAULT_MEMORY_MIN_GB,
291+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
272292
)
273293
ds_bold_t1_ref = pe.Node(
274294
DerivativesDataSink(
@@ -280,7 +300,7 @@ def init_func_derivatives_wf(
280300
),
281301
name="ds_bold_t1_ref",
282302
run_without_submitting=True,
283-
mem_gb=DEFAULT_MEMORY_MIN_GB,
303+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
284304
)
285305
ds_bold_mask_t1 = pe.Node(
286306
DerivativesDataSink(
@@ -293,7 +313,7 @@ def init_func_derivatives_wf(
293313
),
294314
name="ds_bold_mask_t1",
295315
run_without_submitting=True,
296-
mem_gb=DEFAULT_MEMORY_MIN_GB,
316+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
297317
)
298318

299319
# fmt: off
@@ -319,7 +339,7 @@ def init_func_derivatives_wf(
319339
),
320340
name="ds_bold_aseg_t1",
321341
run_without_submitting=True,
322-
mem_gb=DEFAULT_MEMORY_MIN_GB,
342+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
323343
)
324344
ds_bold_aparc_t1 = pe.Node(
325345
DerivativesDataSink(
@@ -332,7 +352,7 @@ def init_func_derivatives_wf(
332352
),
333353
name="ds_bold_aparc_t1",
334354
run_without_submitting=True,
335-
mem_gb=DEFAULT_MEMORY_MIN_GB,
355+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
336356
)
337357

338358
# fmt: off
@@ -351,7 +371,7 @@ def init_func_derivatives_wf(
351371
),
352372
name="ds_aroma_noise_ics",
353373
run_without_submitting=True,
354-
mem_gb=DEFAULT_MEMORY_MIN_GB,
374+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
355375
)
356376
ds_melodic_mix = pe.Node(
357377
DerivativesDataSink(
@@ -362,7 +382,7 @@ def init_func_derivatives_wf(
362382
),
363383
name="ds_melodic_mix",
364384
run_without_submitting=True,
365-
mem_gb=DEFAULT_MEMORY_MIN_GB,
385+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
366386
)
367387
ds_aroma_std = pe.Node(
368388
DerivativesDataSink(
@@ -375,7 +395,7 @@ def init_func_derivatives_wf(
375395
),
376396
name="ds_aroma_std",
377397
run_without_submitting=True,
378-
mem_gb=DEFAULT_MEMORY_MIN_GB,
398+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
379399
)
380400

381401
# fmt: off
@@ -406,7 +426,7 @@ def init_func_derivatives_wf(
406426
KeySelect(fields=["template", "bold_std", "bold_std_ref", "bold_mask_std"]),
407427
name="select_std",
408428
run_without_submitting=True,
409-
mem_gb=DEFAULT_MEMORY_MIN_GB,
429+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
410430
)
411431

412432
ds_bold_std = pe.Node(
@@ -420,7 +440,7 @@ def init_func_derivatives_wf(
420440
),
421441
name="ds_bold_std",
422442
run_without_submitting=True,
423-
mem_gb=DEFAULT_MEMORY_MIN_GB,
443+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
424444
)
425445
ds_bold_std_ref = pe.Node(
426446
DerivativesDataSink(
@@ -431,7 +451,7 @@ def init_func_derivatives_wf(
431451
),
432452
name="ds_bold_std_ref",
433453
run_without_submitting=True,
434-
mem_gb=DEFAULT_MEMORY_MIN_GB,
454+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
435455
)
436456
ds_bold_mask_std = pe.Node(
437457
DerivativesDataSink(
@@ -443,7 +463,7 @@ def init_func_derivatives_wf(
443463
),
444464
name="ds_bold_mask_std",
445465
run_without_submitting=True,
446-
mem_gb=DEFAULT_MEMORY_MIN_GB,
466+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
447467
)
448468

449469
# fmt: off
@@ -481,7 +501,7 @@ def init_func_derivatives_wf(
481501
KeySelect(fields=["bold_aseg_std", "bold_aparc_std", "template"]),
482502
name="select_fs_std",
483503
run_without_submitting=True,
484-
mem_gb=DEFAULT_MEMORY_MIN_GB,
504+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
485505
)
486506
ds_bold_aseg_std = pe.Node(
487507
DerivativesDataSink(
@@ -493,7 +513,7 @@ def init_func_derivatives_wf(
493513
),
494514
name="ds_bold_aseg_std",
495515
run_without_submitting=True,
496-
mem_gb=DEFAULT_MEMORY_MIN_GB,
516+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
497517
)
498518
ds_bold_aparc_std = pe.Node(
499519
DerivativesDataSink(
@@ -505,7 +525,7 @@ def init_func_derivatives_wf(
505525
),
506526
name="ds_bold_aparc_std",
507527
run_without_submitting=True,
508-
mem_gb=DEFAULT_MEMORY_MIN_GB,
528+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
509529
)
510530

511531
# fmt: off
@@ -538,7 +558,7 @@ def init_func_derivatives_wf(
538558
KeySelect(fields=["surfaces", "surf_kwargs"]),
539559
name="select_fs_surf",
540560
run_without_submitting=True,
541-
mem_gb=DEFAULT_MEMORY_MIN_GB,
561+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
542562
)
543563
select_fs_surf.iterables = [("key", fs_outputs)]
544564
select_fs_surf.inputs.surf_kwargs = [{"space": s} for s in fs_outputs]
@@ -560,7 +580,7 @@ def init_func_derivatives_wf(
560580
iterfield=["in_file", "hemi"],
561581
name="ds_bold_surfs",
562582
run_without_submitting=True,
563-
mem_gb=DEFAULT_MEMORY_MIN_GB,
583+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
564584
)
565585

566586
# fmt: off
@@ -588,7 +608,7 @@ def init_func_derivatives_wf(
588608
),
589609
name="ds_bold_cifti",
590610
run_without_submitting=True,
591-
mem_gb=DEFAULT_MEMORY_MIN_GB,
611+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
592612
)
593613

594614
# fmt: off
@@ -690,7 +710,7 @@ def init_bold_preproc_report_wf(mem_gb, reportlets_dir, name="bold_preproc_repor
690710
dismiss_entities=("echo",),
691711
),
692712
name="ds_report_bold",
693-
mem_gb=DEFAULT_MEMORY_MIN_GB,
713+
mem_gb=config.DEFAULT_MEMORY_MIN_GB,
694714
run_without_submitting=True,
695715
)
696716

0 commit comments

Comments
 (0)