From 7a2771b30519293be6f03b5c3f8a83a7f6b4f568 Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Mon, 8 Dec 2025 11:48:48 +0100 Subject: [PATCH 1/2] ENH: add tracer-label functionality for filtering --- petprep/cli/parser.py | 32 +++++++++++++++ petprep/cli/tests/test_parser.py | 70 ++++++++++++++++++++++++++++++++ petprep/config.py | 2 + 3 files changed, 104 insertions(+) diff --git a/petprep/cli/parser.py b/petprep/cli/parser.py index c07844aa..f0809213 100644 --- a/petprep/cli/parser.py +++ b/petprep/cli/parser.py @@ -195,6 +195,13 @@ def _bids_filter(value, parser): help='A space delimited list of session identifiers or a single ' 'identifier (the ses- prefix can be removed)', ) + g_bids.add_argument( + '--tracer-label', + nargs='+', + type=lambda label: label.removeprefix('trc-'), + help='A space delimited list of tracer identifiers or a single ' + 'identifier (the trc- 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') @@ -776,6 +783,13 @@ def parse_args(args=None, namespace=None): 'session': config.execution.session_label, } + if config.execution.tracer_label: + config.execution.bids_filters = config.execution.bids_filters or {} + config.execution.bids_filters['pet'] = { + **config.execution.bids_filters.get('pet', {}), + 'tracer': config.execution.tracer_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.') @@ -946,5 +960,23 @@ def parse_args(args=None, namespace=None): f'{", ".join(sorted(missing_sessions))}.' ) + if config.execution.tracer_label: + tracer_filters = config.execution.bids_filters.get('pet', {}) if config.execution.bids_filters else {} + tracer_filters = {key: value for key, value in tracer_filters.items() if key != 'tracer'} + available_tracers = set( + config.execution.layout.get( + target='tracer', + return_type='id', + subject=list(participant_label) or None, + **tracer_filters, + ) + ) + missing_tracers = set(config.execution.tracer_label) - available_tracers + if missing_tracers: + parser.error( + 'One or more tracer labels were not found in the BIDS directory: ' + f'{", ".join(sorted(missing_tracers))}.' + ) + config.execution.participant_label = sorted(participant_label) config.workflow.skull_strip_template = config.workflow.skull_strip_template[0] diff --git a/petprep/cli/tests/test_parser.py b/petprep/cli/tests/test_parser.py index 79f4ac27..cff21937 100644 --- a/petprep/cli/tests/test_parser.py +++ b/petprep/cli/tests/test_parser.py @@ -266,6 +266,76 @@ def test_session_label_only_filters_pet(tmp_path): _reset_config() +def test_tracer_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' / 'pet' / 'sub-01_trc-ucbj_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', + '--tracer-label', + 'ucbj', + '--skip-bids-validation', + '-w', + str(work_dir), + ] + ) + + filters = config.execution.bids_filters + assert filters.get('pet', {}).get('tracer') == ['ucbj'] + assert 'tracer' not in filters.get('anat', {}) + finally: + _reset_config() + + +def test_tracer_label_validation(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"}') + + pet_path = bids / 'sub-01' / 'pet' / 'sub-01_trc-ucbj_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]}' + ) + + with pytest.raises(SystemExit): + parse_args( + args=[ + str(bids), + str(out_dir), + 'participant', + '--tracer-label', + 'dasb', + '--skip-bids-validation', + '-w', + str(work_dir), + ] + ) + + _reset_config() + + def test_pvc_argument_handling(tmp_path, minimal_bids): out_dir = tmp_path / 'out' work_dir = tmp_path / 'work' diff --git a/petprep/config.py b/petprep/config.py index 8624b384..16647ec7 100644 --- a/petprep/config.py +++ b/petprep/config.py @@ -434,6 +434,8 @@ class execution(_Config): """List of participant identifiers that are to be preprocessed.""" session_label = None """List of session identifiers that are to be preprocessed.""" + tracer_label = None + """List of tracer identifiers that are to be preprocessed.""" task_id = None """Select a particular task from all available in the dataset.""" templateflow_home = _templateflow_home From a61d19bcaab03b2c3f8e3e1fff21f9d800311863 Mon Sep 17 00:00:00 2001 From: mnoergaard Date: Mon, 8 Dec 2025 11:51:24 +0100 Subject: [PATCH 2/2] FIX: style --- petprep/cli/parser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/petprep/cli/parser.py b/petprep/cli/parser.py index f0809213..140d1718 100644 --- a/petprep/cli/parser.py +++ b/petprep/cli/parser.py @@ -961,7 +961,9 @@ def parse_args(args=None, namespace=None): ) if config.execution.tracer_label: - tracer_filters = config.execution.bids_filters.get('pet', {}) if config.execution.bids_filters else {} + tracer_filters = ( + config.execution.bids_filters.get('pet', {}) if config.execution.bids_filters else {} + ) tracer_filters = {key: value for key, value in tracer_filters.items() if key != 'tracer'} available_tracers = set( config.execution.layout.get(