From da647e46f8207396874d060a726105f2992e6d39 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Wed, 11 Jun 2025 14:08:04 -0400 Subject: [PATCH 1/2] take fs license file as argument --- freesurfer_post/cli.py | 19 ++++++++++++++++--- tests/test.sh | 4 +++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/freesurfer_post/cli.py b/freesurfer_post/cli.py index 9a4624a..0ccea69 100644 --- a/freesurfer_post/cli.py +++ b/freesurfer_post/cli.py @@ -17,7 +17,18 @@ @click.option('--subject-id', '-s', help='Subject ID to process') @click.option('--session-id', '-x', help='Session ID to process') @click.option('--working-dir', '-w', help='Path to working directory') -def main(verbose, input_path, output_path, processing_level, subjects_dir, subject_id, session_id, working_dir): +@click.option('--fs-license-file', '-l', help='Path to license file') +def main( + verbose, + input_path, + output_path, + processing_level, + subjects_dir, + subject_id, + session_id, + working_dir, + fs_license_file, +): """FreeSurfer Post-processing Tools. A command line interface for post-processing FreeSurfer outputs. @@ -41,6 +52,7 @@ def main(verbose, input_path, output_path, processing_level, subjects_dir, subje click.echo(f'Subject directory: {subject_fs_dir}') click.echo(f'Processing level: {processing_level}') click.echo(f'Working directory: {working_dir}') + click.echo(f'FreeSurfer license file: {fs_license_file}') workflow = build_workflow( subject_id=subject_id, @@ -50,8 +62,9 @@ def main(verbose, input_path, output_path, processing_level, subjects_dir, subje working_dir=working_dir, ) - workflow.config['execution'] = { - 'stop_on_first_crash': 'true'} + import os + os.environ['FS_LICENSE'] = fs_license_file + workflow.config['execution'] = {'stop_on_first_crash': 'true'} workflow.run() diff --git a/tests/test.sh b/tests/test.sh index cf3d115..5491763 100644 --- a/tests/test.sh +++ b/tests/test.sh @@ -3,7 +3,7 @@ docker run --rm -ti --entrypoint /bin/bash \ -v /Users/mcieslak/Desktop/fmriprep_anat:/data \ -v /Users/mcieslak/Desktop/freesurfer_post:/output \ -v /Users/mcieslak/Desktop/fspost_work:/work \ - --mount type=bind,source=/Users/mcieslak/Desktop/license.txt,target=/opt/freesurfer/license.txt \ + --mount type=bind,source=/Users/mcieslak/Desktop/license.txt,target=/opt/fs_license.txt \ -v /Users/mcieslak/projects/freesurfer-post/freesurfer_post:/opt/conda/envs/freesurfer-post/lib/python3.12/site-packages/freesurfer_post \ pennlinc/freesurfer-post:unstable \ /data \ @@ -18,5 +18,7 @@ freesurfer-post \ participant \ --subjects-dir /data/sourcedata/freesurfer \ --subject-id sub-colornest001 \ + --fs-license-file /opt/fs_license.txt \ + --session-id ses-01 \ -w /work From 56b7a97bcfb5e0e25a8f9f46ba38da541004d9b7 Mon Sep 17 00:00:00 2001 From: mattcieslak Date: Wed, 11 Jun 2025 14:13:38 -0400 Subject: [PATCH 2/2] ruff --- freesurfer_post/__init__.py | 2 +- freesurfer_post/cli.py | 1 + freesurfer_post/interfaces/interfaces.py | 2 + freesurfer_post/interfaces/tabular.py | 8 ++- freesurfer_post/utils.py | 13 ++-- freesurfer_post/workflows.py | 82 +++++++++++++++--------- 6 files changed, 69 insertions(+), 39 deletions(-) diff --git a/freesurfer_post/__init__.py b/freesurfer_post/__init__.py index 14446db..6eefa23 100644 --- a/freesurfer_post/__init__.py +++ b/freesurfer_post/__init__.py @@ -6,4 +6,4 @@ __version__ = '0.1.0' __author__ = 'Your Name' -__email__ = 'your.email@example.com' \ No newline at end of file +__email__ = 'your.email@example.com' diff --git a/freesurfer_post/cli.py b/freesurfer_post/cli.py index 0ccea69..fdb0162 100644 --- a/freesurfer_post/cli.py +++ b/freesurfer_post/cli.py @@ -63,6 +63,7 @@ def main( ) import os + os.environ['FS_LICENSE'] = fs_license_file workflow.config['execution'] = {'stop_on_first_crash': 'true'} workflow.run() diff --git a/freesurfer_post/interfaces/interfaces.py b/freesurfer_post/interfaces/interfaces.py index e71a167..3cdde5d 100644 --- a/freesurfer_post/interfaces/interfaces.py +++ b/freesurfer_post/interfaces/interfaces.py @@ -7,12 +7,14 @@ class _WaitingInputSpec(TraitedSpec): mandatory=True, ) + class _WaitingOutputSpec(TraitedSpec): no_args = traits.Str( desc='No args', mandatory=True, ) + class Waiting(SimpleInterface): # Force a bunch of nodes to finish and then retuen an empty string # to put in "args" of the dependent workflow diff --git a/freesurfer_post/interfaces/tabular.py b/freesurfer_post/interfaces/tabular.py index aa5a393..8c754a2 100644 --- a/freesurfer_post/interfaces/tabular.py +++ b/freesurfer_post/interfaces/tabular.py @@ -15,7 +15,7 @@ def statsfile_to_df(stats_fname, hemi, atlas, column_suffix=''): - with open(stats_fname, 'r') as fo: + with open(stats_fname) as fo: data = fo.readlines() idx = [i for i, l in enumerate(data) if l.startswith('# ColHeaders ')] @@ -137,7 +137,9 @@ def sanity_check_columns(reference_column, redundant_column, atol=0): output_dir = Path(self.inputs.output_dir) / subject_id output_dir.mkdir(parents=True, exist_ok=True) output_prefix = f'{subject_id}_{session_id}' if session_id else subject_id - cleaned_atlas_name = atlas.replace('.', '').replace('_order', '').replace('_', '') + cleaned_atlas_name = ( + atlas.replace('.', '').replace('_order', '').replace('_', '') + ) out_df.to_csv( output_dir / f'{output_prefix}_atlas-{cleaned_atlas_name}_surfacestats.tsv', sep='\t', @@ -204,7 +206,7 @@ def read_stats(stats_file, info, get_measures=False, measures_only=False): (header,) = [line for line in lines if header_tag in line] header = header[len(header_tag) :].strip().split() - stats_df = pd.read_csv(str(stats_file), sep='\s+', comment='#', names=header).melt( + stats_df = pd.read_csv(str(stats_file), sep=r'\s+', comment='#', names=header).melt( id_vars=['StructName'], ignore_index=True ) stats_df = stats_df[stats_df['variable'] != 'Index'] diff --git a/freesurfer_post/utils.py b/freesurfer_post/utils.py index 2e53a64..4dd6905 100644 --- a/freesurfer_post/utils.py +++ b/freesurfer_post/utils.py @@ -1,9 +1,12 @@ """Utility functions for FreeSurfer post-processing.""" + import warnings from pathlib import Path -def find_freesurfer_dir(subjects_dir: str | Path, subject_id: str, session_id: str | None = None) -> Path: +def find_freesurfer_dir( + subjects_dir: str | Path, subject_id: str, session_id: str | None = None +) -> Path: """Find a valid FreeSurfer subject directory in a directory. Parameters @@ -27,8 +30,8 @@ def find_freesurfer_dir(subjects_dir: str | Path, subject_id: str, session_id: s warn_no_session = False if session_id is not None: - if (subjects_dir / f"{subject_id}_{session_id}").exists(): - return subjects_dir / f"{subject_id}_{session_id}" + if (subjects_dir / f'{subject_id}_{session_id}').exists(): + return subjects_dir / f'{subject_id}_{session_id}' warn_no_session = True if (subjects_dir / subject_id).exists(): @@ -39,4 +42,6 @@ def find_freesurfer_dir(subjects_dir: str | Path, subject_id: str, session_id: s stacklevel=2, ) return subjects_dir / subject_id - raise FileNotFoundError(f'No directory found for subject: {subject_id}, session: {session_id}') + raise FileNotFoundError( + f'No directory found for subject: {subject_id}, session: {session_id}' + ) diff --git a/freesurfer_post/workflows.py b/freesurfer_post/workflows.py index 2759c88..4f8bc64 100644 --- a/freesurfer_post/workflows.py +++ b/freesurfer_post/workflows.py @@ -59,7 +59,8 @@ def build_workflow( inputnode = pe.Node( niu.IdentityInterface( - fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir']), + fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir'] + ), name='inputnode', ) inputnode.inputs.subject_id = subject_id @@ -73,29 +74,44 @@ def build_workflow( subject_freesurfer_dir=subject_freesurfer_dir, parc_name=parc_name, ) - workflow.connect([ - (inputnode, parc_wf, [('subject_id', 'inputnode.subject_id'), - ('session_id', 'inputnode.session_id'), - ('fs_subjects_dir', 'inputnode.fs_subjects_dir'), - ('output_dir', 'inputnode.output_dir')]), - ]) + workflow.connect( + [ + ( + inputnode, + parc_wf, + [ + ('subject_id', 'inputnode.subject_id'), + ('session_id', 'inputnode.session_id'), + ('fs_subjects_dir', 'inputnode.fs_subjects_dir'), + ('output_dir', 'inputnode.output_dir'), + ], + ), + ] + ) # Get the segmentation stats and euler number fs_stats = pe.Node(FSStats(), name='fs_stats') - workflow.connect([ - (inputnode, fs_stats, [('subject_id', 'subject_id'), - ('session_id', 'session_id'), - ('fs_subjects_dir', 'subjects_dir'), - ('output_dir', 'output_dir')]), - ]) + workflow.connect( + [ + ( + inputnode, + fs_stats, + [ + ('subject_id', 'subject_id'), + ('session_id', 'session_id'), + ('fs_subjects_dir', 'subjects_dir'), + ('output_dir', 'output_dir'), + ], + ), + ] + ) return workflow def init_parcellation_wf( - subject_id: str, - subject_freesurfer_dir: str | Path, - parc_name: str): + subject_id: str, subject_freesurfer_dir: str | Path, parc_name: str +): """ Initialize a workflow to process a single parcellation. @@ -126,7 +142,8 @@ def init_parcellation_wf( """ inputnode = pe.Node( niu.IdentityInterface( - fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir']), + fields=['subject_id', 'session_id', 'fs_subjects_dir', 'output_dir'] + ), name='inputnode', ) clean_parc_name = parc_name.replace('.', '').replace('_', '') @@ -138,25 +155,32 @@ def init_parcellation_wf( ), name='collect_stats', ) - workflow.connect([ - (inputnode, collect_stats, [ - ('subject_id', 'subject_id'), - ('session_id', 'session_id'), - ('fs_subjects_dir', 'subjects_dir'), - ('output_dir', 'output_dir'), - ]), - ]) + workflow.connect( + [ + ( + inputnode, + collect_stats, + [ + ('subject_id', 'subject_id'), + ('session_id', 'session_id'), + ('fs_subjects_dir', 'subjects_dir'), + ('output_dir', 'output_dir'), + ], + ), + ] + ) transform_nodes = {} parc_stats_nodes = {} gwr_seg_stats_nodes = {} for hemi in ['lh', 'rh']: - fsaverage_annot = ANNOTS_DIR / f'{hemi}.{parc_name}.annot' # native annot is created by SurfaceTransform if it's not a NATIVE_PARCELLATION # otherwise it's already present in the subject's directory native_annot = subject_freesurfer_dir / 'label' / f'{hemi}.{parc_name}.annot' stats_file = subject_freesurfer_dir / 'stats' / f'{hemi}.{parc_name}.stats' - gwr_stats_file = subject_freesurfer_dir / 'stats' / f'{hemi}.{parc_name}.g-w.pct.stats' + gwr_stats_file = ( + subject_freesurfer_dir / 'stats' / f'{hemi}.{parc_name}.g-w.pct.stats' + ) if parc_name in AVAILABLE_PARCELLATIONS: transform_nodes[hemi] = pe.Node( fs.SurfaceTransform( @@ -229,7 +253,3 @@ def init_parcellation_wf( ]) # fmt: off return workflow - - - -