Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a --fs-reuse-base option to reuse existing freesurfer outputs from a longitudinal pipeline #371

Closed
wants to merge 11 commits into from
20 changes: 19 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -241,7 +241,7 @@ jobs:
command: |
mkdir -p /tmp/fslicense
cd /tmp/fslicense
echo "cHJpbnRmICJrcnp5c3p0b2YuZ29yZ29sZXdza2lAZ21haWwuY29tXG41MTcyXG4gKkN2dW12RVYzelRmZ1xuRlM1Si8yYzFhZ2c0RVxuIiA+IGxpY2Vuc2UudHh0Cg==" | base64 -d | sh
echo "cHJpbnRmICJrcnp5c3p0b2YuZ29yZ29sZXdza2lAZ21haWwuY29tXG41MTcyXG4gKkN2dW12RVYzelRmZ1xuRlM1Si8yYzFhZ2c0RVxuIiA+IGxpY2Vuc2UudHh0Cg==" | base64 -d | sh
- persist_to_workspace:
root: /tmp
paths:
@@ -540,6 +540,24 @@ jobs:
- store_artifacts:
path: /tmp/ds005/derivatives
destination: fasttrack
- run:
name: Check fs-reuse-base using existing freesufer output
no_output_timeout: 5m
command: |
bash /tmp/src/smriprep/.circleci/ds005_run.sh --fs-reuse-base
- run:
name: Clean working directory
when: on_fail
command: |
rm -rf /tmp/ds005/work/smriprep_wf/fsdir_run_*/
find /tmp/ds005/work \( -name "*.nii.gz" -or -name "*.nii" -or -name "*.gii" -or -name "*.h5" \) \
-exec sh -c 'rm -f {}; touch {}' \;
- store_artifacts:
path: /tmp/ds005/work
destination: fs_reuse_base
- store_artifacts:
path: /tmp/ds005/derivatives
destination: fs_reuse_base
ds054:
<<: *machine_defaults
environment:
8 changes: 8 additions & 0 deletions smriprep/cli/run.py
Original file line number Diff line number Diff line change
@@ -214,6 +214,13 @@ def get_parser():
help="Path to existing FreeSurfer subjects directory to reuse. "
"(default: OUTPUT_DIR/freesurfer)",
)
g_fs.add_argument(
"--fs-reuse-base",
action="store_true",
dest="fs_reuse_base",
help="Reuse freesurfer base template"
"(from longitudinal preprocessing)",
)
g_fs.add_argument(
"--cifti-output",
nargs="?",
@@ -603,6 +610,7 @@ def build_workflow(opts, retval):
freesurfer=opts.run_reconall,
fs_subjects_dir=opts.fs_subjects_dir,
hires=opts.hires,
fs_reuse_base=opts.fs_reuse_base,
layout=layout,
longitudinal=opts.longitudinal,
low_mem=opts.low_mem,
8 changes: 8 additions & 0 deletions smriprep/workflows/anatomical.py
Original file line number Diff line number Diff line change
@@ -99,6 +99,7 @@ def init_anat_preproc_wf(
cifti_output: ty.Literal["91k", "170k", False] = False,
name: str = "anat_preproc_wf",
skull_strip_fixed_seed: bool = False,
fs_reuse_base: bool = False,
):
"""
Stage the anatomical preprocessing steps of *sMRIPrep*.
@@ -169,6 +170,10 @@ def init_anat_preproc_wf(
Do not use a random seed for skull-stripping - will ensure
run-to-run replicability when used with --omp-nthreads 1
(default: ``False``).
fs_reuse_base : bool
Adjust pipeline to reuse base template
of an existing longitudinal freesurfer output
(default: ``False``).

Inputs
------
@@ -260,6 +265,7 @@ def init_anat_preproc_wf(
sloppy=sloppy,
omp_nthreads=omp_nthreads,
skull_strip_fixed_seed=skull_strip_fixed_seed,
fs_reuse_base=fs_reuse_base,
)
anat_second_derivatives_wf = init_anat_second_derivatives_wf(
bids_root=bids_root,
@@ -371,6 +377,7 @@ def init_anat_fit_wf(
sloppy: bool = False,
name="anat_fit_wf",
skull_strip_fixed_seed: bool = False,
fs_reuse_base: bool = False,
):
"""
Stage the anatomical preprocessing steps of *sMRIPrep*.
@@ -952,6 +959,7 @@ def init_anat_fit_wf(
name="surface_recon_wf",
omp_nthreads=omp_nthreads,
hires=hires,
fs_reuse_base=fs_reuse_base,
precomputed=precomputed,
)

9 changes: 9 additions & 0 deletions smriprep/workflows/base.py
Original file line number Diff line number Diff line change
@@ -48,6 +48,7 @@ def init_smriprep_wf(
freesurfer,
fs_subjects_dir,
hires,
fs_reuse_base,
layout,
longitudinal,
low_mem,
@@ -88,6 +89,7 @@ def init_smriprep_wf(
freesurfer=True,
fs_subjects_dir=None,
hires=True,
fs_reuse_base=False,
layout=BIDSLayout('.'),
longitudinal=False,
low_mem=False,
@@ -178,6 +180,7 @@ def init_smriprep_wf(
freesurfer=freesurfer,
derivatives=derivatives,
hires=hires,
fs_reuse_base=fs_reuse_base,
layout=layout,
longitudinal=longitudinal,
low_mem=low_mem,
@@ -214,6 +217,7 @@ def init_single_subject_wf(
derivatives,
freesurfer,
hires,
fs_reuse_base,
layout,
longitudinal,
low_mem,
@@ -256,6 +260,7 @@ def init_single_subject_wf(
freesurfer=True,
derivatives=[],
hires=True,
fs_reuse_base=False,
layout=BIDSLayout('.'),
longitudinal=False,
low_mem=False,
@@ -284,6 +289,9 @@ def init_single_subject_wf(
Enable FreeSurfer surface reconstruction (may increase runtime)
hires : :obj:`bool`
Enable sub-millimeter preprocessing in FreeSurfer
fs_reuse_base : bool
Adjust pipeline to reuse base template
of an existing longitudinal freesurfer output
layout : BIDSLayout object
BIDS dataset layout
longitudinal : :obj:`bool`
@@ -418,6 +426,7 @@ def init_single_subject_wf(
precomputed=deriv_cache,
freesurfer=freesurfer,
hires=hires,
fs_reuse_base=fs_reuse_base,
longitudinal=longitudinal,
msm_sulc=msm_sulc,
name="anat_preproc_wf",
120 changes: 87 additions & 33 deletions smriprep/workflows/surfaces.py
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@
*,
omp_nthreads: int,
hires: bool,
fs_reuse_base: bool,
precomputed: dict,
name="surface_recon_wf",
):
@@ -123,14 +124,21 @@
:simple_form: yes

from smriprep.workflows.surfaces import init_surface_recon_wf
wf = init_surface_recon_wf(omp_nthreads=1, hires=True, precomputed={})
wf = init_surface_recon_wf(
omp_nthreads=1,
hires=True,
fs_reuse_base=False,
precomputed={})

Parameters
----------
omp_nthreads : int
Maximum number of threads an individual process may use
hires : bool
Enable sub-millimeter preprocessing in FreeSurfer
fs_reuse_base : bool
Adjust pipeline to reuse base template
of an existing longitudinal freesurfer output

Inputs
------
@@ -233,40 +241,86 @@
name="sync",
)

if not fs_reuse_base:

recon_config = pe.Node(FSDetectInputs(hires_enabled=hires), name="recon_config")

Check warning on line 246 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L246

Added line #L246 was not covered by tests

fov_check = pe.Node(niu.Function(function=_check_cw256), name="fov_check")
fov_check.inputs.default_flags = ['-noskullstrip', '-noT2pial', '-noFLAIRpial']

Check warning on line 249 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L248-L249

Added lines #L248 - L249 were not covered by tests

autorecon1 = pe.Node(

Check warning on line 251 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L251

Added line #L251 was not covered by tests
ReconAll(directive="autorecon1", openmp=omp_nthreads),
name="autorecon1",
n_procs=omp_nthreads,
mem_gb=5,
)
autorecon1.interface._can_resume = False
autorecon1.interface._always_run = True

Check warning on line 258 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L257-L258

Added lines #L257 - L258 were not covered by tests

skull_strip_extern = pe.Node(FSInjectBrainExtracted(), name="skull_strip_extern")

Check warning on line 260 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L260

Added line #L260 was not covered by tests

autorecon_resume_wf = init_autorecon_resume_wf(omp_nthreads=omp_nthreads)

Check warning on line 262 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L262

Added line #L262 was not covered by tests

# fmt:off
workflow.connect([

Check warning on line 265 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L265

Added line #L265 was not covered by tests
# Configuration
(inputnode, recon_config, [('t1w', 't1w_list'),
('t2w', 't2w_list'),
('flair', 'flair_list')]),
# Passing subjects_dir / subject_id enforces serial order
(inputnode, autorecon1, [('subjects_dir', 'subjects_dir'),
('subject_id', 'subject_id')]),
(autorecon1, skull_strip_extern, [('subjects_dir', 'subjects_dir'),
('subject_id', 'subject_id')]),
(skull_strip_extern, autorecon_resume_wf, [('subjects_dir', 'inputnode.subjects_dir'),
('subject_id', 'inputnode.subject_id')]),
# Reconstruction phases
(inputnode, autorecon1, [('t1w', 'T1_files')]),
(inputnode, fov_check, [('t1w', 'in_files')]),
(fov_check, autorecon1, [('out', 'flags')]),
(recon_config, autorecon1, [('t2w', 'T2_file'),
('flair', 'FLAIR_file'),
('hires', 'hires'),
# First run only (recon-all saves expert options)
('mris_inflate', 'mris_inflate')]),
(inputnode, skull_strip_extern, [('skullstripped_t1', 'in_brain')]),
(recon_config, autorecon_resume_wf, [('use_t2w', 'inputnode.use_T2'),
('use_flair', 'inputnode.use_FLAIR')]),
# Generate mid-thickness surfaces
(autorecon_resume_wf, get_surfaces, [
('outputnode.subjects_dir', 'subjects_dir'),
('outputnode.subject_id', 'subject_id'),
]),
(autorecon_resume_wf, save_midthickness, [
('outputnode.subjects_dir', 'base_directory'),
('outputnode.subject_id', 'container'),
]),
])
# fmt:on
else:
fs_base_inputs = autorecon1 = pe.Node(

Check warning on line 301 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L301

Added line #L301 was not covered by tests
nio.FreeSurferSource(),
name='fs_base_inputs'
)

# fmt:off
workflow.connect([

Check warning on line 307 in smriprep/workflows/surfaces.py

Codecov / codecov/patch

smriprep/workflows/surfaces.py#L307

Added line #L307 was not covered by tests
(inputnode, fs_base_inputs, [('subjects_dir', 'subjects_dir'),
('subject_id', 'subject_id')]),
# Generate mid-thickness surfaces
(inputnode, get_surfaces, [
('subjects_dir', 'subjects_dir'),
('subject_id', 'subject_id'),
]),
(inputnode, save_midthickness, [
('subjects_dir', 'base_directory'),
('subject_id', 'container'),
]),
])
# fmt:on

# fmt:off
workflow.connect([
# Configuration
(inputnode, recon_config, [('t1w', 't1w_list'),
('t2w', 't2w_list'),
('flair', 'flair_list')]),
# Passing subjects_dir / subject_id enforces serial order
(inputnode, autorecon1, [('subjects_dir', 'subjects_dir'),
('subject_id', 'subject_id')]),
(autorecon1, skull_strip_extern, [('subjects_dir', 'subjects_dir'),
('subject_id', 'subject_id')]),
(skull_strip_extern, autorecon_resume_wf, [('subjects_dir', 'inputnode.subjects_dir'),
('subject_id', 'inputnode.subject_id')]),
# Reconstruction phases
(inputnode, autorecon1, [('t1w', 'T1_files')]),
(inputnode, fov_check, [('t1w', 'in_files')]),
(fov_check, autorecon1, [('out', 'flags')]),
(recon_config, autorecon1, [('t2w', 'T2_file'),
('flair', 'FLAIR_file'),
('hires', 'hires'),
# First run only (recon-all saves expert options)
('mris_inflate', 'mris_inflate')]),
(inputnode, skull_strip_extern, [('skullstripped_t1', 'in_brain')]),
(recon_config, autorecon_resume_wf, [('use_t2w', 'inputnode.use_T2'),
('use_flair', 'inputnode.use_FLAIR')]),
# Generate mid-thickness surfaces
(autorecon_resume_wf, get_surfaces, [
('outputnode.subjects_dir', 'subjects_dir'),
('outputnode.subject_id', 'subject_id'),
]),
(autorecon_resume_wf, save_midthickness, [
('outputnode.subjects_dir', 'base_directory'),
('outputnode.subject_id', 'container'),
]),
(get_surfaces, midthickness, [
('white', 'in_file'),
('graymid', 'graymid'),