Skip to content

Commit f3464b5

Browse files
authored
feat: Add flag to fallback to Estimated* metadata or a passed value for TotalReadoutTime (#3423)
This flag provides two additional ways to retrieve TotalReadoutTime. If `EstimatedTotalReadoutTime` or `EstimatedEffectiveEchoSpacing` are defined, the user can pass `--fallback-total-readout-time estimated` to direct fMRIPrep to use these. This could be turned on by default, but I don't think this is prudent. The reason that `dcm2niix` does not simply say `TotalReadoutTime` or `EffectiveEchoSpacing` is not to misrepresent the confidence that these are correct. Previously, users had to actively remove the `Estimated` to accept the estimates, and I don't think we want to eliminate the conscious decision. However, we can make it so that users do not need to modify their datasets to accept them. Closes #3009.
2 parents 9da3867 + da9fc85 commit f3464b5

File tree

9 files changed

+109
-39
lines changed

9 files changed

+109
-39
lines changed

.circleci/config.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,6 @@ jobs:
486486
487487
# Inject pretend metadata
488488
json_sidecar=/tmp/data/${DATASET}/task-mixedgamblestask_bold.json
489-
awk 'NR==1{print; print " \"TotalReadoutTime\": 0.05,"} NR!=1' ${json_sidecar} > tmp && mv tmp ${json_sidecar}
490489
awk 'NR==1{print; print " \"PhaseEncodingDirection\": \"j\","} NR!=1' ${json_sidecar} > tmp && mv tmp ${json_sidecar}
491490
492491
fmriprep-docker -i nipreps/fmriprep:latest \
@@ -530,7 +529,8 @@ jobs:
530529
/tmp/data/${DATASET} /tmp/${DATASET}/fmriprep-partial participant \
531530
--fs-subjects-dir /tmp/${DATASET}/freesurfer \
532531
${FASTRACK_ARG} \
533-
--sloppy --write-graph --use-syn-sdc --mem-mb 14336 \
532+
--use-syn-sdc --fallback-total-readout-time 0.03125 \
533+
--sloppy --write-graph --mem-mb 14336 \
534534
--output-spaces MNI152NLin2009cAsym fsaverage5 fsnative MNI152NLin6Asym anat \
535535
--nthreads 4 --cifti-output --project-goodvoxels -vv
536536
- store_artifacts:
@@ -745,7 +745,7 @@ jobs:
745745
# Inject pretend metadata for SDCFlows not to crash
746746
# TODO / open question - do all echos need the metadata?
747747
chmod +w /tmp/data/${DATASET}
748-
echo '{"PhaseEncodingDirection": "j", "TotalReadoutTime": 0.058}' >> /tmp/data/${DATASET}/task-cuedSGT_bold.json
748+
echo '{"PhaseEncodingDirection": "j"}' >> /tmp/data/${DATASET}/task-cuedSGT_bold.json
749749
chmod -R -w /tmp/data/${DATASET}
750750
751751
fmriprep-docker -i nipreps/fmriprep:latest \
@@ -754,7 +754,8 @@ jobs:
754754
/tmp/data/${DATASET} /tmp/${DATASET}/fmriprep participant \
755755
${FASTRACK_ARG} \
756756
--me-output-echos \
757-
--fs-no-reconall --use-syn-sdc --ignore slicetiming \
757+
--fs-no-reconall --ignore slicetiming \
758+
--use-syn-sdc --fallback-total-readout-time 0.0625 \
758759
--dummy-scans 1 --sloppy --write-graph \
759760
--output-spaces MNI152NLin2009cAsym \
760761
--mem-mb 14336 --nthreads 4 -vv

fmriprep/cli/parser.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ def _slice_time_ref(value, parser):
149149
raise parser.error(f'Slice time reference must be in range 0-1. Received {value}.')
150150
return value
151151

152+
def _fallback_trt(value, parser):
153+
if value == 'estimated':
154+
return value
155+
try:
156+
return float(value)
157+
except ValueError:
158+
raise parser.error(
159+
f'Falling back to TRT must be a number or "estimated". Received {value}.'
160+
) from None
161+
152162
verstr = f'fMRIPrep v{config.environment.version}'
153163
currentv = Version(config.environment.version)
154164
is_release = not any((currentv.is_devrelease, currentv.is_prerelease, currentv.is_postrelease))
@@ -163,6 +173,7 @@ def _slice_time_ref(value, parser):
163173
PositiveInt = partial(_min_one, parser=parser)
164174
BIDSFilter = partial(_bids_filter, parser=parser)
165175
SliceTimeRef = partial(_slice_time_ref, parser=parser)
176+
FallbackTRT = partial(_fallback_trt, parser=parser)
166177

167178
# Arguments as specified by BIDS-Apps
168179
# required, positional arguments
@@ -417,6 +428,15 @@ def _slice_time_ref(value, parser):
417428
type=int,
418429
help='Number of nonsteady-state volumes. Overrides automatic detection.',
419430
)
431+
g_conf.add_argument(
432+
'--fallback-total-readout-time',
433+
required=False,
434+
action='store',
435+
default=None,
436+
type=FallbackTRT,
437+
help='Fallback value for Total Readout Time (TRT) calculation. '
438+
'May be a number or "estimated".',
439+
)
420440
g_conf.add_argument(
421441
'--random-seed',
422442
dest='_random_seed',

fmriprep/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,9 @@ class workflow(_Config):
575575
"""Remove the mean from fieldmaps."""
576576
force_syn = None
577577
"""Run *fieldmap-less* susceptibility-derived distortions estimation."""
578+
fallback_total_readout_time = None
579+
"""Infer the total readout time if unavailable from authoritative metadata.
580+
This may be a number or the string "estimated"."""
578581
hires = None
579582
"""Run FreeSurfer ``recon-all`` with the ``-hires`` flag."""
580583
fs_no_resume = None

fmriprep/interfaces/resampling.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ class ResampleSeriesInputSpec(TraitedSpec):
3030
ref_file = File(exists=True, mandatory=True, desc='File to resample in_file to')
3131
transforms = InputMultiObject(
3232
File(exists=True),
33-
mandatory=True,
3433
desc='Transform files, from in_file to ref_file (image mode)',
3534
)
3635
inverse = InputMultiObject(
@@ -92,7 +91,8 @@ def _run_interface(self, runtime):
9291

9392
nvols = source.shape[3] if source.ndim > 3 else 1
9493

95-
transforms = load_transforms(self.inputs.transforms, self.inputs.inverse)
94+
# No transforms appear Undefined, pass as empty list
95+
transforms = load_transforms(self.inputs.transforms or [], self.inputs.inverse)
9696

9797
pe_dir = self.inputs.pe_dir
9898
ro_time = self.inputs.ro_time
@@ -191,6 +191,13 @@ def _run_interface(self, runtime):
191191
class DistortionParametersInputSpec(TraitedSpec):
192192
in_file = File(exists=True, desc='EPI image corresponding to the metadata')
193193
metadata = traits.Dict(mandatory=True, desc='metadata corresponding to the inputs')
194+
fallback = traits.Either(
195+
None,
196+
'estimated',
197+
traits.Float,
198+
usedefault=True,
199+
desc='Fallback value for missing metadata',
200+
)
194201

195202

196203
class DistortionParametersOutputSpec(TraitedSpec):
@@ -215,6 +222,8 @@ def _run_interface(self, runtime):
215222
self._results['readout_time'] = get_trt(
216223
self.inputs.metadata,
217224
self.inputs.in_file or None,
225+
use_estimate=self.inputs.fallback == 'estimated',
226+
fallback=self.inputs.fallback if isinstance(self.inputs.fallback, float) else None,
218227
)
219228
self._results['pe_direction'] = self.inputs.metadata['PhaseEncodingDirection']
220229
except (KeyError, ValueError):

fmriprep/workflows/base.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -614,13 +614,27 @@ def init_single_subject_wf(subject_id: str):
614614
from sdcflows import fieldmaps as fm
615615
from sdcflows.workflows.base import init_fmap_preproc_wf
616616

617-
fmap_wf = init_fmap_preproc_wf(
618-
debug='fieldmaps' in config.execution.debug,
619-
estimators=fmap_estimators,
620-
omp_nthreads=omp_nthreads,
621-
output_dir=fmriprep_dir,
622-
subject=subject_id,
623-
)
617+
fallback_trt = config.workflow.fallback_total_readout_time
618+
try:
619+
fmap_wf = init_fmap_preproc_wf(
620+
use_metadata_estimates=fallback_trt == 'estimated',
621+
fallback_total_readout_time=fallback_trt
622+
if isinstance(fallback_trt, float)
623+
else None,
624+
debug='fieldmaps' in config.execution.debug,
625+
estimators=fmap_estimators,
626+
omp_nthreads=omp_nthreads,
627+
output_dir=fmriprep_dir,
628+
subject=subject_id,
629+
)
630+
except RuntimeError:
631+
message = (
632+
'Missing readout time information. '
633+
'See documentation for `--fallback-total-readout-time`.'
634+
)
635+
config.loggers.workflow.critical(message, exc_info=True)
636+
sys.exit(os.EX_DATAERR)
637+
624638
fmap_wf.__desc__ = f"""
625639
626640
Preprocessing of B<sub>0</sub> inhomogeneity mappings

fmriprep/workflows/bold/apply.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def init_bold_volumetric_resample_wf(
1717
metadata: dict,
1818
mem_gb: dict[str, float],
1919
jacobian: bool,
20+
fallback_total_readout_time: str | float | None = None,
2021
fieldmap_id: str | None = None,
2122
omp_nthreads: int = 1,
2223
name: str = 'bold_volumetric_resample_wf',
@@ -161,7 +162,10 @@ def init_bold_volumetric_resample_wf(
161162
run_without_submitting=True,
162163
)
163164
distortion_params = pe.Node(
164-
DistortionParameters(metadata=metadata),
165+
DistortionParameters(
166+
metadata=metadata,
167+
fallback=fallback_total_readout_time,
168+
),
165169
name='distortion_params',
166170
run_without_submitting=True,
167171
)

fmriprep/workflows/bold/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ def init_bold_wf(
383383
# Resample to anatomical space
384384
bold_anat_wf = init_bold_volumetric_resample_wf(
385385
metadata=all_metadata[0],
386+
fallback_total_readout_time=config.workflow.fallback_total_readout_time,
386387
fieldmap_id=fieldmap_id if not multiecho else None,
387388
omp_nthreads=omp_nthreads,
388389
mem_gb=mem_gb,

fmriprep/workflows/bold/fit.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
from niworkflows.interfaces.header import ValidateImage
3232
from niworkflows.interfaces.nitransforms import ConcatenateXFMs
3333
from niworkflows.interfaces.utility import KeySelect
34-
from sdcflows.workflows.apply.correction import init_unwarp_wf
3534
from sdcflows.workflows.apply.registration import init_coeff2epi_wf
3635

3736
from ... import config
@@ -189,7 +188,6 @@ def init_bold_fit_wf(
189188
* :py:func:`~fmriprep.workflows.bold.hmc.init_bold_hmc_wf`
190189
* :py:func:`~niworkflows.func.utils.init_enhance_and_skullstrip_bold_wf`
191190
* :py:func:`~sdcflows.workflows.apply.registration.init_coeff2epi_wf`
192-
* :py:func:`~sdcflows.workflows.apply.correction.init_unwarp_wf`
193191
* :py:func:`~fmriprep.workflows.bold.registration.init_bold_reg_wf`
194192
* :py:func:`~fmriprep.workflows.bold.outputs.init_ds_boldref_wf`
195193
* :py:func:`~fmriprep.workflows.bold.outputs.init_ds_hmc_wf`
@@ -547,12 +545,26 @@ def init_bold_fit_wf(
547545
(ds_fmapreg_wf, fmapreg_buffer, [('outputnode.xform', 'boldref2fmap_xfm')]),
548546
]) # fmt:skip
549547

550-
unwarp_wf = init_unwarp_wf(
551-
free_mem=config.environment.free_mem,
552-
debug='fieldmaps' in config.execution.debug,
553-
omp_nthreads=config.nipype.omp_nthreads,
548+
boldref_fmap = pe.Node(
549+
ReconstructFieldmap(inverse=[True]), name='boldref_fmap', mem_gb=1
550+
)
551+
552+
distortion_params = pe.Node(
553+
DistortionParameters(
554+
metadata=metadata,
555+
in_file=bold_file,
556+
fallback=config.workflow.fallback_total_readout_time,
557+
),
558+
name='distortion_params',
559+
run_without_submitting=True,
560+
)
561+
562+
unwarp_boldref = pe.Node(
563+
ResampleSeries(jacobian=jacobian),
564+
name='unwarp_boldref',
565+
n_procs=omp_nthreads,
566+
mem_gb=mem_gb['resampled'],
554567
)
555-
unwarp_wf.inputs.inputnode.metadata = layout.get_metadata(bold_file)
556568

557569
skullstrip_bold_wf = init_skullstrip_bold_wf()
558570

@@ -564,24 +576,26 @@ def init_bold_fit_wf(
564576
('sdc_method', 'sdc_method'),
565577
('fmap_id', 'keys'),
566578
]),
567-
(fmap_select, unwarp_wf, [
568-
('fmap_coeff', 'inputnode.fmap_coeff'),
579+
(fmapref_buffer, boldref_fmap, [('out', 'target_ref_file')]),
580+
(fmapreg_buffer, boldref_fmap, [('boldref2fmap_xfm', 'transforms')]),
581+
(fmap_select, boldref_fmap, [
582+
('fmap_coeff', 'in_coeffs'),
583+
('fmap_ref', 'fmap_ref_file'),
569584
]),
570-
(fmapreg_buffer, unwarp_wf, [
571-
# This looks backwards, but unwarp_wf describes transforms in
572-
# terms of points while we (and init_coeff2epi_wf) describe them
573-
# in terms of images. Mapping fieldmap coordinates into boldref
574-
# coordinates maps the boldref image onto the fieldmap image.
575-
('boldref2fmap_xfm', 'inputnode.fmap2data_xfm'),
585+
(fmapref_buffer, unwarp_boldref, [('out', 'ref_file')]),
586+
(enhance_boldref_wf, unwarp_boldref, [
587+
('outputnode.bias_corrected_file', 'in_file'),
576588
]),
577-
(enhance_boldref_wf, unwarp_wf, [
578-
('outputnode.bias_corrected_file', 'inputnode.distorted'),
589+
(boldref_fmap, unwarp_boldref, [('out_file', 'fieldmap')]),
590+
(distortion_params, unwarp_boldref, [
591+
('readout_time', 'ro_time'),
592+
('pe_direction', 'pe_dir'),
579593
]),
580-
(unwarp_wf, ds_coreg_boldref_wf, [
581-
('outputnode.corrected', 'inputnode.boldref'),
594+
(unwarp_boldref, ds_coreg_boldref_wf, [
595+
('out_file', 'inputnode.boldref'),
582596
]),
583-
(unwarp_wf, skullstrip_bold_wf, [
584-
('outputnode.corrected', 'inputnode.in_file'),
597+
(ds_coreg_boldref_wf, skullstrip_bold_wf, [
598+
('outputnode.boldref', 'inputnode.in_file'),
585599
]),
586600
(skullstrip_bold_wf, ds_boldmask_wf, [
587601
('outputnode.mask_file', 'inputnode.boldmask'),
@@ -591,7 +605,7 @@ def init_bold_fit_wf(
591605
(fmapreg_buffer, func_fit_reports_wf, [
592606
('boldref2fmap_xfm', 'inputnode.boldref2fmap_xfm'),
593607
]),
594-
(unwarp_wf, func_fit_reports_wf, [('outputnode.fieldmap', 'inputnode.fieldmap')]),
608+
(boldref_fmap, func_fit_reports_wf, [('out_file', 'inputnode.fieldmap')]),
595609
]) # fmt:skip
596610
else:
597611
workflow.connect([
@@ -859,7 +873,11 @@ def init_bold_native_wf(
859873
)
860874

861875
distortion_params = pe.Node(
862-
DistortionParameters(metadata=metadata, in_file=bold_file),
876+
DistortionParameters(
877+
metadata=metadata,
878+
in_file=bold_file,
879+
fallback=config.workflow.fallback_total_readout_time,
880+
),
863881
name='distortion_params',
864882
run_without_submitting=True,
865883
)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ dependencies = [
3434
"psutil >= 5.4",
3535
"pybids >= 0.16",
3636
"requests >= 2.27",
37-
"sdcflows >= 2.11.0",
38-
"smriprep >= 0.17.0",
37+
"sdcflows >= 2.13.0",
38+
"smriprep >= 0.18.0",
3939
"tedana >= 23.0.2",
4040
"templateflow >= 24.2.2",
4141
"transforms3d >= 0.4",

0 commit comments

Comments
 (0)