From b5111a8c3cafc8da5e0cabc4b656b8967a4869eb Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 4 Dec 2025 12:14:48 -0500 Subject: [PATCH 01/13] Add SpatialReference field to some outputs. --- src/smriprep/interfaces/templateflow.py | 31 +++++++++++++++++++++++++ src/smriprep/workflows/outputs.py | 18 ++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/smriprep/interfaces/templateflow.py b/src/smriprep/interfaces/templateflow.py index a48321fdde9..f899eb94897 100644 --- a/src/smriprep/interfaces/templateflow.py +++ b/src/smriprep/interfaces/templateflow.py @@ -218,3 +218,34 @@ def fetch_template_files( # Not guaranteed to exist so add fallback files['t2w'] = tf.get(name[0], desc=None, suffix='T2w', **specs) or Undefined return files + + +class _TemplateFlowReferenceInputSpec(BaseInterfaceInputSpec): + template = File(exists=True, mandatory=True, desc='Path to template reference file') + + +class _TemplateFlowReferenceOutputSpec(TraitedSpec): + uri = traits.Str(desc='URI of the template reference file') + + +class TemplateFlowReference(SimpleInterface): + """Get template reference file from TemplateFlow.""" + + input_spec = _TemplateFlowReferenceInputSpec + output_spec = _TemplateFlowReferenceOutputSpec + + def _run_interface(self, runtime): + from pathlib import Path + + # For standard templates from TemplateFlow, we want the URL. + # For custom templates, we want the local path. + tf_url = 'https://templateflow.s3.amazonaws.com' + local_path = self.inputs.templateflow_dir + + rel_path = Path(self.inputs.template).relative_to(local_path) + template_name = rel_path.name.split('_')[0].split('-')[1] + if template_name in tf.TF_LAYOUT.get_templates(): + self._results['uri'] = f'{tf_url}/{str(rel_path)}' + else: + self._results['uri'] = str(self.inputs.template) + return runtime diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 9a750f145e7..a6bfacf9c26 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -33,7 +33,11 @@ from niworkflows.interfaces.utility import KeySelect from ..interfaces import DerivativesDataSink -from ..interfaces.templateflow import TemplateFlowSelect, fetch_template_files +from ..interfaces.templateflow import ( + TemplateFlowReference, + TemplateFlowSelect, + fetch_template_files, +) if ty.TYPE_CHECKING: from niworkflows.utils.spaces import SpatialReferences @@ -958,6 +962,11 @@ def init_ds_anat_volumes_wf( raw_sources = pe.Node(niu.Function(function=_bids_relative), name='raw_sources') raw_sources.inputs.bids_root = bids_root + spatial_reference = pe.Node( + TemplateFlowReference(), + name='spatial_reference', + ) + gen_ref = pe.Node(GenerateSamplingReference(), name='gen_ref', mem_gb=0.01) # Mask T1w preproc images @@ -1018,9 +1027,11 @@ def init_ds_anat_volumes_wf( ds_std_tpms.inputs.label = tpm_labels workflow.connect([ + (inputnode, spatial_reference, [('ref_file', 'in_file')]), (inputnode, gen_ref, [ ('ref_file', 'fixed_image'), (('resolution', _is_native), 'keep_native'), + ('anat_preproc', 'moving_image'), ]), (inputnode, mask_anat, [ ('anat_preproc', 'in_file'), @@ -1030,11 +1041,14 @@ def init_ds_anat_volumes_wf( (inputnode, anat2std_mask, [('anat_mask', 'input_image')]), (inputnode, anat2std_dseg, [('anat_dseg', 'input_image')]), (inputnode, anat2std_tpms, [('anat_tpms', 'input_image')]), - (inputnode, gen_ref, [('anat_preproc', 'moving_image')]), (anat2std_t1w, ds_std_t1w, [('output_image', 'in_file')]), + (spatial_reference, ds_std_t1w, [('out_file', 'SpatialReference')]), (anat2std_mask, ds_std_mask, [('output_image', 'in_file')]), + (spatial_reference, ds_std_mask, [('out_file', 'SpatialReference')]), (anat2std_dseg, ds_std_dseg, [('output_image', 'in_file')]), + (spatial_reference, ds_std_dseg, [('out_file', 'SpatialReference')]), (anat2std_tpms, ds_std_tpms, [('output_image', 'in_file')]), + (spatial_reference, ds_std_tpms, [('out_file', 'SpatialReference')]), ]) # fmt:skip workflow.connect( From bf7898368138b6307428f32a85bf5363f2ad00ee Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 4 Dec 2025 12:22:20 -0500 Subject: [PATCH 02/13] Fix connections. --- src/smriprep/workflows/outputs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index a6bfacf9c26..7e35640435b 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -1027,7 +1027,7 @@ def init_ds_anat_volumes_wf( ds_std_tpms.inputs.label = tpm_labels workflow.connect([ - (inputnode, spatial_reference, [('ref_file', 'in_file')]), + (inputnode, spatial_reference, [('ref_file', 'template')]), (inputnode, gen_ref, [ ('ref_file', 'fixed_image'), (('resolution', _is_native), 'keep_native'), @@ -1042,13 +1042,13 @@ def init_ds_anat_volumes_wf( (inputnode, anat2std_dseg, [('anat_dseg', 'input_image')]), (inputnode, anat2std_tpms, [('anat_tpms', 'input_image')]), (anat2std_t1w, ds_std_t1w, [('output_image', 'in_file')]), - (spatial_reference, ds_std_t1w, [('out_file', 'SpatialReference')]), + (spatial_reference, ds_std_t1w, [('uri', 'SpatialReference')]), (anat2std_mask, ds_std_mask, [('output_image', 'in_file')]), - (spatial_reference, ds_std_mask, [('out_file', 'SpatialReference')]), + (spatial_reference, ds_std_mask, [('uri', 'SpatialReference')]), (anat2std_dseg, ds_std_dseg, [('output_image', 'in_file')]), - (spatial_reference, ds_std_dseg, [('out_file', 'SpatialReference')]), + (spatial_reference, ds_std_dseg, [('uri', 'SpatialReference')]), (anat2std_tpms, ds_std_tpms, [('output_image', 'in_file')]), - (spatial_reference, ds_std_tpms, [('out_file', 'SpatialReference')]), + (spatial_reference, ds_std_tpms, [('uri', 'SpatialReference')]), ]) # fmt:skip workflow.connect( From b265c2d948d13cc9c52af63a81da218de7ddb4ae Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 4 Dec 2025 15:57:07 -0500 Subject: [PATCH 03/13] Fix. --- src/smriprep/interfaces/templateflow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/smriprep/interfaces/templateflow.py b/src/smriprep/interfaces/templateflow.py index f899eb94897..a1e1c2fbd48 100644 --- a/src/smriprep/interfaces/templateflow.py +++ b/src/smriprep/interfaces/templateflow.py @@ -240,9 +240,8 @@ def _run_interface(self, runtime): # For standard templates from TemplateFlow, we want the URL. # For custom templates, we want the local path. tf_url = 'https://templateflow.s3.amazonaws.com' - local_path = self.inputs.templateflow_dir - rel_path = Path(self.inputs.template).relative_to(local_path) + rel_path = Path(self.inputs.template).relative_to(tf.TF_LAYOUT.root) template_name = rel_path.name.split('_')[0].split('-')[1] if template_name in tf.TF_LAYOUT.get_templates(): self._results['uri'] = f'{tf_url}/{str(rel_path)}' From 27878f8930ab41aede8a9dace20e0c74da33c6de Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Thu, 4 Dec 2025 16:20:15 -0500 Subject: [PATCH 04/13] Update expected outputs. --- .circleci/ds054_outputs.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/ds054_outputs.txt b/.circleci/ds054_outputs.txt index 6e970152d9a..71c614350ef 100644 --- a/.circleci/ds054_outputs.txt +++ b/.circleci/ds054_outputs.txt @@ -24,6 +24,7 @@ smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_desc-brain_mask.json smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_desc-brain_mask.nii.gz smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_desc-preproc_T1w.json smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_desc-preproc_T1w.nii.gz +smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_dseg.json smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_dseg.nii.gz smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_label-CSF_probseg.nii.gz smriprep/sub-100185/anat/sub-100185_space-MNI152Lin_label-GM_probseg.nii.gz @@ -32,6 +33,7 @@ smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_desc-brain_mask.js smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_desc-brain_mask.nii.gz smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_desc-preproc_T1w.json smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_desc-preproc_T1w.nii.gz +smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_dseg.json smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_dseg.nii.gz smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_label-CSF_probseg.nii.gz smriprep/sub-100185/anat/sub-100185_space-MNI152NLin2009cAsym_label-GM_probseg.nii.gz From 15f7fc89e7368a1d2b96d4d672c04f87b616d0e0 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 5 Dec 2025 14:09:17 -0500 Subject: [PATCH 05/13] Add dataset_links input and write BIDSURI. --- src/smriprep/interfaces/bids.py | 62 +++++++++++++++++++++++++++++ src/smriprep/utils/bids.py | 65 +++++++++++++++++++++++++++++++ src/smriprep/workflows/outputs.py | 24 ++++++++++-- 3 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/smriprep/interfaces/bids.py diff --git a/src/smriprep/interfaces/bids.py b/src/smriprep/interfaces/bids.py new file mode 100644 index 00000000000..87ec5d63018 --- /dev/null +++ b/src/smriprep/interfaces/bids.py @@ -0,0 +1,62 @@ +"""BIDS-related interfaces.""" + +from pathlib import Path + +from bids.utils import listify +from nipype.interfaces.base import ( + DynamicTraitedSpec, + SimpleInterface, + TraitedSpec, + isdefined, + traits, +) +from nipype.interfaces.io import add_traits +from nipype.interfaces.utility.base import _ravel + +from ..utils.bids import _find_nearest_path + + +class _BIDSURIInputSpec(DynamicTraitedSpec): + dataset_links = traits.Dict(mandatory=True, desc='Dataset links') + out_dir = traits.Str(mandatory=True, desc='Output directory') + + +class _BIDSURIOutputSpec(TraitedSpec): + out = traits.List( + traits.Str, + desc='BIDS URI(s) for file', + ) + + +class BIDSURI(SimpleInterface): + """Convert input filenames to BIDS URIs, based on links in the dataset. + + This interface can combine multiple lists of inputs. + """ + + input_spec = _BIDSURIInputSpec + output_spec = _BIDSURIOutputSpec + + def __init__(self, numinputs=0, **inputs): + super().__init__(**inputs) + self._numinputs = numinputs + if numinputs >= 1: + input_names = [f'in{i + 1}' for i in range(numinputs)] + else: + input_names = [] + add_traits(self.inputs, input_names) + + def _run_interface(self, runtime): + inputs = [getattr(self.inputs, f'in{i + 1}') for i in range(self._numinputs)] + in_files = listify(inputs) + in_files = _ravel(in_files) + # Remove undefined inputs + in_files = [f for f in in_files if isdefined(f)] + # Convert the dataset links to BIDS URI prefixes + updated_keys = {f'bids:{k}:': Path(v) for k, v in self.inputs.dataset_links.items()} + updated_keys['bids::'] = Path(self.inputs.out_dir) + # Convert the paths to BIDS URIs + out = [_find_nearest_path(updated_keys, f) for f in in_files] + self._results['out'] = out + + return runtime diff --git a/src/smriprep/utils/bids.py b/src/smriprep/utils/bids.py index fe2bd3bb11e..d92b46a4010 100644 --- a/src/smriprep/utils/bids.py +++ b/src/smriprep/utils/bids.py @@ -197,3 +197,68 @@ def write_derivative_description(bids_dir, deriv_dir): desc['License'] = orig_desc['License'] Path.write_text(deriv_dir / 'dataset_description.json', json.dumps(desc, indent=4)) + + +def _find_nearest_path(path_dict, input_path): + """Find the nearest relative path from an input path to a dictionary of paths. + + If ``input_path`` is not relative to any of the paths in ``path_dict``, + the absolute path string is returned. + + If ``input_path`` is already a BIDS-URI, then it will be returned unmodified. + + Parameters + ---------- + path_dict : dict of (str, Path) + A dictionary of paths. + input_path : Path + The input path to match. + + Returns + ------- + matching_path : str + The nearest relative path from the input path to a path in the dictionary. + This is either the concatenation of the associated key from ``path_dict`` + and the relative path from the associated value from ``path_dict`` to ``input_path``, + or the absolute path to ``input_path`` if no matching path is found from ``path_dict``. + + Examples + -------- + >>> from pathlib import Path + >>> path_dict = { + ... 'bids::': Path('/data/derivatives/fmriprep'), + ... 'bids:raw:': Path('/data'), + ... 'bids:deriv-0:': Path('/data/derivatives/source-1'), + ... } + >>> input_path = Path('/data/derivatives/source-1/sub-01/func/sub-01_task-rest_bold.nii.gz') + >>> _find_nearest_path(path_dict, input_path) # match to 'bids:deriv-0:' + 'bids:deriv-0:sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> input_path = Path('/out/sub-01/func/sub-01_task-rest_bold.nii.gz') + >>> _find_nearest_path(path_dict, input_path) # no match- absolute path + '/out/sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> input_path = Path('/data/sub-01/func/sub-01_task-rest_bold.nii.gz') + >>> _find_nearest_path(path_dict, input_path) # match to 'bids:raw:' + 'bids:raw:sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> input_path = 'bids::sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> _find_nearest_path(path_dict, input_path) # already a BIDS-URI + 'bids::sub-01/func/sub-01_task-rest_bold.nii.gz' + """ + # Don't modify BIDS-URIs + if isinstance(input_path, str) and input_path.startswith('bids:'): + return input_path + + input_path = Path(input_path) + matching_path = None + for key, path in path_dict.items(): + if input_path.is_relative_to(path): + relative_path = input_path.relative_to(path) + if (matching_path is None) or (len(relative_path.parts) < len(matching_path.parts)): + matching_key = key + matching_path = relative_path + + if matching_path is None: + matching_path = str(input_path.absolute()) + else: + matching_path = f'{matching_key}{matching_path}' + + return matching_path diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 7e35640435b..e44ae67a8a0 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -33,6 +33,7 @@ from niworkflows.interfaces.utility import KeySelect from ..interfaces import DerivativesDataSink +from ..interfaces.bids import BIDSURI from ..interfaces.templateflow import ( TemplateFlowReference, TemplateFlowSelect, @@ -933,6 +934,7 @@ def init_ds_anat_volumes_wf( *, bids_root: str, output_dir: str, + dataset_links: dict[str, str] | None = None, name='ds_anat_volumes_wf', tpm_labels=BIDS_TISSUE_ORDER, ) -> pe.Workflow: @@ -967,6 +969,19 @@ def init_ds_anat_volumes_wf( name='spatial_reference', ) + dataset_links = dataset_links or {} + if 'bids' not in dataset_links: + dataset_links['bids'] = str(output_dir) + + spatial_reference_uri = pe.Node( + BIDSURI( + numinputs=1, + dataset_links=dataset_links, + out_dir=str(output_dir), + ), + name='spatial_reference_uri', + ) + gen_ref = pe.Node(GenerateSamplingReference(), name='gen_ref', mem_gb=0.01) # Mask T1w preproc images @@ -1028,6 +1043,7 @@ def init_ds_anat_volumes_wf( workflow.connect([ (inputnode, spatial_reference, [('ref_file', 'template')]), + (spatial_reference, spatial_reference_uri, [('uri', 'in1')]), (inputnode, gen_ref, [ ('ref_file', 'fixed_image'), (('resolution', _is_native), 'keep_native'), @@ -1042,13 +1058,13 @@ def init_ds_anat_volumes_wf( (inputnode, anat2std_dseg, [('anat_dseg', 'input_image')]), (inputnode, anat2std_tpms, [('anat_tpms', 'input_image')]), (anat2std_t1w, ds_std_t1w, [('output_image', 'in_file')]), - (spatial_reference, ds_std_t1w, [('uri', 'SpatialReference')]), + (spatial_reference_uri, ds_std_t1w, [(('out', _pop), 'SpatialReference')]), (anat2std_mask, ds_std_mask, [('output_image', 'in_file')]), - (spatial_reference, ds_std_mask, [('uri', 'SpatialReference')]), + (spatial_reference_uri, ds_std_mask, [(('out', _pop), 'SpatialReference')]), (anat2std_dseg, ds_std_dseg, [('output_image', 'in_file')]), - (spatial_reference, ds_std_dseg, [('uri', 'SpatialReference')]), + (spatial_reference_uri, ds_std_dseg, [(('out', _pop), 'SpatialReference')]), (anat2std_tpms, ds_std_tpms, [('output_image', 'in_file')]), - (spatial_reference, ds_std_tpms, [('uri', 'SpatialReference')]), + (spatial_reference_uri, ds_std_tpms, [(('out', _pop), 'SpatialReference')]), ]) # fmt:skip workflow.connect( From d3f4e63b0c5fc7f479bb33efe02cacbe85bb0332 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 5 Dec 2025 14:31:49 -0500 Subject: [PATCH 06/13] Work around URLs. --- src/smriprep/utils/bids.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/smriprep/utils/bids.py b/src/smriprep/utils/bids.py index d92b46a4010..81e987e8221 100644 --- a/src/smriprep/utils/bids.py +++ b/src/smriprep/utils/bids.py @@ -242,11 +242,25 @@ def _find_nearest_path(path_dict, input_path): >>> input_path = 'bids::sub-01/func/sub-01_task-rest_bold.nii.gz' >>> _find_nearest_path(path_dict, input_path) # already a BIDS-URI 'bids::sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> input_path = 'https://example.com/sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> _find_nearest_path(path_dict, input_path) # already a URL + 'https://example.com/sub-01/func/sub-01_task-rest_bold.nii.gz' """ # Don't modify BIDS-URIs if isinstance(input_path, str) and input_path.startswith('bids:'): return input_path + # Only modify URLs if there's a URL in the path_dict + if isinstance(input_path, str) and input_path.startswith('http'): + remote_found = False + for path in path_dict.values(): + if path.startswith('http'): + remote_found = True + break + + if not remote_found: + return input_path + input_path = Path(input_path) matching_path = None for key, path in path_dict.items(): From c8633d371aad44e73119ab0e6ac031e1b85c6f33 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 5 Dec 2025 14:39:05 -0500 Subject: [PATCH 07/13] Update bids.py --- src/smriprep/utils/bids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/utils/bids.py b/src/smriprep/utils/bids.py index 81e987e8221..fe5d340d765 100644 --- a/src/smriprep/utils/bids.py +++ b/src/smriprep/utils/bids.py @@ -254,7 +254,7 @@ def _find_nearest_path(path_dict, input_path): if isinstance(input_path, str) and input_path.startswith('http'): remote_found = False for path in path_dict.values(): - if path.startswith('http'): + if str(path).startswith('http'): remote_found = True break From 7b3edfc7619987e843586b3822e3f50fabb8ae55 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 5 Dec 2025 14:56:02 -0500 Subject: [PATCH 08/13] Support templateflow URIs. --- src/smriprep/utils/bids.py | 3 +++ src/smriprep/workflows/outputs.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/smriprep/utils/bids.py b/src/smriprep/utils/bids.py index fe5d340d765..506ce076982 100644 --- a/src/smriprep/utils/bids.py +++ b/src/smriprep/utils/bids.py @@ -245,6 +245,9 @@ def _find_nearest_path(path_dict, input_path): >>> input_path = 'https://example.com/sub-01/func/sub-01_task-rest_bold.nii.gz' >>> _find_nearest_path(path_dict, input_path) # already a URL 'https://example.com/sub-01/func/sub-01_task-rest_bold.nii.gz' + >>> path_dict['bids:tfl:'] = 'https://example.com' + >>> _find_nearest_path(path_dict, input_path) # match to 'bids:tfl:' + 'bids:tfl:sub-01/func/sub-01_task-rest_bold.nii.gz' """ # Don't modify BIDS-URIs if isinstance(input_path, str) and input_path.startswith('bids:'): diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index e44ae67a8a0..6da3203bd27 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -972,6 +972,8 @@ def init_ds_anat_volumes_wf( dataset_links = dataset_links or {} if 'bids' not in dataset_links: dataset_links['bids'] = str(output_dir) + if 'templateflow' not in dataset_links: + dataset_links['templateflow'] = 'https://templateflow.s3.amazonaws.com' spatial_reference_uri = pe.Node( BIDSURI( From f02d4d2d288d0f3689d8fb2fa0f15b17c88043e4 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 5 Dec 2025 17:14:21 -0500 Subject: [PATCH 09/13] Update src/smriprep/workflows/outputs.py Co-authored-by: Chris Markiewicz --- src/smriprep/workflows/outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 6da3203bd27..405a0ec966f 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -969,7 +969,7 @@ def init_ds_anat_volumes_wf( name='spatial_reference', ) - dataset_links = dataset_links or {} + dataset_links = dataset_links.copy() or {} if 'bids' not in dataset_links: dataset_links['bids'] = str(output_dir) if 'templateflow' not in dataset_links: From 917b5a0eb9f080c0c76e363725e9a4d312610d7c Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 18 Feb 2026 09:59:13 -0500 Subject: [PATCH 10/13] Address test failure. --- src/smriprep/workflows/outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index 3d5a9e3c9fc..bccc2130124 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -968,7 +968,7 @@ def init_ds_anat_volumes_wf( name='spatial_reference', ) - dataset_links = dataset_links.copy() or {} + dataset_links = (dataset_links or {}).copy() if 'bids' not in dataset_links: dataset_links['bids'] = str(output_dir) if 'templateflow' not in dataset_links: From 5bbf29fe88942c07736043ca48b230b89eeb7108 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 18 Feb 2026 10:02:45 -0500 Subject: [PATCH 11/13] Remove standard/custom template distinction. --- src/smriprep/interfaces/templateflow.py | 30 ------------------------- src/smriprep/workflows/outputs.py | 12 +++------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/smriprep/interfaces/templateflow.py b/src/smriprep/interfaces/templateflow.py index a1e1c2fbd48..a48321fdde9 100644 --- a/src/smriprep/interfaces/templateflow.py +++ b/src/smriprep/interfaces/templateflow.py @@ -218,33 +218,3 @@ def fetch_template_files( # Not guaranteed to exist so add fallback files['t2w'] = tf.get(name[0], desc=None, suffix='T2w', **specs) or Undefined return files - - -class _TemplateFlowReferenceInputSpec(BaseInterfaceInputSpec): - template = File(exists=True, mandatory=True, desc='Path to template reference file') - - -class _TemplateFlowReferenceOutputSpec(TraitedSpec): - uri = traits.Str(desc='URI of the template reference file') - - -class TemplateFlowReference(SimpleInterface): - """Get template reference file from TemplateFlow.""" - - input_spec = _TemplateFlowReferenceInputSpec - output_spec = _TemplateFlowReferenceOutputSpec - - def _run_interface(self, runtime): - from pathlib import Path - - # For standard templates from TemplateFlow, we want the URL. - # For custom templates, we want the local path. - tf_url = 'https://templateflow.s3.amazonaws.com' - - rel_path = Path(self.inputs.template).relative_to(tf.TF_LAYOUT.root) - template_name = rel_path.name.split('_')[0].split('-')[1] - if template_name in tf.TF_LAYOUT.get_templates(): - self._results['uri'] = f'{tf_url}/{str(rel_path)}' - else: - self._results['uri'] = str(self.inputs.template) - return runtime diff --git a/src/smriprep/workflows/outputs.py b/src/smriprep/workflows/outputs.py index bccc2130124..a4943c522dc 100644 --- a/src/smriprep/workflows/outputs.py +++ b/src/smriprep/workflows/outputs.py @@ -31,11 +31,11 @@ from niworkflows.interfaces.nibabel import ApplyMask, GenerateSamplingReference from niworkflows.interfaces.space import SpaceDataSource from niworkflows.interfaces.utility import KeySelect +from templateflow import api as tf from ..interfaces import DerivativesDataSink from ..interfaces.bids import BIDSURI from ..interfaces.templateflow import ( - TemplateFlowReference, TemplateFlowSelect, fetch_template_files, ) @@ -963,16 +963,11 @@ def init_ds_anat_volumes_wf( raw_sources = pe.Node(niu.Function(function=_bids_relative), name='raw_sources') raw_sources.inputs.bids_root = bids_root - spatial_reference = pe.Node( - TemplateFlowReference(), - name='spatial_reference', - ) - dataset_links = (dataset_links or {}).copy() if 'bids' not in dataset_links: dataset_links['bids'] = str(output_dir) if 'templateflow' not in dataset_links: - dataset_links['templateflow'] = 'https://templateflow.s3.amazonaws.com' + dataset_links['templateflow'] = str(tf.TF_LAYOUT.root) spatial_reference_uri = pe.Node( BIDSURI( @@ -1043,8 +1038,7 @@ def init_ds_anat_volumes_wf( ds_std_tpms.inputs.label = tpm_labels workflow.connect([ - (inputnode, spatial_reference, [('ref_file', 'template')]), - (spatial_reference, spatial_reference_uri, [('uri', 'in1')]), + (inputnode, spatial_reference_uri, [('ref_file', 'in1')]), (inputnode, gen_ref, [ ('ref_file', 'fixed_image'), (('resolution', _is_native), 'keep_native'), From 5323b00abb1ed30b91b86962b58020531a00701f Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 18 Feb 2026 10:13:09 -0500 Subject: [PATCH 12/13] Add header to file. --- src/smriprep/interfaces/bids.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/smriprep/interfaces/bids.py b/src/smriprep/interfaces/bids.py index 87ec5d63018..bafc76b6b0f 100644 --- a/src/smriprep/interfaces/bids.py +++ b/src/smriprep/interfaces/bids.py @@ -1,3 +1,25 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2021 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# """BIDS-related interfaces.""" from pathlib import Path From 3b0004da8b34a7bcf7db93c6b625bcdc418a39ef Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Wed, 18 Feb 2026 10:16:13 -0500 Subject: [PATCH 13/13] Update bids.py --- src/smriprep/interfaces/bids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/smriprep/interfaces/bids.py b/src/smriprep/interfaces/bids.py index bafc76b6b0f..ecdd24f7e8d 100644 --- a/src/smriprep/interfaces/bids.py +++ b/src/smriprep/interfaces/bids.py @@ -1,7 +1,7 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: # -# Copyright 2021 The NiPreps Developers +# Copyright The NiPreps Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License.