From dfc906677c4da8f568e971abc2675983713f6525 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 31 Jul 2025 16:14:36 -0400 Subject: [PATCH 01/26] Draft cortex mask workflow. --- src/smriprep/workflows/anatomical.py | 31 +++++++++++ src/smriprep/workflows/surfaces.py | 82 ++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 293dabe0b5..03ca1d9cb8 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -78,6 +78,7 @@ ) from .surfaces import ( init_anat_ribbon_wf, + init_cortex_mask_wf, init_fsLR_reg_wf, init_gifti_morphometrics_wf, init_gifti_surfaces_wf, @@ -668,6 +669,7 @@ def init_anat_fit_wf( 'sphere_reg', 'sphere_reg_fsLR', 'sphere_reg_msm', + 'cortex_mask', 'anat_ribbon', # Reverse transform; not computable from forward transform 'std2anat_xfm', @@ -1386,6 +1388,35 @@ def init_anat_fit_wf( else: LOGGER.info('ANAT Stage 10: MSM-Sulc disabled') + # Stage 11: Cortical surface mask + if len(precomputed.get('cortex_mask', [])) < 2: + LOGGER.info('ANAT Stage 11: Creating cortical surface mask') + anat_cortex_mask_wf = init_cortex_mask_wf() + ds_cortex_mask_wf = init_ds_mask_wf( + bids_root=bids_root, + output_dir=output_dir, + mask_type='roi', + name='ds_cortex_mask_wf', + extra_entities={'extension': '.label.gii'}, + ) + workflow.connect([ + (surfaces_buffer, anat_cortex_mask_wf, [ + ('midthickness', 'inputnode.midthickness'), + ('thickness', 'inputnode.thickness'), + ]), + (anat_cortex_mask_wf, ds_cortex_mask_wf, [ + ('outputnode.cortex_mask', 'inputnode.mask_file'), + ]), + (surfaces_buffer, ds_cortex_mask_wf, [ + ('midthickness', 'inputnode.source_files'), + ('thickness', 'inputnode.source_files'), + ]), + (ds_cortex_mask_wf, outputnode, [('outputnode.mask_file', 'cortex_mask')]), + ]) # fmt:skip + else: + LOGGER.info('ANAT Stage 11: Found pre-computed cortical surface mask') + outputnode.inputs.cortex_mask = sorted(precomputed['cortex_mask']) + return workflow diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index 2840a166b6..a45990c647 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1071,8 +1071,6 @@ def init_hcp_morphometrics_wf( HCP-style curvature file in GIFTI format sulc HCP-style sulcal depth file in GIFTI format - roi - HCP-style cortical ROI file in GIFTI format """ DEFAULT_MEMORY_MIN_GB = 0.01 @@ -1090,7 +1088,7 @@ def init_hcp_morphometrics_wf( ) outputnode = pe.JoinNode( - niu.IdentityInterface(fields=['thickness', 'curv', 'sulc', 'roi']), + niu.IdentityInterface(fields=['thickness', 'curv', 'sulc']), name='outputnode', joinsource='itersource', ) @@ -1115,11 +1113,6 @@ def init_hcp_morphometrics_wf( # Thickness is presumably already positive, but HCP uses abs(-thickness) abs_thickness = pe.Node(MetricMath(metric='thickness', operation='abs'), name='abs_thickness') - # Native ROI is thickness > 0, with holes and islands filled - initial_roi = pe.Node(MetricMath(metric='roi', operation='bin'), name='initial_roi') - fill_holes = pe.Node(MetricFillHoles(), name='fill_holes', mem_gb=DEFAULT_MEMORY_MIN_GB) - native_roi = pe.Node(MetricRemoveIslands(), name='native_roi', mem_gb=DEFAULT_MEMORY_MIN_GB) - # Dilation happens separately from ROI creation dilate_curv = pe.Node( MetricDilate(distance=10, nearest=True), @@ -1158,8 +1151,77 @@ def init_hcp_morphometrics_wf( (dilate_curv, outputnode, [('out_file', 'curv')]), (dilate_thickness, outputnode, [('out_file', 'thickness')]), (invert_sulc, outputnode, [('metric_file', 'sulc')]), - # Native ROI file from thickness - (inputnode, initial_roi, [('subject_id', 'subject_id')]), + ]) # fmt:skip + + return workflow + + +def init_cortex_mask_wf( + *, + name: str = 'cortex_mask_wf', +): + """Create a cortical surface mask from a surface file. + + Workflow Graph + .. workflow:: + :graph2use: orig + :simple_form: yes + + from smriprep.workflows.surfaces import init_cortex_mask_wf + wf = init_cortex_mask_wf() + + Inputs + ------ + midthickness + FreeSurfer midthickness surface file in GIFTI format + thickness + FreeSurfer thickness file in GIFTI format + + Outputs + ------- + roi + Cortical surface mask in GIFTI format + """ + DEFAULT_MEMORY_MIN_GB = 0.01 + + workflow = Workflow(name=name) + + inputnode = pe.Node( + niu.IdentityInterface(fields=['midthickness', 'thickness']), + name='inputnode', + ) + outputnode = pe.Node(niu.IdentityInterface(fields=['roi']), name='outputnode') + + itersource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='itersource', + iterables=[('hemi', ['L', 'R'])], + ) + select_surfaces = pe.Node( + KeySelect( + fields=['midthickness', 'thickness'], + keys=['L', 'R'], + ), + name='select_surfaces', + run_without_submitting=True, + ) + + # Thickness is presumably already positive, but HCP uses abs(-thickness) + abs_thickness = pe.Node(MetricMath(metric='thickness', operation='abs'), name='abs_thickness') + + # Native ROI is thickness > 0, with holes and islands filled + initial_roi = pe.Node(MetricMath(metric='roi', operation='bin'), name='initial_roi') + fill_holes = pe.Node(MetricFillHoles(), name='fill_holes', mem_gb=DEFAULT_MEMORY_MIN_GB) + native_roi = pe.Node(MetricRemoveIslands(), name='native_roi', mem_gb=DEFAULT_MEMORY_MIN_GB) + + workflow.connect([ + (inputnode, select_surfaces, [ + ('thickness', 'thickness'), + ('midthickness', 'midthickness'), + ]), + (itersource, select_surfaces, [('hemi', 'key')]), + (itersource, abs_thickness, [('hemi', 'hemisphere')]), + (select_surfaces, abs_thickness, [('thickness', 'metric_file')]), (itersource, initial_roi, [('hemi', 'hemisphere')]), (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), (select_surfaces, fill_holes, [('midthickness', 'surface_file')]), From 0bfc59635d5ab8d76f6be534b0b7f802ad0e60b2 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 31 Jul 2025 16:20:02 -0400 Subject: [PATCH 02/26] Keep working. --- src/smriprep/workflows/anatomical.py | 53 ++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 03ca1d9cb8..c4551f610d 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1392,27 +1392,52 @@ def init_anat_fit_wf( if len(precomputed.get('cortex_mask', [])) < 2: LOGGER.info('ANAT Stage 11: Creating cortical surface mask') anat_cortex_mask_wf = init_cortex_mask_wf() - ds_cortex_mask_wf = init_ds_mask_wf( - bids_root=bids_root, - output_dir=output_dir, - mask_type='roi', - name='ds_cortex_mask_wf', - extra_entities={'extension': '.label.gii'}, - ) workflow.connect([ (surfaces_buffer, anat_cortex_mask_wf, [ ('midthickness', 'inputnode.midthickness'), ('thickness', 'inputnode.thickness'), ]), - (anat_cortex_mask_wf, ds_cortex_mask_wf, [ - ('outputnode.cortex_mask', 'inputnode.mask_file'), - ]), - (surfaces_buffer, ds_cortex_mask_wf, [ - ('midthickness', 'inputnode.source_files'), - ('thickness', 'inputnode.source_files'), + ]) # fmt:skip + + # Combine the inputs into a list + combine_inputs = pe.Node( + niu.Merge(2), + name='combine_inputs', + ) + workflow.connect([ + (surfaces_buffer, combine_inputs, [ + ('midthickness', 'in1'), + ('thickness', 'in2'), ]), - (ds_cortex_mask_wf, outputnode, [('outputnode.mask_file', 'cortex_mask')]), ]) # fmt:skip + + # Merge outputs into a single list + merge_outputs = pe.Node( + niu.Merge(2), + name='merge_outputs', + ) + workflow.connect([(merge_outputs, outputnode, [('out', 'cortex_mask')])]) + + # Need to loop over hemispheres here. + for i_hemi, hemi in enumerate(['L', 'R']): + ds_cortex_mask_wf = init_ds_mask_wf( + bids_root=bids_root, + output_dir=output_dir, + mask_type='roi', + name=f'ds_cortex_mask_wf_{hemi}', + extra_entities={'extension': '.label.gii', 'hemi': hemi}, + ) + # Select the hemisphere-specific mask + select_mask = pe.Node( + niu.Select(index=i_hemi), + name=f'select_mask_{hemi}', + ) + workflow.connect([ + (anat_cortex_mask_wf, select_mask, [('outputnode.cortex_mask', 'inlist')]), + (select_mask, ds_cortex_mask_wf, [('out', 'inputnode.mask_file')]), + (combine_inputs, ds_cortex_mask_wf, [('out', 'inputnode.source_files')]), + (ds_cortex_mask_wf, merge_outputs, [('outputnode.mask_file', f'in{i_hemi + 1}')]), + ]) # fmt:skip else: LOGGER.info('ANAT Stage 11: Found pre-computed cortical surface mask') outputnode.inputs.cortex_mask = sorted(precomputed['cortex_mask']) From 5ee51d5cccd15f8901d256d23f5a37aa9e6f8fa2 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 31 Jul 2025 16:23:11 -0400 Subject: [PATCH 03/26] Fix connections. --- src/smriprep/workflows/anatomical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index c4551f610d..210f53d771 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -430,12 +430,12 @@ def init_anat_preproc_wf( f"outputnode.sphere_reg_{'msm' if msm_sulc else 'fsLR'}", 'inputnode.sphere_reg_fsLR', ), + ('outputnode.cortex_mask', 'inputnode.roi'), ]), (hcp_morphometrics_wf, morph_grayords_wf, [ ('outputnode.curv', 'inputnode.curv'), ('outputnode.sulc', 'inputnode.sulc'), ('outputnode.thickness', 'inputnode.thickness'), - ('outputnode.roi', 'inputnode.roi'), ]), (resample_surfaces_wf, morph_grayords_wf, [ ('outputnode.midthickness_fsLR', 'inputnode.midthickness_fsLR'), From 766df5d681e22dee3d4d70ab8d5aea27722414af Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 31 Jul 2025 16:25:28 -0400 Subject: [PATCH 04/26] Update test outputs. --- .circleci/ds005_outputs.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/ds005_outputs.txt b/.circleci/ds005_outputs.txt index c96a92743c..042cbf0636 100644 --- a/.circleci/ds005_outputs.txt +++ b/.circleci/ds005_outputs.txt @@ -22,6 +22,7 @@ smriprep/sub-01/anat/sub-01_dseg.nii.gz smriprep/sub-01/anat/sub-01_from-fsnative_to-T1w_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_from-T1w_to-fsnative_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_hemi-L_curv.shape.gii +smriprep/sub-01/anat/sub-01_hemi-L_desc-cortex_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-L_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_midthickness.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_pial.surf.gii @@ -36,6 +37,7 @@ smriprep/sub-01/anat/sub-01_hemi-L_sulc.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_thickness.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_white.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_curv.shape.gii +smriprep/sub-01/anat/sub-01_hemi-R_desc-cortex_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-R_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_midthickness.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_pial.surf.gii From 1d1b97a27835a419c6dadd104fc3a97fbd172a98 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 31 Jul 2025 16:27:56 -0400 Subject: [PATCH 05/26] Fix? --- src/smriprep/workflows/anatomical.py | 6 +++--- src/smriprep/workflows/surfaces.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 210f53d771..979ad00c88 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1391,9 +1391,9 @@ def init_anat_fit_wf( # Stage 11: Cortical surface mask if len(precomputed.get('cortex_mask', [])) < 2: LOGGER.info('ANAT Stage 11: Creating cortical surface mask') - anat_cortex_mask_wf = init_cortex_mask_wf() + cortex_mask_wf = init_cortex_mask_wf() workflow.connect([ - (surfaces_buffer, anat_cortex_mask_wf, [ + (surfaces_buffer, cortex_mask_wf, [ ('midthickness', 'inputnode.midthickness'), ('thickness', 'inputnode.thickness'), ]), @@ -1433,7 +1433,7 @@ def init_anat_fit_wf( name=f'select_mask_{hemi}', ) workflow.connect([ - (anat_cortex_mask_wf, select_mask, [('outputnode.cortex_mask', 'inlist')]), + (cortex_mask_wf, select_mask, [('outputnode.roi', 'inlist')]), (select_mask, ds_cortex_mask_wf, [('out', 'inputnode.mask_file')]), (combine_inputs, ds_cortex_mask_wf, [('out', 'inputnode.source_files')]), (ds_cortex_mask_wf, merge_outputs, [('outputnode.mask_file', f'in{i_hemi + 1}')]), diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index a45990c647..742195e3a6 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1179,7 +1179,7 @@ def init_cortex_mask_wf( Outputs ------- - roi + roi : list of two strings Cortical surface mask in GIFTI format """ DEFAULT_MEMORY_MIN_GB = 0.01 From bca14f77e7726564c3217ddf1df10c630c07f563 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 1 Aug 2025 09:00:24 -0400 Subject: [PATCH 06/26] Address @effigies' review. --- src/smriprep/workflows/anatomical.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 979ad00c88..6fa4978c1c 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1249,13 +1249,13 @@ def init_anat_fit_wf( ('outputnode.subjects_dir', 'inputnode.subjects_dir'), ('outputnode.fsnative2t1w_xfm', 'inputnode.fsnative2anat_xfm'), ]), - (gifti_surfaces_wf, surfaces_buffer, [ - (f'outputnode.{surf}', surf) for surf in surfs - ]), (sourcefile_buffer, ds_surfaces_wf, [('source_files', 'inputnode.source_files')]), (gifti_surfaces_wf, ds_surfaces_wf, [ (f'outputnode.{surf}', f'inputnode.{surf}') for surf in surfs ]), + (ds_surfaces_wf, surfaces_buffer, [ + (f'outputnode.{surf}', surf) for surf in surfs + ]), ]) # fmt:on if spheres: @@ -1295,13 +1295,13 @@ def init_anat_fit_wf( ('outputnode.subject_id', 'inputnode.subject_id'), ('outputnode.subjects_dir', 'inputnode.subjects_dir'), ]), - (gifti_morph_wf, surfaces_buffer, [ - (f'outputnode.{metric}', metric) for metric in metrics - ]), (sourcefile_buffer, ds_morph_wf, [('source_files', 'inputnode.source_files')]), (gifti_morph_wf, ds_morph_wf, [ (f'outputnode.{metric}', f'inputnode.{metric}') for metric in metrics ]), + (ds_morph_wf, surfaces_buffer, [ + (f'outputnode.{metric}', metric) for metric in metrics + ]), ]) # fmt:on From 322f61dd30a678aa6655ba33ab0a5b03ee6d1a77 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 1 Aug 2025 10:34:31 -0400 Subject: [PATCH 07/26] Try to fix. I realize that using KeySelect is probably cleaner, but I am not very familiar with that approach. --- src/smriprep/workflows/anatomical.py | 67 +++++++++++++++------------- src/smriprep/workflows/surfaces.py | 43 ++++++------------ 2 files changed, 50 insertions(+), 60 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 278477d2c5..0b4023436d 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1349,35 +1349,43 @@ def init_anat_fit_wf( # Stage 11: Cortical surface mask if len(precomputed.get('cortex_mask', [])) < 2: LOGGER.info('ANAT Stage 11: Creating cortical surface mask') - cortex_mask_wf = init_cortex_mask_wf() - workflow.connect([ - (surfaces_buffer, cortex_mask_wf, [ - ('midthickness', 'inputnode.midthickness'), - ('thickness', 'inputnode.thickness'), - ]), - ]) # fmt:skip - - # Combine the inputs into a list - combine_inputs = pe.Node( - niu.Merge(2), - name='combine_inputs', - ) - workflow.connect([ - (surfaces_buffer, combine_inputs, [ - ('midthickness', 'in1'), - ('thickness', 'in2'), - ]), - ]) # fmt:skip # Merge outputs into a single list - merge_outputs = pe.Node( + merge_cortex_masks = pe.Node( niu.Merge(2), - name='merge_outputs', + name='merge_cortex_masks', ) - workflow.connect([(merge_outputs, outputnode, [('out', 'cortex_mask')])]) + workflow.connect([(merge_cortex_masks, outputnode, [('out', 'cortex_mask')])]) - # Need to loop over hemispheres here. for i_hemi, hemi in enumerate(['L', 'R']): + select_midthickness = pe.Node( + niu.Select(index=i_hemi), + name=f'select_midthickness_{hemi}', + ) + select_thickness = pe.Node( + niu.Select(index=i_hemi), + name=f'select_thickness_{hemi}', + ) + cortex_mask_wf = init_cortex_mask_wf(name=f'cortex_mask_wf_{hemi}') + cortex_mask_wf.inputs.inputnode.hemi = hemi + + workflow.connect([ + (surfaces_buffer, select_midthickness, [('midthickness', 'midthickness')]), + (surfaces_buffer, select_thickness, [('thickness', 'thickness')]), + (select_midthickness, cortex_mask_wf, [('out', 'inputnode.midthickness')]), + (select_thickness, cortex_mask_wf, [('out', 'inputnode.thickness')]), + ]) # fmt:skip + + # Combine the inputs into a list + combine_inputs = pe.Node( + niu.Merge(2), + name='combine_inputs', + ) + workflow.connect([ + (select_midthickness, combine_inputs, [('out', 'in1')]), + (select_thickness, combine_inputs, [('out', 'in2')]), + ]) # fmt:skip + ds_cortex_mask_wf = init_ds_mask_wf( bids_root=bids_root, output_dir=output_dir, @@ -1385,16 +1393,13 @@ def init_anat_fit_wf( name=f'ds_cortex_mask_wf_{hemi}', extra_entities={'extension': '.label.gii', 'hemi': hemi}, ) - # Select the hemisphere-specific mask - select_mask = pe.Node( - niu.Select(index=i_hemi), - name=f'select_mask_{hemi}', - ) + workflow.connect([ - (cortex_mask_wf, select_mask, [('outputnode.roi', 'inlist')]), - (select_mask, ds_cortex_mask_wf, [('out', 'inputnode.mask_file')]), + (cortex_mask_wf, ds_cortex_mask_wf, [('outputnode.roi', 'inputnode.mask_file')]), (combine_inputs, ds_cortex_mask_wf, [('out', 'inputnode.source_files')]), - (ds_cortex_mask_wf, merge_outputs, [('outputnode.mask_file', f'in{i_hemi + 1}')]), + (ds_cortex_mask_wf, merge_cortex_masks, [ + ('outputnode.mask_file', f'in{i_hemi + 1}'), + ]), ]) # fmt:skip else: LOGGER.info('ANAT Stage 11: Found pre-computed cortical surface mask') diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index 742195e3a6..c96a4520f9 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1172,14 +1172,16 @@ def init_cortex_mask_wf( Inputs ------ - midthickness - FreeSurfer midthickness surface file in GIFTI format - thickness - FreeSurfer thickness file in GIFTI format + midthickness : str + One hemisphere's FreeSurfer midthickness surface file in GIFTI format + thickness : str + One hemisphere's FreeSurfer thickness file in GIFTI format + hemi : {'L', 'R'} + Hemisphere indicator Outputs ------- - roi : list of two strings + roi : str Cortical surface mask in GIFTI format """ DEFAULT_MEMORY_MIN_GB = 0.01 @@ -1187,25 +1189,11 @@ def init_cortex_mask_wf( workflow = Workflow(name=name) inputnode = pe.Node( - niu.IdentityInterface(fields=['midthickness', 'thickness']), + niu.IdentityInterface(fields=['midthickness', 'thickness', 'hemi']), name='inputnode', ) outputnode = pe.Node(niu.IdentityInterface(fields=['roi']), name='outputnode') - itersource = pe.Node( - niu.IdentityInterface(fields=['hemi']), - name='itersource', - iterables=[('hemi', ['L', 'R'])], - ) - select_surfaces = pe.Node( - KeySelect( - fields=['midthickness', 'thickness'], - keys=['L', 'R'], - ), - name='select_surfaces', - run_without_submitting=True, - ) - # Thickness is presumably already positive, but HCP uses abs(-thickness) abs_thickness = pe.Node(MetricMath(metric='thickness', operation='abs'), name='abs_thickness') @@ -1215,17 +1203,14 @@ def init_cortex_mask_wf( native_roi = pe.Node(MetricRemoveIslands(), name='native_roi', mem_gb=DEFAULT_MEMORY_MIN_GB) workflow.connect([ - (inputnode, select_surfaces, [ - ('thickness', 'thickness'), - ('midthickness', 'midthickness'), + (inputnode, abs_thickness, [ + ('hemi', 'hemisphere'), + ('thickness', 'metric_file'), ]), - (itersource, select_surfaces, [('hemi', 'key')]), - (itersource, abs_thickness, [('hemi', 'hemisphere')]), - (select_surfaces, abs_thickness, [('thickness', 'metric_file')]), - (itersource, initial_roi, [('hemi', 'hemisphere')]), + (inputnode, initial_roi, [('hemi', 'hemisphere')]), (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), - (select_surfaces, fill_holes, [('midthickness', 'surface_file')]), - (select_surfaces, native_roi, [('midthickness', 'surface_file')]), + (inputnode, fill_holes, [('midthickness', 'surface_file')]), + (inputnode, native_roi, [('midthickness', 'surface_file')]), (initial_roi, fill_holes, [('metric_file', 'metric_file')]), (fill_holes, native_roi, [('out_file', 'metric_file')]), (native_roi, outputnode, [('out_file', 'roi')]), From 82e53f30fdcfdd0e838a3d2e47633ddeba34189a Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 1 Aug 2025 10:36:41 -0400 Subject: [PATCH 08/26] Update anatomical.py --- src/smriprep/workflows/anatomical.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 0b4023436d..4d2b287f48 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1379,7 +1379,7 @@ def init_anat_fit_wf( # Combine the inputs into a list combine_inputs = pe.Node( niu.Merge(2), - name='combine_inputs', + name=f'combine_inputs_{hemi}', ) workflow.connect([ (select_midthickness, combine_inputs, [('out', 'in1')]), From 52386640d4306600547090a004d6d36973de4cba Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 1 Aug 2025 11:24:45 -0400 Subject: [PATCH 09/26] Keep working. --- src/smriprep/data/io_spec.json | 8 ++++++++ src/smriprep/workflows/anatomical.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/smriprep/data/io_spec.json b/src/smriprep/data/io_spec.json index 54edaf3ac1..b3c97c588b 100644 --- a/src/smriprep/data/io_spec.json +++ b/src/smriprep/data/io_spec.json @@ -144,6 +144,14 @@ "desc": "msmsulc", "suffix": "sphere", "extension": ".surf.gii" + }, + "cortex_mask": { + "datatype": "anat", + "hemi": ["L", "R"], + "space": null, + "desc": "cortex", + "suffix": "mask", + "extension": ".label.gii" } }, "masks": { diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 4d2b287f48..e17a2f2bdc 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1370,8 +1370,8 @@ def init_anat_fit_wf( cortex_mask_wf.inputs.inputnode.hemi = hemi workflow.connect([ - (surfaces_buffer, select_midthickness, [('midthickness', 'midthickness')]), - (surfaces_buffer, select_thickness, [('thickness', 'thickness')]), + (surfaces_buffer, select_midthickness, [('midthickness', 'inlist')]), + (surfaces_buffer, select_thickness, [('thickness', 'inlist')]), (select_midthickness, cortex_mask_wf, [('out', 'inputnode.midthickness')]), (select_thickness, cortex_mask_wf, [('out', 'inputnode.thickness')]), ]) # fmt:skip From aed07ddba2e3f379036988799588edaf28142661 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 1 Aug 2025 12:38:36 -0400 Subject: [PATCH 10/26] Fix name of mask. --- .circleci/ds005_outputs.txt | 4 ++-- src/smriprep/data/io_spec.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/ds005_outputs.txt b/.circleci/ds005_outputs.txt index 042cbf0636..c987543b7f 100644 --- a/.circleci/ds005_outputs.txt +++ b/.circleci/ds005_outputs.txt @@ -22,7 +22,7 @@ smriprep/sub-01/anat/sub-01_dseg.nii.gz smriprep/sub-01/anat/sub-01_from-fsnative_to-T1w_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_from-T1w_to-fsnative_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_hemi-L_curv.shape.gii -smriprep/sub-01/anat/sub-01_hemi-L_desc-cortex_mask.label.gii +smriprep/sub-01/anat/sub-01_hemi-L_desc-roi_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-L_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_midthickness.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_pial.surf.gii @@ -37,7 +37,7 @@ smriprep/sub-01/anat/sub-01_hemi-L_sulc.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_thickness.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_white.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_curv.shape.gii -smriprep/sub-01/anat/sub-01_hemi-R_desc-cortex_mask.label.gii +smriprep/sub-01/anat/sub-01_hemi-R_desc-roi_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-R_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_midthickness.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_pial.surf.gii diff --git a/src/smriprep/data/io_spec.json b/src/smriprep/data/io_spec.json index b3c97c588b..2f48352fcf 100644 --- a/src/smriprep/data/io_spec.json +++ b/src/smriprep/data/io_spec.json @@ -149,7 +149,7 @@ "datatype": "anat", "hemi": ["L", "R"], "space": null, - "desc": "cortex", + "desc": "roi", "suffix": "mask", "extension": ".label.gii" } From ed1e14caa1ec34f3933b4119caf20f13c96cfd49 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Sun, 3 Aug 2025 08:50:56 -0400 Subject: [PATCH 11/26] chore: Depend on niworkflows@master --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f6e7bed85e..9b9eb5febe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "nibabel >= 4.0.1", "nipype >= 1.8.5", "nireports >= 25.2.0", - "niworkflows >= 1.13.4", + "niworkflows @ git+https://github.com/nipreps/niworkflows.git@master", "numpy >= 1.24", "packaging >= 24", "pybids >= 0.16", From 1bcdf261da56ed2b821d8dfbb8f473cc2825d506 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 09:06:43 -0400 Subject: [PATCH 12/26] Update ds005_outputs.txt --- .circleci/ds005_outputs.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/ds005_outputs.txt b/.circleci/ds005_outputs.txt index c987543b7f..e4f9dd818e 100644 --- a/.circleci/ds005_outputs.txt +++ b/.circleci/ds005_outputs.txt @@ -22,6 +22,7 @@ smriprep/sub-01/anat/sub-01_dseg.nii.gz smriprep/sub-01/anat/sub-01_from-fsnative_to-T1w_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_from-T1w_to-fsnative_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_hemi-L_curv.shape.gii +smriprep/sub-01/anat/sub-01_hemi-L_desc-roi_mask.json smriprep/sub-01/anat/sub-01_hemi-L_desc-roi_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-L_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_midthickness.surf.gii @@ -37,6 +38,7 @@ smriprep/sub-01/anat/sub-01_hemi-L_sulc.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_thickness.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_white.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_curv.shape.gii +smriprep/sub-01/anat/sub-01_hemi-R_desc-roi_mask.json smriprep/sub-01/anat/sub-01_hemi-R_desc-roi_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-R_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_midthickness.surf.gii From 1e88f27b5dc0ce8b7639d8e8788762cc41761652 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 15:47:26 -0400 Subject: [PATCH 13/26] Change desc-roi to desc-cortex. --- .circleci/ds005_outputs.txt | 8 ++++---- src/smriprep/workflows/anatomical.py | 2 +- src/smriprep/workflows/outputs.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/ds005_outputs.txt b/.circleci/ds005_outputs.txt index e4f9dd818e..2167c1bcc1 100644 --- a/.circleci/ds005_outputs.txt +++ b/.circleci/ds005_outputs.txt @@ -22,8 +22,8 @@ smriprep/sub-01/anat/sub-01_dseg.nii.gz smriprep/sub-01/anat/sub-01_from-fsnative_to-T1w_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_from-T1w_to-fsnative_mode-image_xfm.txt smriprep/sub-01/anat/sub-01_hemi-L_curv.shape.gii -smriprep/sub-01/anat/sub-01_hemi-L_desc-roi_mask.json -smriprep/sub-01/anat/sub-01_hemi-L_desc-roi_mask.label.gii +smriprep/sub-01/anat/sub-01_hemi-L_desc-cortex_mask.json +smriprep/sub-01/anat/sub-01_hemi-L_desc-cortex_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-L_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_midthickness.surf.gii smriprep/sub-01/anat/sub-01_hemi-L_pial.surf.gii @@ -38,8 +38,8 @@ smriprep/sub-01/anat/sub-01_hemi-L_sulc.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_thickness.shape.gii smriprep/sub-01/anat/sub-01_hemi-L_white.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_curv.shape.gii -smriprep/sub-01/anat/sub-01_hemi-R_desc-roi_mask.json -smriprep/sub-01/anat/sub-01_hemi-R_desc-roi_mask.label.gii +smriprep/sub-01/anat/sub-01_hemi-R_desc-cortex_mask.json +smriprep/sub-01/anat/sub-01_hemi-R_desc-cortex_mask.label.gii smriprep/sub-01/anat/sub-01_hemi-R_inflated.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_midthickness.surf.gii smriprep/sub-01/anat/sub-01_hemi-R_pial.surf.gii diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index e17a2f2bdc..a6c86b9094 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1389,7 +1389,7 @@ def init_anat_fit_wf( ds_cortex_mask_wf = init_ds_mask_wf( bids_root=bids_root, output_dir=output_dir, - mask_type='roi', + mask_type='cortex', name=f'ds_cortex_mask_wf_{hemi}', extra_entities={'extension': '.label.gii', 'hemi': hemi}, ) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index a2f946356e..54700ec31b 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -325,7 +325,7 @@ def init_ds_mask_wf( *, bids_root: str, output_dir: str, - mask_type: ty.Literal['brain', 'roi', 'ribbon'], + mask_type: ty.Literal['brain', 'roi', 'ribbon', 'cortex'], extra_entities: dict | None = None, name='ds_mask_wf', ): From 66e181a970b9423cf74daae8fdea96ad99ca73ec Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 16:54:24 -0400 Subject: [PATCH 14/26] Simplify base workflow. --- src/smriprep/workflows/anatomical.py | 73 ++++++----------- src/smriprep/workflows/outputs.py | 79 +++++++++++++++++++ src/smriprep/workflows/surfaces.py | 114 +++++++++++++++++++-------- 3 files changed, 183 insertions(+), 83 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index a6c86b9094..980eb01dd6 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -69,6 +69,7 @@ init_ds_fs_segs_wf, init_ds_grayord_metrics_wf, init_ds_mask_wf, + init_ds_surface_masks_wf, init_ds_surface_metrics_wf, init_ds_surfaces_wf, init_ds_template_registration_wf, @@ -78,7 +79,7 @@ ) from .surfaces import ( init_anat_ribbon_wf, - init_cortex_mask_wf, + init_cortex_masks_wf, init_fsLR_reg_wf, init_gifti_morphometrics_wf, init_gifti_surfaces_wf, @@ -1350,57 +1351,27 @@ def init_anat_fit_wf( if len(precomputed.get('cortex_mask', [])) < 2: LOGGER.info('ANAT Stage 11: Creating cortical surface mask') - # Merge outputs into a single list - merge_cortex_masks = pe.Node( - niu.Merge(2), - name='merge_cortex_masks', - ) - workflow.connect([(merge_cortex_masks, outputnode, [('out', 'cortex_mask')])]) - - for i_hemi, hemi in enumerate(['L', 'R']): - select_midthickness = pe.Node( - niu.Select(index=i_hemi), - name=f'select_midthickness_{hemi}', - ) - select_thickness = pe.Node( - niu.Select(index=i_hemi), - name=f'select_thickness_{hemi}', - ) - cortex_mask_wf = init_cortex_mask_wf(name=f'cortex_mask_wf_{hemi}') - cortex_mask_wf.inputs.inputnode.hemi = hemi - - workflow.connect([ - (surfaces_buffer, select_midthickness, [('midthickness', 'inlist')]), - (surfaces_buffer, select_thickness, [('thickness', 'inlist')]), - (select_midthickness, cortex_mask_wf, [('out', 'inputnode.midthickness')]), - (select_thickness, cortex_mask_wf, [('out', 'inputnode.thickness')]), - ]) # fmt:skip - - # Combine the inputs into a list - combine_inputs = pe.Node( - niu.Merge(2), - name=f'combine_inputs_{hemi}', - ) - workflow.connect([ - (select_midthickness, combine_inputs, [('out', 'in1')]), - (select_thickness, combine_inputs, [('out', 'in2')]), - ]) # fmt:skip - - ds_cortex_mask_wf = init_ds_mask_wf( - bids_root=bids_root, - output_dir=output_dir, - mask_type='cortex', - name=f'ds_cortex_mask_wf_{hemi}', - extra_entities={'extension': '.label.gii', 'hemi': hemi}, - ) + cortex_masks_wf = init_cortex_masks_wf() + workflow.connect([ + (surfaces_buffer, cortex_masks_wf, [ + ('midthickness', 'inputnode.midthickness'), + ('thickness', 'inputnode.thickness'), + ]), + ]) # fmt:skip - workflow.connect([ - (cortex_mask_wf, ds_cortex_mask_wf, [('outputnode.roi', 'inputnode.mask_file')]), - (combine_inputs, ds_cortex_mask_wf, [('out', 'inputnode.source_files')]), - (ds_cortex_mask_wf, merge_cortex_masks, [ - ('outputnode.mask_file', f'in{i_hemi + 1}'), - ]), - ]) # fmt:skip + ds_cortex_masks_wf = init_ds_surface_masks_wf( + output_dir=output_dir, + mask_type='cortex', + name='ds_cortex_masks_wf', + entities={'extension': '.label.gii'}, + ) + workflow.connect([ + (cortex_masks_wf, ds_cortex_masks_wf, [ + ('outputnode.cortex_masks', 'inputnode.mask_files'), + ('outputnode.source_files', 'inputnode.source_files'), + ]), + (ds_cortex_masks_wf, outputnode, [('outputnode.mask_files', 'cortex_mask')]), + ]) # fmt:skip else: LOGGER.info('ANAT Stage 11: Found pre-computed cortical surface mask') outputnode.inputs.cortex_mask = sorted(precomputed['cortex_mask']) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 54700ec31b..baa832fd00 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -1231,6 +1231,85 @@ def init_template_iterator_wf( return workflow +def init_ds_surface_masks_wf( + *, + output_dir: str, + mask_type: ty.Literal['cortex', 'roi', 'ribbon', 'brain'], + entities: dict[str, str] | None = None, + name='ds_surface_masks_wf', +) -> Workflow: + """Save GIFTI surface masks. + + Parameters + ---------- + output_dir : :class:`str` + Directory in which to save derivatives + mask_type : :class:`str` + Type of mask to save + entities : :class:`dict` of :class:`str` + Entities to include in outputs + name : :class:`str` + Workflow name (default: ds_surface_masks_wf) + + Inputs + ------ + source_files : list of lists of str + List of lists of source files. + Left hemisphere sources first, then right hemisphere sources. + mask_files : list of str + List of input mask files. + Left hemisphere mask first, then right hemisphere mask. + + Outputs + ------- + mask_files : list of str + List of output mask files. + Left hemisphere mask first, then right hemisphere mask. + """ + workflow = Workflow(name=name) + + if entities is None: + entities = {} + + inputnode = pe.Node( + niu.IdentityInterface(fields=['mask_files', 'source_files']), + name='inputnode', + ) + outputnode = pe.Node(niu.IdentityInterface(fields=['mask_files']), name='outputnode') + + sources = pe.MapNode( + niu.Function(function=_bids_relative), + name='sources', + iterfield='in_files', + ) + sources.inputs.bids_root = output_dir + + ds_mask = pe.MapNode( + DerivativesDataSink( + base_directory=output_dir, + hemi=['L', 'R'], + desc=mask_type, + **entities, + ), + iterfield=('in_file', 'hemi', 'source_file'), + name='ds_mask', + run_without_submitting=True, + ) + if mask_type == 'brain': + ds_mask.inputs.Type = 'Brain' + else: + ds_mask.inputs.Type = 'ROI' + + workflow.connect([ + (inputnode, ds_mask, [('mask_files', 'in_file')]), + (inputnode, sources, [('source_files', 'in_files')]), + (sources, ds_mask, [('out', 'source_file')]), + (ds_mask, outputnode, [('out_file', 'mask_files')]), + ]) # fmt:skip + + return workflow + + def _bids_relative(in_files, bids_root): from pathlib import Path diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index c96a4520f9..d66adbfa28 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1156,9 +1156,9 @@ def init_hcp_morphometrics_wf( return workflow -def init_cortex_mask_wf( +def init_cortex_masks_wf( *, - name: str = 'cortex_mask_wf', + name: str = 'cortex_masks_wf', ): """Create a cortical surface mask from a surface file. @@ -1167,55 +1167,100 @@ def init_cortex_mask_wf( :graph2use: orig :simple_form: yes - from smriprep.workflows.surfaces import init_cortex_mask_wf - wf = init_cortex_mask_wf() + from smriprep.workflows.surfaces import init_cortex_masks_wf + wf = init_cortex_masks_wf() Inputs ------ - midthickness : str - One hemisphere's FreeSurfer midthickness surface file in GIFTI format - thickness : str - One hemisphere's FreeSurfer thickness file in GIFTI format - hemi : {'L', 'R'} - Hemisphere indicator + midthickness : list of str + Each hemisphere's FreeSurfer midthickness surface file in GIFTI format + thickness : list of str + Each hemisphere's FreeSurfer thickness file in GIFTI format Outputs ------- - roi : str - Cortical surface mask in GIFTI format + cortex_masks : list of str + Cortical surface mask in GIFTI format for each hemisphere """ DEFAULT_MEMORY_MIN_GB = 0.01 workflow = Workflow(name=name) inputnode = pe.Node( - niu.IdentityInterface(fields=['midthickness', 'thickness', 'hemi']), + niu.IdentityInterface(fields=['midthickness', 'thickness']), name='inputnode', ) - outputnode = pe.Node(niu.IdentityInterface(fields=['roi']), name='outputnode') - - # Thickness is presumably already positive, but HCP uses abs(-thickness) - abs_thickness = pe.Node(MetricMath(metric='thickness', operation='abs'), name='abs_thickness') - - # Native ROI is thickness > 0, with holes and islands filled - initial_roi = pe.Node(MetricMath(metric='roi', operation='bin'), name='initial_roi') - fill_holes = pe.Node(MetricFillHoles(), name='fill_holes', mem_gb=DEFAULT_MEMORY_MIN_GB) - native_roi = pe.Node(MetricRemoveIslands(), name='native_roi', mem_gb=DEFAULT_MEMORY_MIN_GB) + outputnode = pe.Node(niu.IdentityInterface(fields=['cortex_masks']), name='outputnode') + # Combine the inputs into a list + combine_sources = pe.Node( + niu.Merge(2, no_flatten=True), + name='combine_sources', + ) workflow.connect([ - (inputnode, abs_thickness, [ - ('hemi', 'hemisphere'), - ('thickness', 'metric_file'), + (inputnode, combine_sources, [ + ('midthickness', 'in1'), + ('thickness', 'in2'), ]), - (inputnode, initial_roi, [('hemi', 'hemisphere')]), - (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), - (inputnode, fill_holes, [('midthickness', 'surface_file')]), - (inputnode, native_roi, [('midthickness', 'surface_file')]), - (initial_roi, fill_holes, [('metric_file', 'metric_file')]), - (fill_holes, native_roi, [('out_file', 'metric_file')]), - (native_roi, outputnode, [('out_file', 'roi')]), + (combine_sources, outputnode, [(('out', _transpose_lol), 'source_files')]), ]) # fmt:skip + combine_masks = pe.Node( + niu.Merge(2), + name='combine_masks', + ) + workflow.connect([(combine_masks, outputnode, [('out', 'cortex_masks')])]) + + for i_hemi, hemi in enumerate(['L', 'R']): + select_midthickness = pe.Node( + niu.Select(index=i_hemi), + name=f'select_midthickness_{hemi}', + ) + select_thickness = pe.Node( + niu.Select(index=i_hemi), + name=f'select_thickness_{hemi}', + ) + workflow.connect([ + (inputnode, select_midthickness, [('midthickness', 'inlist')]), + (inputnode, select_thickness, [('thickness', 'inlist')]), + ]) # fmt:skip + + # Thickness is presumably already positive, but HCP uses abs(-thickness) + abs_thickness = pe.Node( + MetricMath(metric='thickness', operation='abs'), + name=f'abs_thickness_{hemi}', + ) + + # Native ROI is thickness > 0, with holes and islands filled + initial_roi = pe.Node( + MetricMath(metric='roi', operation='bin'), + name=f'initial_roi_{hemi}', + ) + fill_holes = pe.Node( + MetricFillHoles(), + name=f'fill_holes_{hemi}', + mem_gb=DEFAULT_MEMORY_MIN_GB, + ) + native_roi = pe.Node( + MetricRemoveIslands(), + name=f'native_roi_{hemi}', + mem_gb=DEFAULT_MEMORY_MIN_GB, + ) + + workflow.connect([ + (inputnode, abs_thickness, [ + ('hemi', 'hemisphere'), + ('thickness', 'metric_file'), + ]), + (inputnode, initial_roi, [('hemi', 'hemisphere')]), + (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), + (inputnode, fill_holes, [('midthickness', 'surface_file')]), + (inputnode, native_roi, [('midthickness', 'surface_file')]), + (initial_roi, fill_holes, [('metric_file', 'metric_file')]), + (fill_holes, native_roi, [('out_file', 'metric_file')]), + (native_roi, combine_masks, [('out_file', f'in{i_hemi + 1}')]), + ]) # fmt:skip + return workflow @@ -1758,3 +1803,8 @@ def _select_seg(in_files, segmentation): def _repeat(seq: list, count: int) -> list: return seq * count + + +def _transpose_lol(inlist): + """Transpose a list of lists.""" + return list(map(list, zip(*inlist, strict=False))) From c69c7c9d5cb8d0e00d1448cb95c67464c91bc834 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 16:55:41 -0400 Subject: [PATCH 15/26] Fix spacing. --- src/smriprep/workflows/surfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index d66adbfa28..fa444416db 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1225,7 +1225,7 @@ def init_cortex_masks_wf( (inputnode, select_thickness, [('thickness', 'inlist')]), ]) # fmt:skip - # Thickness is presumably already positive, but HCP uses abs(-thickness) + # Thickness is presumably already positive, but HCP uses abs(-thickness) abs_thickness = pe.Node( MetricMath(metric='thickness', operation='abs'), name=f'abs_thickness_{hemi}', From 87547b71576ed8a92daff92c4fb7385251e75551 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 17:00:39 -0400 Subject: [PATCH 16/26] Update surfaces.py --- src/smriprep/workflows/surfaces.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index fa444416db..2948c119fb 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1190,7 +1190,10 @@ def init_cortex_masks_wf( niu.IdentityInterface(fields=['midthickness', 'thickness']), name='inputnode', ) - outputnode = pe.Node(niu.IdentityInterface(fields=['cortex_masks']), name='outputnode') + outputnode = pe.Node( + niu.IdentityInterface(fields=['cortex_masks', 'source_files']), + name='outputnode', + ) # Combine the inputs into a list combine_sources = pe.Node( From a9338d0449b784ad45a9fe64ad83ac6e88b9c697 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 18:31:33 -0400 Subject: [PATCH 17/26] Update surfaces.py --- src/smriprep/workflows/surfaces.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index 2948c119fb..ab2d939974 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1236,7 +1236,7 @@ def init_cortex_masks_wf( # Native ROI is thickness > 0, with holes and islands filled initial_roi = pe.Node( - MetricMath(metric='roi', operation='bin'), + MetricMath(metric='roi', operation='bin', hemisphere=hemi), name=f'initial_roi_{hemi}', ) fill_holes = pe.Node( @@ -1245,17 +1245,13 @@ def init_cortex_masks_wf( mem_gb=DEFAULT_MEMORY_MIN_GB, ) native_roi = pe.Node( - MetricRemoveIslands(), + MetricRemoveIslands(hemisphere=hemi), name=f'native_roi_{hemi}', mem_gb=DEFAULT_MEMORY_MIN_GB, ) workflow.connect([ - (inputnode, abs_thickness, [ - ('hemi', 'hemisphere'), - ('thickness', 'metric_file'), - ]), - (inputnode, initial_roi, [('hemi', 'hemisphere')]), + (inputnode, abs_thickness, [('thickness', 'metric_file')]), (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), (inputnode, fill_holes, [('midthickness', 'surface_file')]), (inputnode, native_roi, [('midthickness', 'surface_file')]), From 388d942af658e0c6f09da207c73fffaa103a54cc Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 18:37:43 -0400 Subject: [PATCH 18/26] Fix. --- src/smriprep/workflows/surfaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index ab2d939974..851c1a3c4d 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1230,7 +1230,7 @@ def init_cortex_masks_wf( # Thickness is presumably already positive, but HCP uses abs(-thickness) abs_thickness = pe.Node( - MetricMath(metric='thickness', operation='abs'), + MetricMath(metric='thickness', operation='abs', hemisphere=hemi), name=f'abs_thickness_{hemi}', ) @@ -1245,7 +1245,7 @@ def init_cortex_masks_wf( mem_gb=DEFAULT_MEMORY_MIN_GB, ) native_roi = pe.Node( - MetricRemoveIslands(hemisphere=hemi), + MetricRemoveIslands(), name=f'native_roi_{hemi}', mem_gb=DEFAULT_MEMORY_MIN_GB, ) From b6a567221d49c895cc73ffe9fcc25cb19a8308f6 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 4 Aug 2025 19:47:31 -0400 Subject: [PATCH 19/26] Update surfaces.py --- src/smriprep/workflows/surfaces.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index 851c1a3c4d..2aa7a3c5ba 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1251,10 +1251,10 @@ def init_cortex_masks_wf( ) workflow.connect([ - (inputnode, abs_thickness, [('thickness', 'metric_file')]), + (select_thickness, abs_thickness, [('out', 'metric_file')]), (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), - (inputnode, fill_holes, [('midthickness', 'surface_file')]), - (inputnode, native_roi, [('midthickness', 'surface_file')]), + (select_midthickness, fill_holes, [('out', 'surface_file')]), + (select_midthickness, native_roi, [('out', 'surface_file')]), (initial_roi, fill_holes, [('metric_file', 'metric_file')]), (fill_holes, native_roi, [('out_file', 'metric_file')]), (native_roi, combine_masks, [('out_file', f'in{i_hemi + 1}')]), From dfddba8233004ddea3a42c28b0ef1c41d1a4bcdf Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 5 Aug 2025 09:00:37 -0400 Subject: [PATCH 20/26] metadata fields don't work with MapNodes. --- src/smriprep/workflows/outputs.py | 72 ++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index baa832fd00..1e022612d8 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -1277,35 +1277,55 @@ def init_ds_surface_masks_wf( ) outputnode = pe.Node(niu.IdentityInterface(fields=['mask_files']), name='outputnode') - sources = pe.MapNode( - niu.Function(function=_bids_relative), - name='sources', - iterfield='in_files', + combine_masks = pe.Node( + niu.Merge(2), + name='combine_masks', ) - sources.inputs.bids_root = output_dir + workflow.connect(combine_masks, outputnode, [('out', 'mask_files')]) - ds_mask = pe.MapNode( - DerivativesDataSink( - base_directory=output_dir, - hemi=['L', 'R'], - desc=mask_type, - **entities, - ), - iterfield=('in_file', 'hemi', 'source_file'), - name='ds_mask', - run_without_submitting=True, - ) - if mask_type == 'brain': - ds_mask.inputs.Type = 'Brain' - else: - ds_mask.inputs.Type = 'ROI' + for i_hemi, hemi in enumerate(['L', 'R']): + select_mask = pe.Node( + niu.Select(index=i_hemi), + name=f'select_mask_{hemi}', + run_without_submitting=True, + ) + workflow.connect(inputnode, select_mask, [('mask_files', 'inlist')]) - workflow.connect([ - (inputnode, ds_mask, [('mask_files', 'in_file')]), - (inputnode, sources, [('source_files', 'in_files')]), - (sources, ds_mask, [('out', 'source_file')]), - (ds_mask, outputnode, [('out_file', 'mask_files')]), - ]) # fmt:skip + select_source = pe.Node( + niu.Select(index=i_hemi), + name=f'select_source_{hemi}', + run_without_submitting=True, + ) + workflow.connect(inputnode, select_source, [('source_files', 'inlist')]) + + sources = pe.Node( + niu.Function(function=_bids_relative), + name=f'sources_{hemi}', + ) + sources.inputs.bids_root = output_dir + + ds_mask = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + hemi=hemi, + desc=mask_type, + **entities, + ), + name=f'ds_mask_{hemi}', + run_without_submitting=True, + ) + if mask_type == 'brain': + ds_mask.inputs.Type = 'Brain' + else: + ds_mask.inputs.Type = 'ROI' + + workflow.connect([ + (select_mask, ds_mask, [('out', 'in_file')]), + (select_source, sources, [('out', 'in_files')]), + (select_source, ds_mask, [('out', 'source_file')]), + (sources, ds_mask, [('out', 'Sources')]), + (ds_mask, combine_masks, [('out_file', f'in{i_hemi + 1}')]), + ]) # fmt:skip return workflow From ede33ff801f6d9426ae247fbba45680f0ddd3ad8 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 5 Aug 2025 09:04:08 -0400 Subject: [PATCH 21/26] Fix connections. --- src/smriprep/workflows/outputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 1e022612d8..8871b0cf86 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -1281,7 +1281,7 @@ def init_ds_surface_masks_wf( niu.Merge(2), name='combine_masks', ) - workflow.connect(combine_masks, outputnode, [('out', 'mask_files')]) + workflow.connect([(combine_masks, outputnode, [('out', 'mask_files')])]) for i_hemi, hemi in enumerate(['L', 'R']): select_mask = pe.Node( @@ -1289,14 +1289,14 @@ def init_ds_surface_masks_wf( name=f'select_mask_{hemi}', run_without_submitting=True, ) - workflow.connect(inputnode, select_mask, [('mask_files', 'inlist')]) + workflow.connect([(inputnode, select_mask, [('mask_files', 'inlist')])]) select_source = pe.Node( niu.Select(index=i_hemi), name=f'select_source_{hemi}', run_without_submitting=True, ) - workflow.connect(inputnode, select_source, [('source_files', 'inlist')]) + workflow.connect([(inputnode, select_source, [('source_files', 'inlist')])]) sources = pe.Node( niu.Function(function=_bids_relative), From 965a47b59ab608ff49a97316de376947ad46deac Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 5 Aug 2025 10:01:55 -0400 Subject: [PATCH 22/26] Missed the precomputed desc. --- src/smriprep/data/io_spec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/data/io_spec.json b/src/smriprep/data/io_spec.json index 2f48352fcf..b3c97c588b 100644 --- a/src/smriprep/data/io_spec.json +++ b/src/smriprep/data/io_spec.json @@ -149,7 +149,7 @@ "datatype": "anat", "hemi": ["L", "R"], "space": null, - "desc": "roi", + "desc": "cortex", "suffix": "mask", "extension": ".label.gii" } From 9ea70c438d6450de52f14052f8722c81217442b8 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Tue, 5 Aug 2025 10:04:37 -0400 Subject: [PATCH 23/26] Clean up docstring. --- src/smriprep/workflows/outputs.py | 2 +- src/smriprep/workflows/surfaces.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 8871b0cf86..d6608457ac 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -325,7 +325,7 @@ def init_ds_mask_wf( *, bids_root: str, output_dir: str, - mask_type: ty.Literal['brain', 'roi', 'ribbon', 'cortex'], + mask_type: ty.Literal['brain', 'roi', 'ribbon'], extra_entities: dict | None = None, name='ds_mask_wf', ): diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index 2aa7a3c5ba..674d7fbc88 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1160,7 +1160,7 @@ def init_cortex_masks_wf( *, name: str = 'cortex_masks_wf', ): - """Create a cortical surface mask from a surface file. + """Create cortical surface masks from surface files. Workflow Graph .. workflow:: @@ -1172,15 +1172,17 @@ def init_cortex_masks_wf( Inputs ------ - midthickness : list of str + midthickness : len-2 list of str Each hemisphere's FreeSurfer midthickness surface file in GIFTI format - thickness : list of str + thickness : len-2 list of str Each hemisphere's FreeSurfer thickness file in GIFTI format Outputs ------- - cortex_masks : list of str + cortex_masks : len-2 list of str Cortical surface mask in GIFTI format for each hemisphere + source_files : len-2 list of lists of str + Each hemisphere's source files, which are used to create the mask """ DEFAULT_MEMORY_MIN_GB = 0.01 From 0f857e814248edf827b6179a0c928aac012d6a31 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 6 Aug 2025 13:52:39 -0400 Subject: [PATCH 24/26] rf: Combine connect calls --- src/smriprep/workflows/anatomical.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 980eb01dd6..5f36faeec5 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1352,20 +1352,18 @@ def init_anat_fit_wf( LOGGER.info('ANAT Stage 11: Creating cortical surface mask') cortex_masks_wf = init_cortex_masks_wf() - workflow.connect([ - (surfaces_buffer, cortex_masks_wf, [ - ('midthickness', 'inputnode.midthickness'), - ('thickness', 'inputnode.thickness'), - ]), - ]) # fmt:skip - ds_cortex_masks_wf = init_ds_surface_masks_wf( output_dir=output_dir, mask_type='cortex', name='ds_cortex_masks_wf', entities={'extension': '.label.gii'}, ) + workflow.connect([ + (surfaces_buffer, cortex_masks_wf, [ + ('midthickness', 'inputnode.midthickness'), + ('thickness', 'inputnode.thickness'), + ]), (cortex_masks_wf, ds_cortex_masks_wf, [ ('outputnode.cortex_masks', 'inputnode.mask_files'), ('outputnode.source_files', 'inputnode.source_files'), From ccc727dee294380b5c6fc96e8a3d7ab723eb3845 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 6 Aug 2025 13:56:13 -0400 Subject: [PATCH 25/26] rf: Revert init_cortex_masks_wf to use iterables --- src/smriprep/workflows/surfaces.py | 107 ++++++++++++----------------- 1 file changed, 43 insertions(+), 64 deletions(-) diff --git a/src/smriprep/workflows/surfaces.py b/src/smriprep/workflows/surfaces.py index 674d7fbc88..3dce50fc81 100644 --- a/src/smriprep/workflows/surfaces.py +++ b/src/smriprep/workflows/surfaces.py @@ -1192,76 +1192,60 @@ def init_cortex_masks_wf( niu.IdentityInterface(fields=['midthickness', 'thickness']), name='inputnode', ) - outputnode = pe.Node( + + itersource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='itersource', + iterables=[('hemi', ['L', 'R'])], + ) + + outputnode = pe.JoinNode( niu.IdentityInterface(fields=['cortex_masks', 'source_files']), name='outputnode', + joinsource='itersource', + ) + + select_surfaces = pe.Node( + KeySelect(fields=['thickness', 'midthickness'], keys=['L', 'R']), + name='select_surfaces', + run_without_submitting=True, ) - # Combine the inputs into a list - combine_sources = pe.Node( - niu.Merge(2, no_flatten=True), - name='combine_sources', + combine_sources = pe.Node(niu.Merge(2), name='combine_sources', run_without_submitting=True) + + abs_thickness = pe.Node( + MetricMath(metric='thickness', operation='abs'), + name='abs_thickness', + mem_gb=DEFAULT_MEMORY_MIN_GB, ) + initial_roi = pe.Node( + MetricMath(metric='roi', operation='bin'), name='initial_roi', mem_gb=DEFAULT_MEMORY_MIN_GB + ) + fill_holes = pe.Node(MetricFillHoles(), name='fill_holes', mem_gb=DEFAULT_MEMORY_MIN_GB) + native_roi = pe.Node(MetricRemoveIslands(), name='native_roi', mem_gb=DEFAULT_MEMORY_MIN_GB) + workflow.connect([ - (inputnode, combine_sources, [ + (inputnode, select_surfaces, [ + ('thickness', 'thickness'), + ('midthickness', 'midthickness'), + ]), + (itersource, select_surfaces, [('hemi', 'key')]), + (itersource, abs_thickness, [('hemi', 'hemisphere')]), + (itersource, initial_roi, [('hemi', 'hemisphere')]), + (select_surfaces, abs_thickness, [('thickness', 'metric_file')]), + (select_surfaces, fill_holes, [('midthickness', 'surface_file')]), + (select_surfaces, native_roi, [('midthickness', 'surface_file')]), + (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), + (initial_roi, fill_holes, [('metric_file', 'metric_file')]), + (fill_holes, native_roi, [('out_file', 'metric_file')]), + (native_roi, outputnode, [('out_file', 'cortex_masks')]), + (select_surfaces, combine_sources, [ ('midthickness', 'in1'), ('thickness', 'in2'), ]), - (combine_sources, outputnode, [(('out', _transpose_lol), 'source_files')]), + (combine_sources, outputnode, [('out', 'source_files')]), ]) # fmt:skip - combine_masks = pe.Node( - niu.Merge(2), - name='combine_masks', - ) - workflow.connect([(combine_masks, outputnode, [('out', 'cortex_masks')])]) - - for i_hemi, hemi in enumerate(['L', 'R']): - select_midthickness = pe.Node( - niu.Select(index=i_hemi), - name=f'select_midthickness_{hemi}', - ) - select_thickness = pe.Node( - niu.Select(index=i_hemi), - name=f'select_thickness_{hemi}', - ) - workflow.connect([ - (inputnode, select_midthickness, [('midthickness', 'inlist')]), - (inputnode, select_thickness, [('thickness', 'inlist')]), - ]) # fmt:skip - - # Thickness is presumably already positive, but HCP uses abs(-thickness) - abs_thickness = pe.Node( - MetricMath(metric='thickness', operation='abs', hemisphere=hemi), - name=f'abs_thickness_{hemi}', - ) - - # Native ROI is thickness > 0, with holes and islands filled - initial_roi = pe.Node( - MetricMath(metric='roi', operation='bin', hemisphere=hemi), - name=f'initial_roi_{hemi}', - ) - fill_holes = pe.Node( - MetricFillHoles(), - name=f'fill_holes_{hemi}', - mem_gb=DEFAULT_MEMORY_MIN_GB, - ) - native_roi = pe.Node( - MetricRemoveIslands(), - name=f'native_roi_{hemi}', - mem_gb=DEFAULT_MEMORY_MIN_GB, - ) - - workflow.connect([ - (select_thickness, abs_thickness, [('out', 'metric_file')]), - (abs_thickness, initial_roi, [('metric_file', 'metric_file')]), - (select_midthickness, fill_holes, [('out', 'surface_file')]), - (select_midthickness, native_roi, [('out', 'surface_file')]), - (initial_roi, fill_holes, [('metric_file', 'metric_file')]), - (fill_holes, native_roi, [('out_file', 'metric_file')]), - (native_roi, combine_masks, [('out_file', f'in{i_hemi + 1}')]), - ]) # fmt:skip - return workflow @@ -1804,8 +1788,3 @@ def _select_seg(in_files, segmentation): def _repeat(seq: list, count: int) -> list: return seq * count - - -def _transpose_lol(inlist): - """Transpose a list of lists.""" - return list(map(list, zip(*inlist, strict=False))) From 04643a4d0bb9ffba436650fd007b98dca56afae2 Mon Sep 17 00:00:00 2001 From: "Christopher J. Markiewicz" Date: Wed, 6 Aug 2025 14:09:17 -0400 Subject: [PATCH 26/26] rf: Replace explicit loop with iterables in ds_surface_masks_wf --- src/smriprep/workflows/anatomical.py | 1 - src/smriprep/workflows/outputs.py | 90 ++++++++++++++-------------- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/smriprep/workflows/anatomical.py b/src/smriprep/workflows/anatomical.py index 5f36faeec5..2c1c3a9806 100644 --- a/src/smriprep/workflows/anatomical.py +++ b/src/smriprep/workflows/anatomical.py @@ -1356,7 +1356,6 @@ def init_anat_fit_wf( output_dir=output_dir, mask_type='cortex', name='ds_cortex_masks_wf', - entities={'extension': '.label.gii'}, ) workflow.connect([ diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index d6608457ac..9a750f145e 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -1275,57 +1275,53 @@ def init_ds_surface_masks_wf( niu.IdentityInterface(fields=['mask_files', 'source_files']), name='inputnode', ) - outputnode = pe.Node(niu.IdentityInterface(fields=['mask_files']), name='outputnode') + outputnode = pe.JoinNode( + niu.IdentityInterface(fields=['mask_files']), name='outputnode', joinsource='ds_itersource' + ) - combine_masks = pe.Node( - niu.Merge(2), - name='combine_masks', + ds_itersource = pe.Node( + niu.IdentityInterface(fields=['hemi']), + name='ds_itersource', + iterables=[('hemi', ['L', 'R'])], ) - workflow.connect([(combine_masks, outputnode, [('out', 'mask_files')])]) - for i_hemi, hemi in enumerate(['L', 'R']): - select_mask = pe.Node( - niu.Select(index=i_hemi), - name=f'select_mask_{hemi}', - run_without_submitting=True, - ) - workflow.connect([(inputnode, select_mask, [('mask_files', 'inlist')])]) + sources = pe.Node(niu.Function(function=_bids_relative), name='sources') + sources.inputs.bids_root = output_dir - select_source = pe.Node( - niu.Select(index=i_hemi), - name=f'select_source_{hemi}', - run_without_submitting=True, - ) - workflow.connect([(inputnode, select_source, [('source_files', 'inlist')])]) - - sources = pe.Node( - niu.Function(function=_bids_relative), - name=f'sources_{hemi}', - ) - sources.inputs.bids_root = output_dir + select_files = pe.Node( + KeySelect(fields=['mask_file', 'sources'], keys=['L', 'R']), + name='select_files', + run_without_submitting=True, + ) - ds_mask = pe.Node( - DerivativesDataSink( - base_directory=output_dir, - hemi=hemi, - desc=mask_type, - **entities, - ), - name=f'ds_mask_{hemi}', - run_without_submitting=True, - ) - if mask_type == 'brain': - ds_mask.inputs.Type = 'Brain' - else: - ds_mask.inputs.Type = 'ROI' + ds_surf_mask = pe.Node( + DerivativesDataSink( + base_directory=output_dir, + suffix='mask', + desc=mask_type, + extension='.label.gii', + Type='Brain' if mask_type == 'brain' else 'ROI', + **entities, + ), + name='ds_surf_mask', + run_without_submitting=True, + ) - workflow.connect([ - (select_mask, ds_mask, [('out', 'in_file')]), - (select_source, sources, [('out', 'in_files')]), - (select_source, ds_mask, [('out', 'source_file')]), - (sources, ds_mask, [('out', 'Sources')]), - (ds_mask, combine_masks, [('out_file', f'in{i_hemi + 1}')]), - ]) # fmt:skip + workflow.connect([ + (inputnode, select_files, [ + ('mask_files', 'mask_file'), + ('source_files', 'sources'), + ]), + (select_files, sources, [('sources', 'in_files')]), + (ds_itersource, select_files, [('hemi', 'key')]), + (ds_itersource, ds_surf_mask, [('hemi', 'hemi')]), + (select_files, ds_surf_mask, [ + ('mask_file', 'in_file'), + (('sources', _pop), 'source_file'), + ]), + (sources, ds_surf_mask, [('out', 'Sources')]), + (ds_surf_mask, outputnode, [('out_file', 'mask_files')]), + ]) # fmt: skip return workflow @@ -1434,3 +1430,7 @@ def _read_json(in_file): from pathlib import Path return loads(Path(in_file).read_text()) + + +def _pop(in_list): + return in_list[0]