From a65c12c894f6e2026b02eb673ad0f60e54ed346f Mon Sep 17 00:00:00 2001 From: Martin Norgaard Date: Mon, 17 Nov 2025 10:39:30 +0100 Subject: [PATCH 1/5] ENH: Add session-label option for analyses --- petprep/cli/parser.py | 28 ++++++++++++++++++++++++++-- petprep/config.py | 2 ++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/petprep/cli/parser.py b/petprep/cli/parser.py index 50114f31..488b60df 100644 --- a/petprep/cli/parser.py +++ b/petprep/cli/parser.py @@ -188,8 +188,13 @@ def _bids_filter(value, parser): 'identifier (the sub- prefix can be removed)', ) # Re-enable when option is actually implemented - # g_bids.add_argument('-s', '--session-id', action='store', default='single_session', - # help='Select a specific session to be processed') + g_bids.add_argument( + '--session-label', + nargs='+', + type=lambda label: label.removeprefix('ses-'), + help='A space delimited list of session identifiers or a single ' + 'identifier (the ses- prefix can be removed)', + ) # Re-enable when option is actually implemented # g_bids.add_argument('-r', '--run-id', action='store', default='single_run', # help='Select a specific run to be processed') @@ -749,6 +754,14 @@ def parse_args(args=None, namespace=None): config.execution.log_level = int(max(25 - 5 * opts.verbose_count, logging.DEBUG)) config.from_dict(vars(opts), init=['nipype']) + if config.execution.session_label: + config.execution.bids_filters = config.execution.bids_filters or {} + for modality in ('pet', 'anat'): + config.execution.bids_filters[modality] = { + **config.execution.bids_filters.get(modality, {}), + 'session': config.execution.session_label, + } + pvc_vals = (opts.pvc_tool, opts.pvc_method, opts.pvc_psf) if any(val is not None for val in pvc_vals) and not all(val is not None for val in pvc_vals): parser.error('Options --pvc-tool, --pvc-method and --pvc-psf must be used together.') @@ -908,5 +921,16 @@ def parse_args(args=None, namespace=None): f'One or more participant labels were not found in the BIDS directory: {", ".join(missing_subjects)}.' ) + if config.execution.session_label: + available_sessions = set( + config.execution.layout.get_sessions(subject=list(participant_label) or None) + ) + missing_sessions = set(config.execution.session_label) - available_sessions + if missing_sessions: + parser.error( + 'One or more session labels were not found in the BIDS directory: ' + f"{', '.join(sorted(missing_sessions))}." + ) + config.execution.participant_label = sorted(participant_label) config.workflow.skull_strip_template = config.workflow.skull_strip_template[0] diff --git a/petprep/config.py b/petprep/config.py index c61319b1..2cfc100b 100644 --- a/petprep/config.py +++ b/petprep/config.py @@ -432,6 +432,8 @@ class execution(_Config): """Unique identifier of this particular run.""" participant_label = None """List of participant identifiers that are to be preprocessed.""" + session_label = None + """List of session identifiers that are to be preprocessed.""" task_id = None """Select a particular task from all available in the dataset.""" templateflow_home = _templateflow_home From 923f6e5c5a05e90e3c492a7393d2e51001231ead Mon Sep 17 00:00:00 2001 From: Martin Norgaard Date: Mon, 17 Nov 2025 10:52:51 +0100 Subject: [PATCH 2/5] FIX: update parser to only include PET --- petprep/cli/parser.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/petprep/cli/parser.py b/petprep/cli/parser.py index 488b60df..4598806c 100644 --- a/petprep/cli/parser.py +++ b/petprep/cli/parser.py @@ -756,11 +756,10 @@ def parse_args(args=None, namespace=None): if config.execution.session_label: config.execution.bids_filters = config.execution.bids_filters or {} - for modality in ('pet', 'anat'): - config.execution.bids_filters[modality] = { - **config.execution.bids_filters.get(modality, {}), - 'session': config.execution.session_label, - } + config.execution.bids_filters['pet'] = { + **config.execution.bids_filters.get('pet', {}), + 'session': config.execution.session_label, + } pvc_vals = (opts.pvc_tool, opts.pvc_method, opts.pvc_psf) if any(val is not None for val in pvc_vals) and not all(val is not None for val in pvc_vals): From bfb2a1b5e23df7df25f593fd37cc322f3a8b9c97 Mon Sep 17 00:00:00 2001 From: Martin Norgaard Date: Mon, 17 Nov 2025 10:53:11 +0100 Subject: [PATCH 3/5] FIX: add test for session-label option --- petprep/cli/tests/test_parser.py | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/petprep/cli/tests/test_parser.py b/petprep/cli/tests/test_parser.py index 78d47ebb..86c7bcf6 100644 --- a/petprep/cli/tests/test_parser.py +++ b/petprep/cli/tests/test_parser.py @@ -25,6 +25,8 @@ from argparse import ArgumentError import pytest +import nibabel as nb +import numpy as np from packaging.version import Version from ... import config @@ -225,6 +227,47 @@ def test_derivatives(tmp_path): _reset_config() +def test_session_label_only_filters_pet(tmp_path): + bids = tmp_path / 'bids' + out_dir = tmp_path / 'out' + work_dir = tmp_path / 'work' + bids.mkdir() + (bids / 'dataset_description.json').write_text( + '{"Name": "Test", "BIDSVersion": "1.8.0"}' + ) + + anat_path = bids / 'sub-01' / 'anat' / 'sub-01_T1w.nii.gz' + anat_path.parent.mkdir(parents=True, exist_ok=True) + nb.Nifti1Image(np.zeros((5, 5, 5)), np.eye(4)).to_filename(anat_path) + + pet_path = bids / 'sub-01' / 'ses-blocked' / 'pet' / 'sub-01_ses-blocked_pet.nii.gz' + pet_path.parent.mkdir(parents=True, exist_ok=True) + nb.Nifti1Image(np.zeros((5, 5, 5, 1)), np.eye(4)).to_filename(pet_path) + (pet_path.with_suffix('').with_suffix('.json')).write_text( + '{"FrameTimesStart": [0], "FrameDuration": [1]}' + ) + + try: + parse_args( + args=[ + str(bids), + str(out_dir), + 'participant', + '--session-label', + 'blocked', + '--skip-bids-validation', + '-w', + str(work_dir), + ] + ) + + filters = config.execution.bids_filters + assert filters.get('pet', {}).get('session') == ['blocked'] + assert 'session' not in filters.get('anat', {}) + finally: + _reset_config() + + def test_pvc_argument_handling(tmp_path, minimal_bids): out_dir = tmp_path / 'out' work_dir = tmp_path / 'work' From f447fd21a2809c10b4fa89532643151412137e6b Mon Sep 17 00:00:00 2001 From: Martin Norgaard Date: Mon, 17 Nov 2025 10:56:08 +0100 Subject: [PATCH 4/5] FIX: style --- petprep/cli/tests/test_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/petprep/cli/tests/test_parser.py b/petprep/cli/tests/test_parser.py index 86c7bcf6..9ece4116 100644 --- a/petprep/cli/tests/test_parser.py +++ b/petprep/cli/tests/test_parser.py @@ -24,9 +24,9 @@ from argparse import ArgumentError -import pytest import nibabel as nb import numpy as np +import pytest from packaging.version import Version from ... import config From db25bbd241dbb984917e5844803bd49ac5a6b3f6 Mon Sep 17 00:00:00 2001 From: Martin Norgaard Date: Mon, 17 Nov 2025 10:58:17 +0100 Subject: [PATCH 5/5] FIX: style --- petprep/cli/parser.py | 2 +- petprep/cli/tests/test_parser.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/petprep/cli/parser.py b/petprep/cli/parser.py index 4598806c..7c470800 100644 --- a/petprep/cli/parser.py +++ b/petprep/cli/parser.py @@ -928,7 +928,7 @@ def parse_args(args=None, namespace=None): if missing_sessions: parser.error( 'One or more session labels were not found in the BIDS directory: ' - f"{', '.join(sorted(missing_sessions))}." + f'{", ".join(sorted(missing_sessions))}.' ) config.execution.participant_label = sorted(participant_label) diff --git a/petprep/cli/tests/test_parser.py b/petprep/cli/tests/test_parser.py index 9ece4116..d259f6f8 100644 --- a/petprep/cli/tests/test_parser.py +++ b/petprep/cli/tests/test_parser.py @@ -232,9 +232,7 @@ def test_session_label_only_filters_pet(tmp_path): out_dir = tmp_path / 'out' work_dir = tmp_path / 'work' bids.mkdir() - (bids / 'dataset_description.json').write_text( - '{"Name": "Test", "BIDSVersion": "1.8.0"}' - ) + (bids / 'dataset_description.json').write_text('{"Name": "Test", "BIDSVersion": "1.8.0"}') anat_path = bids / 'sub-01' / 'anat' / 'sub-01_T1w.nii.gz' anat_path.parent.mkdir(parents=True, exist_ok=True)