diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c67a2d7ae..4987515d87 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -541,6 +541,24 @@ jobs: - store_artifacts: path: /tmp/ds005/derivatives destination: fasttrack + - run: + name: Check fs-no-resume using existing freesufer output + no_output_timeout: 5m + command: | + bash /tmp/src/smriprep/.circleci/ds005_run.sh --fs-no-resume + - 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_no_resume + - store_artifacts: + path: /tmp/ds005/derivatives + destination: fs_no_resume ds054: <<: *machine_defaults environment: diff --git a/smriprep/cli/run.py b/smriprep/cli/run.py index 31cdef4dba..5510bcbb65 100644 --- a/smriprep/cli/run.py +++ b/smriprep/cli/run.py @@ -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-no-resume', + action='store_true', + dest='fs_no_resume', + help='EXPERT: Import pre-computed FreeSurfer reconstruction without resuming. ' + 'The user is responsible for ensuring that all necessary files are present.', + ) g_fs.add_argument( '--cifti-output', nargs='?', @@ -616,6 +623,7 @@ def build_workflow(opts, retval): freesurfer=opts.run_reconall, fs_subjects_dir=opts.fs_subjects_dir, hires=opts.hires, + fs_no_resume=opts.fs_no_resume, layout=layout, longitudinal=opts.longitudinal, low_mem=opts.low_mem, diff --git a/smriprep/workflows/anatomical.py b/smriprep/workflows/anatomical.py index 4bdfd285c5..f879b2ea7e 100644 --- a/smriprep/workflows/anatomical.py +++ b/smriprep/workflows/anatomical.py @@ -112,6 +112,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_no_resume: bool = False, ): """ Stage the anatomical preprocessing steps of *sMRIPrep*. @@ -184,6 +185,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_no_resume : bool + EXPERT: Import pre-computed FreeSurfer reconstruction without resuming. + The user is responsible for ensuring that all necessary files are present. + (default: ``False``). Inputs ------ @@ -276,6 +281,7 @@ def init_anat_preproc_wf( sloppy=sloppy, omp_nthreads=omp_nthreads, skull_strip_fixed_seed=skull_strip_fixed_seed, + fs_no_resume=fs_no_resume, ) template_iterator_wf = init_template_iterator_wf(spaces=spaces, sloppy=sloppy) ds_std_volumes_wf = init_ds_anat_volumes_wf( @@ -459,6 +465,7 @@ def init_anat_fit_wf( sloppy: bool = False, name='anat_fit_wf', skull_strip_fixed_seed: bool = False, + fs_no_resume: bool = False, ): """ Stage the anatomical preprocessing steps of *sMRIPrep*. @@ -1042,6 +1049,7 @@ def init_anat_fit_wf( name='surface_recon_wf', omp_nthreads=omp_nthreads, hires=hires, + fs_no_resume=fs_no_resume, precomputed=precomputed, ) if t2w or flair: diff --git a/smriprep/workflows/base.py b/smriprep/workflows/base.py index 5e1bda87e6..f06c13eb05 100644 --- a/smriprep/workflows/base.py +++ b/smriprep/workflows/base.py @@ -47,6 +47,7 @@ def init_smriprep_wf( freesurfer, fs_subjects_dir, hires, + fs_no_resume, layout, longitudinal, low_mem, @@ -89,6 +90,7 @@ def init_smriprep_wf( freesurfer=True, fs_subjects_dir=None, hires=True, + fs_no_resume=False, layout=BIDSLayout('.'), longitudinal=False, low_mem=False, @@ -179,6 +181,7 @@ def init_smriprep_wf( freesurfer=freesurfer, derivatives=derivatives, hires=hires, + fs_no_resume=fs_no_resume, layout=layout, longitudinal=longitudinal, low_mem=low_mem, @@ -215,6 +218,7 @@ def init_single_subject_wf( derivatives, freesurfer, hires, + fs_no_resume, layout, longitudinal, low_mem, @@ -259,6 +263,7 @@ def init_single_subject_wf( freesurfer=True, derivatives=[], hires=True, + fs_no_resume=False, layout=BIDSLayout('.'), longitudinal=False, low_mem=False, @@ -287,6 +292,9 @@ def init_single_subject_wf( Enable FreeSurfer surface reconstruction (may increase runtime) hires : :obj:`bool` Enable sub-millimeter preprocessing in FreeSurfer + fs_no_resume : bool + Adjust pipeline to reuse base template + of an existing longitudinal freesurfer output layout : BIDSLayout object BIDS dataset layout longitudinal : :obj:`bool` @@ -419,6 +427,7 @@ def init_single_subject_wf( precomputed=deriv_cache, freesurfer=freesurfer, hires=hires, + fs_no_resume=fs_no_resume, longitudinal=longitudinal, msm_sulc=msm_sulc, name='anat_preproc_wf', diff --git a/smriprep/workflows/surfaces.py b/smriprep/workflows/surfaces.py index b32c2e2862..12e78c501a 100644 --- a/smriprep/workflows/surfaces.py +++ b/smriprep/workflows/surfaces.py @@ -65,6 +65,7 @@ def init_surface_recon_wf( *, omp_nthreads: int, hires: bool, + fs_no_resume: bool, precomputed: dict, name='surface_recon_wf', ): @@ -130,7 +131,11 @@ def init_surface_recon_wf( :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_no_resume=False, + precomputed={}) Parameters ---------- @@ -138,6 +143,9 @@ def init_surface_recon_wf( Maximum number of threads an individual process may use hires : bool Enable sub-millimeter preprocessing in FreeSurfer + fs_no_resume : bool + use precomputed freesurfer without attempting to resume + (eg. for longitudinal base or fastsurfer) Inputs ------ @@ -239,39 +247,62 @@ def init_surface_recon_wf( name='sync', ) + if not fs_no_resume: + 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'), + ]), + ]) # fmt:skip + else: + # Pretend to be the autorecon1 node so fsnative2t1w_xfm gets run ASAP + fs_base_inputs = autorecon1 = pe.Node(nio.FreeSurferSource(), name='fs_base_inputs') + + workflow.connect([ + (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:skip + 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'),