diff --git a/clinica/pipelines/engine.py b/clinica/pipelines/engine.py index a6904536b..1465ead59 100644 --- a/clinica/pipelines/engine.py +++ b/clinica/pipelines/engine.py @@ -11,6 +11,7 @@ from nipype.interfaces.utility import IdentityInterface from nipype.pipeline.engine import Node, Workflow +from clinica.utils.bids import Visit from clinica.utils.check_dependency import SoftwareDependency, ThirdPartySoftware from clinica.utils.group import GroupID, GroupLabel from clinica.utils.stream import log_and_warn @@ -593,6 +594,18 @@ def sessions(self, value: List[str]): self._sessions = value self.is_built = False + @property + def visits(self) -> list[Visit]: + return [ + Visit(subject, session) + for subject, session in zip(self.subjects, self.sessions) + ] + + @visits.setter + def visits(self, value: list[Visit]): + self.subjects = [v.subject for v in value] + self.sessions = [v.session for v in value] + @property def tsv_file(self) -> Optional[Path]: return self._tsv_file @@ -601,24 +614,31 @@ def tsv_file(self) -> Optional[Path]: def info_file(self) -> Path: return self._info_file - @staticmethod - def get_processed_images( - caps_directory: Path, subjects: List[str], sessions: List[str] - ) -> List[str]: - """Extract processed image IDs in `caps_directory` based on `subjects`_`sessions`. + def determine_subject_and_session_to_process(self): + """Query expected output files in the CAPS folder in order to process only those missing. - Todo: - [ ] Implement this static method in all pipelines - [ ] Make it abstract to force overload in future pipelines + If expected output files already exist in the CAPS folder for some subjects and sessions, + then do not process those again. """ - from clinica.utils.exceptions import ClinicaException - from clinica.utils.stream import cprint + from clinica.utils.stream import log_and_warn - cprint(msg="Pipeline finished with errors.", lvl="error") - cprint(msg="CAPS outputs were not found for some image(s):", lvl="error") - raise ClinicaException( - "Implementation on which image(s) failed will appear soon." + visits_already_processed = self.get_processed_visits() + if len(visits_already_processed) == 0: + return + message = ( + f"In the provided CAPS folder {self.caps_directory}, Clinica found already processed " + f"images for {len(visits_already_processed)} visit(s):\n- " ) + message += "\n- ".join([str(visit) for visit in visits_already_processed]) + message += "\nThose visits will be ignored by Clinica." + log_and_warn(message, UserWarning) + self.visits = [ + visit for visit in self.visits if visit not in visits_already_processed + ] + + def get_processed_visits(self) -> list[Visit]: + """Examine the files present in the CAPS output folder and return the visits for which processing has already been done.""" + return [] def _init_nodes(self) -> None: """Init the basic workflow and I/O nodes necessary before build.""" @@ -691,6 +711,7 @@ def build(self): self._check_dependencies() self._check_pipeline_parameters() if not self.has_input_connections(): + self.determine_subject_and_session_to_process() self._build_input_node() self._build_core_nodes() if not self.has_output_connections(): diff --git a/clinica/pipelines/pet/linear/pipeline.py b/clinica/pipelines/pet/linear/pipeline.py index fc31e64e4..863afdfb7 100644 --- a/clinica/pipelines/pet/linear/pipeline.py +++ b/clinica/pipelines/pet/linear/pipeline.py @@ -1,10 +1,12 @@ # Use hash instead of parameters for iterables folder names # Otherwise path will be too long and generate OSError +from pathlib import Path from typing import List from nipype import config from clinica.pipelines.pet.engine import PETPipeline +from clinica.utils.bids import Visit cfg = dict(execution={"parameterize_dirs": False}) config.update_config(cfg) @@ -30,6 +32,53 @@ def _check_custom_dependencies(self) -> None: """Check dependencies that can not be listed in the `info.json` file.""" pass + def get_processed_visits(self) -> list[Visit]: + """Return a list of visits for which the pipeline is assumed to have run already. + + Before running the pipeline, for a given visit, if both the PET SUVR registered image + and the rigid transformation files already exist, then the visit is added to this list. + The pipeline will further skip these visits and run processing only for the remaining + visits. + """ + from functools import reduce + + from clinica.utils.filemanip import extract_visits + from clinica.utils.input_files import ( + pet_linear_nii, + pet_linear_transformation_matrix, + ) + from clinica.utils.inputs import clinica_file_reader + + if not self.caps_directory.is_dir(): + return [] + pet_registered_image, _ = clinica_file_reader( + self.subjects, + self.sessions, + self.caps_directory, + pet_linear_nii( + acq_label=self.parameters["acq_label"], + suvr_reference_region=self.parameters["suvr_reference_region"], + uncropped_image=self.parameters.get("uncropped_image", False), + ), + ) + visits = [set(extract_visits(pet_registered_image))] + transformation, _ = clinica_file_reader( + self.subjects, + self.sessions, + self.caps_directory, + pet_linear_transformation_matrix(tracer=self.parameters["acq_label"]), + ) + visits.append(set(extract_visits(transformation))) + if self.parameters.get("save_PETinT1w", False): + pet_image_in_t1w_space, _ = clinica_file_reader( + self.subjects, + self.sessions, + self.caps_directory, + pet_linear_nii(acq_label=self.parameters["acq_label"], space="T1w"), + ) + visits.append(set(extract_visits(pet_image_in_t1w_space))) + return sorted(list(reduce(lambda x, y: x.intersection(y), visits))) + def get_input_fields(self) -> List[str]: """Specify the list of possible inputs of this pipeline. diff --git a/clinica/pipelines/t1_linear/anat_linear_pipeline.py b/clinica/pipelines/t1_linear/anat_linear_pipeline.py index daa0c714d..e7e2808e3 100644 --- a/clinica/pipelines/t1_linear/anat_linear_pipeline.py +++ b/clinica/pipelines/t1_linear/anat_linear_pipeline.py @@ -6,6 +6,7 @@ from nipype import config from clinica.pipelines.engine import Pipeline +from clinica.utils.bids import Visit from clinica.utils.check_dependency import ThirdPartySoftware from clinica.utils.stream import log_and_warn @@ -67,24 +68,46 @@ def __init__( caps_name=caps_name, ) - @staticmethod - def get_processed_images( - caps_directory: Path, subjects: List[str], sessions: List[str] - ) -> List[str]: - from clinica.utils.filemanip import extract_image_ids - from clinica.utils.input_files import T1W_LINEAR_CROPPED + def get_processed_visits(self) -> list[Visit]: + """Return a list of visits for which the pipeline is assumed to have run already. + + Before running the pipeline, for a given visit, if both the T1w image registered + to the MNI152NLin2009cSym template and the affine transformation estimated with ANTs + already exist, then the visit is added to this list. + The pipeline will further skip these visits and run processing only for the remaining + visits. + """ + from clinica.utils.filemanip import extract_visits + from clinica.utils.input_files import ( + T1W_LINEAR, + T1W_LINEAR_CROPPED, + T1W_TO_MNI_TRANSFORM, + ) from clinica.utils.inputs import clinica_file_reader - image_ids: List[str] = [] - if caps_directory.is_dir(): - cropped_files, _ = clinica_file_reader( - subjects, - sessions, - caps_directory, - T1W_LINEAR_CROPPED, + if not self.caps_directory.is_dir(): + return [] + images, _ = clinica_file_reader( + self.subjects, + self.sessions, + self.caps_directory, + T1W_LINEAR + if self.parameters.get("uncropped_image", False) + else T1W_LINEAR_CROPPED, + ) + visits_having_image = extract_visits(images) + transformation, _ = clinica_file_reader( + self.subjects, + self.sessions, + self.caps_directory, + T1W_TO_MNI_TRANSFORM, + ) + visits_having_transformation = extract_visits(transformation) + return sorted( + list( + set(visits_having_image).intersection(set(visits_having_transformation)) ) - image_ids = extract_image_ids(cropped_files) - return image_ids + ) def _check_custom_dependencies(self) -> None: """Check dependencies that can not be listed in the `info.json` file.""" @@ -119,8 +142,6 @@ def _build_input_node(self): import nipype.interfaces.utility as nutil import nipype.pipeline.engine as npe - from clinica.utils.exceptions import ClinicaBIDSError, ClinicaException - from clinica.utils.filemanip import extract_subjects_sessions_from_filename from clinica.utils.image import get_mni_template from clinica.utils.input_files import T1W_NII, Flair_T2W_NII from clinica.utils.inputs import clinica_file_filter @@ -131,27 +152,6 @@ def _build_input_node(self): "t1" if self.name == "t1-linear" else "flair" ) - # Display image(s) already present in CAPS folder - # =============================================== - processed_ids = self.get_processed_images( - self.caps_directory, self.subjects, self.sessions - ) - if len(processed_ids) > 0: - cprint( - msg=f"Clinica found {len(processed_ids)} image(s) already processed in CAPS directory:", - lvl="warning", - ) - for image_id in processed_ids: - cprint(msg=f"{image_id.replace('_', ' | ')}", lvl="warning") - cprint(msg=f"Image(s) will be ignored by Clinica.", lvl="warning") - input_ids = [ - f"{p_id}_{s_id}" for p_id, s_id in zip(self.subjects, self.sessions) - ] - to_process_ids = list(set(input_ids) - set(processed_ids)) - self.subjects, self.sessions = extract_subjects_sessions_from_filename( - to_process_ids - ) - # Inputs from anat/ folder # ======================== # anat image file: diff --git a/clinica/utils/bids.py b/clinica/utils/bids.py index 6f6107c42..739b1f16b 100644 --- a/clinica/utils/bids.py +++ b/clinica/utils/bids.py @@ -12,11 +12,31 @@ "BIDS_VERSION", "Extension", "Suffix", + "Visit", ] BIDS_VERSION = Version("1.7.0") +@dataclass(frozen=True) +class Visit: + subject: str + session: str + + def __lt__(self, obj): + return (self.subject < obj.subject) or ( + self.subject == obj.subject and self.session < obj.session + ) + + def __gt__(self, obj): + return (self.subject > obj.subject) or ( + self.subject == obj.subject and self.session > obj.session + ) + + def __str__(self) -> str: + return f"{self.subject} {self.session}" + + class Extension(str, Enum): """Possible extensions in BIDS file names.""" diff --git a/clinica/utils/filemanip.py b/clinica/utils/filemanip.py index f62fcced6..f6b73c8f6 100644 --- a/clinica/utils/filemanip.py +++ b/clinica/utils/filemanip.py @@ -2,12 +2,15 @@ from pathlib import Path from typing import Callable, List, Optional, Union +from .bids import Visit + __all__ = [ "UserProvidedPath", "delete_directories", "delete_directories_task", "extract_crash_files_from_log_file", "extract_image_ids", + "extract_visits", "extract_metadata_from_json", "extract_subjects_sessions_from_filename", "get_filename_no_ext", @@ -365,6 +368,13 @@ def extract_image_ids(bids_or_caps_files: list[str]) -> list[str]: return id_bids_or_caps_files +def extract_visits(bids_or_caps_files: list[str]) -> list[Visit]: + return [ + Visit(*image_id.split("_")) + for image_id in extract_image_ids(bids_or_caps_files) + ] + + def extract_subjects_sessions_from_filename( bids_or_caps_files: list[str], ) -> tuple[list[str], list[str]]: diff --git a/clinica/utils/input_files.py b/clinica/utils/input_files.py index f3d925116..835526042 100644 --- a/clinica/utils/input_files.py +++ b/clinica/utils/input_files.py @@ -680,30 +680,73 @@ def pet_volume_normalized_suvr_pet( return information +def _clean_pattern(pattern: str, character: str = "*") -> str: + """Removes multiple '*' wildcards in provided pattern.""" + cleaned = [] + for c in pattern: + if not cleaned or not cleaned[-1] == c == character: + cleaned.append(c) + return "".join(cleaned) + + def pet_linear_nii( - acq_label: Union[str, Tracer], - suvr_reference_region: Union[str, SUVRReferenceRegion], - uncropped_image: bool, + acq_label: Optional[Union[str, Tracer]] = None, + suvr_reference_region: Optional[Union[str, SUVRReferenceRegion]] = None, + uncropped_image: bool = False, + space: str = "mni", + resolution: Optional[int] = None, ) -> dict: from pathlib import Path - acq_label = Tracer(acq_label) - region = SUVRReferenceRegion(suvr_reference_region) - - if uncropped_image: - description = "" - else: + tracer_filter = "*" + tracer_description = "" + if acq_label: + acq_label = Tracer(acq_label) + tracer_filter = f"_trc-{acq_label.value}" + tracer_description = f" obtained with tracer {acq_label.value}" + region_filter = "*" + region_description = "" + if suvr_reference_region: + region = SUVRReferenceRegion(suvr_reference_region) + region_filter = f"_suvr-{region.value}" + region_description = f" for SUVR region {region.value}" + space_filer = f"_space-{'MNI152NLin2009cSym' if space == 'mni' else 'T1w'}" + space_description = f" affinely registered to the {'MNI152NLin2009cSym template' if space == 'mni' else 'associated T1w image'}" + description = "*" + if space == "mni" and not uncropped_image: description = "_desc-Crop" - + resolution_filter = "*" + resolution_description = "" + if resolution: + resolution_explicit = f"{resolution}x{resolution}x{resolution}" + resolution_filter = f"_res-{resolution_explicit}" + resolution_description = f" of resolution {resolution_explicit}" information = { "pattern": Path("pet_linear") - / f"*_trc-{acq_label.value}_pet_space-MNI152NLin2009cSym{description}_res-1x1x1_suvr-{region.value}_pet.nii.gz", - "description": "", + / _clean_pattern( + f"*{tracer_filter}{space_filer}{description}{resolution_filter}{region_filter}_pet.nii.gz" + ), + "description": ( + f"{'Cropped ' if space == 'mni' and not uncropped_image else ''}PET nifti image{resolution_description}" + f"{tracer_description}{region_description}{space_description} resulting from the pet-linear pipeline" + ), "needed_pipeline": "pet-linear", } return information +def pet_linear_transformation_matrix(tracer: Union[str, Tracer]) -> dict: + from pathlib import Path + + tracer = Tracer(tracer) + + return { + "pattern": Path("pet_linear") / f"*_trc-{tracer.value}_space-T1w_rigid.mat", + "description": "Rigid transformation matrix between the PET and T1w images estimated with ANTs.", + "needed_pipeline": "pet-linear", + } + + # CUSTOM def custom_pipeline(pattern, description): information = {"pattern": pattern, "description": description} diff --git a/clinica/utils/testing_utils.py b/clinica/utils/testing_utils.py index b133c7f8f..9fb26efe6 100644 --- a/clinica/utils/testing_utils.py +++ b/clinica/utils/testing_utils.py @@ -150,7 +150,8 @@ def build_caps_directory(directory: Path, configuration: dict) -> Path: Dictionary containing the configuration for building the fake CAPS directory. It should have the following structure: - "groups": ["group_labels"...] - - "pipelines": ["pipeline_names"...] + - "pipelines": {"pipeline_names": config}, where config is a dictionary + specifying more details for the files that should be written. - "subjects": {"subject_labels": ["session_labels"...]}. Returns @@ -186,13 +187,13 @@ def _build_groups(directory: Path, configuration: dict) -> None: (directory / "groups").mkdir() for group_label in configuration["groups"]: (directory / "groups" / f"group-{group_label}").mkdir() - for pipeline in configuration["pipelines"]: - (directory / "groups" / f"group-{group_label}" / pipeline).mkdir() + for pipeline_name, pipeline_config in configuration["pipelines"].items(): + (directory / "groups" / f"group-{group_label}" / pipeline_name).mkdir() ( directory / "groups" / f"group-{group_label}" - / pipeline + / pipeline_name / f"group-{group_label}_template.nii.gz" ).touch() @@ -200,29 +201,55 @@ def _build_groups(directory: Path, configuration: dict) -> None: def _build_subjects(directory: Path, configuration: dict) -> None: """Build a fake subjects file structure in a CAPS directory if requested.""" if "subjects" in configuration: - (directory / "subjects").mkdir() + (directory / "subjects").mkdir(exist_ok=True) for sub, sessions in configuration["subjects"].items(): - (directory / "subjects" / sub).mkdir() + (directory / "subjects" / sub).mkdir(exist_ok=True) for ses in sessions: - (directory / "subjects" / sub / ses).mkdir() - for pipeline in configuration["pipelines"]: - (directory / "subjects" / sub / ses / pipeline).mkdir() - if pipeline == "t1_linear": - _build_t1_linear(directory, sub, ses) - if pipeline == "t1": + (directory / "subjects" / sub / ses).mkdir(exist_ok=True) + for pipeline_name, pipeline_config in configuration[ + "pipelines" + ].items(): + (directory / "subjects" / sub / ses / pipeline_name).mkdir( + exist_ok=True + ) + if pipeline_name == "t1_linear": + _build_t1_linear(directory, sub, ses, pipeline_config) + if pipeline_name == "pet_linear": + _build_pet_linear(directory, sub, ses, pipeline_config) + if pipeline_name == "t1": _build_t1(directory, sub, ses, configuration) -def _build_t1_linear(directory: Path, sub: str, ses: str) -> None: +def _build_t1_linear(directory: Path, sub: str, ses: str, config: dict) -> None: """Build a fake t1-linear file structure in a CAPS directory.""" - ( - directory - / "subjects" - / sub - / ses - / "t1_linear" - / f"{sub}_{ses}_T1w_space-MNI152NLin2009cSym_res-1x1x1_T1w.nii.gz" - ).touch() + uncropped = config.get("uncropped_image", False) + for filename in ( + f"{sub}_{ses}_space-MNI152NLin2009cSym{'' if uncropped else '_desc-Crop'}_res-1x1x1_T1w.nii.gz", + f"{sub}_{ses}_space-MNI152NLin2009cSym_res-1x1x1_affine.mat", + ): + (directory / "subjects" / sub / ses / "t1_linear" / filename).touch() + + +def _build_pet_linear(directory: Path, sub: str, ses: str, config: dict) -> None: + """Build a fake pet-linear file structure in a CAPS directory.""" + from clinica.utils.pet import SUVRReferenceRegion, Tracer + + tracer = Tracer(config["acq_label"]) + suvr = SUVRReferenceRegion(config["suvr_reference_region"]) + if config.get("save_PETinT1w", False): + ( + directory + / "subjects" + / sub + / ses + / "pet_linear" + / f"{sub}_{ses}_trc-{tracer.value}_space-T1w_pet.nii.gz" + ).touch() + for filename in ( + f"{sub}_{ses}_trc-{tracer.value}_space-MNI152NLin2009cSym_desc-Crop_res-1x1x1_suvr-{suvr.value}_pet.nii.gz", + f"{sub}_{ses}_trc-{tracer.value}_space-T1w_rigid.mat", + ): + (directory / "subjects" / sub / ses / "pet_linear" / filename).touch() def _build_t1(directory: Path, sub: str, ses: str, configuration: dict) -> None: diff --git a/test/unittests/pipelines/anatomical/freesurfer/atlas/test_t1_freesurfer_atlas_pipeline.py b/test/unittests/pipelines/anatomical/freesurfer/atlas/test_t1_freesurfer_atlas_pipeline.py index 7507327ef..35e2f3c2b 100644 --- a/test/unittests/pipelines/anatomical/freesurfer/atlas/test_t1_freesurfer_atlas_pipeline.py +++ b/test/unittests/pipelines/anatomical/freesurfer/atlas/test_t1_freesurfer_atlas_pipeline.py @@ -6,7 +6,7 @@ def test_t1_freesurfer_atlas_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["compute-atlas"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"compute-atlas": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = T1FreeSurferAtlas(caps_directory=str(caps)) @@ -35,7 +35,7 @@ def test_t1_freesurfer_atlas_dependencies(tmp_path, mocker): ) caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["compute-atlas"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"compute-atlas": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = T1FreeSurferAtlas(caps_directory=str(caps)) diff --git a/test/unittests/pipelines/anatomical/freesurfer/longitudinal/correction/test_t1_freesurfer_longitudinal_correction_pipeline.py b/test/unittests/pipelines/anatomical/freesurfer/longitudinal/correction/test_t1_freesurfer_longitudinal_correction_pipeline.py index 5fb7f7a41..c54f0dfe4 100644 --- a/test/unittests/pipelines/anatomical/freesurfer/longitudinal/correction/test_t1_freesurfer_longitudinal_correction_pipeline.py +++ b/test/unittests/pipelines/anatomical/freesurfer/longitudinal/correction/test_t1_freesurfer_longitudinal_correction_pipeline.py @@ -8,7 +8,7 @@ def test_t1_freesurfer_longitudinal_correction_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["compute-atlas"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"compute-atlas": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = T1FreeSurferLongitudinalCorrection(caps_directory=str(caps)) @@ -40,7 +40,7 @@ def test_t1_freesurfer_longitudinal_correction_dependencies(tmp_path, mocker): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-freesurfer-longitudinal-correction"], + "pipelines": {"t1-freesurfer-longitudinal-correction": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) diff --git a/test/unittests/pipelines/anatomical/freesurfer/longitudinal/template/test_t1_freesurfer_longitudinal_template_pipeline.py b/test/unittests/pipelines/anatomical/freesurfer/longitudinal/template/test_t1_freesurfer_longitudinal_template_pipeline.py index 6f1101652..a6c009020 100644 --- a/test/unittests/pipelines/anatomical/freesurfer/longitudinal/template/test_t1_freesurfer_longitudinal_template_pipeline.py +++ b/test/unittests/pipelines/anatomical/freesurfer/longitudinal/template/test_t1_freesurfer_longitudinal_template_pipeline.py @@ -8,7 +8,7 @@ def test_t1_freesurfer_longitudinal_template_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["compute-atlas"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"compute-atlas": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = T1FreeSurferTemplate(caps_directory=str(caps)) @@ -39,7 +39,10 @@ def test_t1_freesurfer_longitudinal_template_dependencies(tmp_path, mocker): ) caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["t1-freesurfer-template"], "subjects": {"sub-01": ["ses-M000"]}}, + { + "pipelines": {"t1-freesurfer-template": {}}, + "subjects": {"sub-01": ["ses-M000"]}, + }, ) pipeline = T1FreeSurferTemplate(caps_directory=str(caps)) diff --git a/test/unittests/pipelines/dwi/connectome/test_dwi_connectome_pipeline.py b/test/unittests/pipelines/dwi/connectome/test_dwi_connectome_pipeline.py index b48d5cb77..516a55286 100644 --- a/test/unittests/pipelines/dwi/connectome/test_dwi_connectome_pipeline.py +++ b/test/unittests/pipelines/dwi/connectome/test_dwi_connectome_pipeline.py @@ -6,7 +6,7 @@ def test_dwi_connectome_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["dwi-connectome"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"dwi-connectome": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = DwiConnectome(caps_directory=str(caps)) @@ -45,7 +45,7 @@ def test_dwi_connectome_dependencies(tmp_path, mocker): ) caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["dwi-connectome"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"dwi-connectome": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = DwiConnectome(caps_directory=str(caps)) diff --git a/test/unittests/pipelines/dwi/dti/test_dwi_dti_pipeline.py b/test/unittests/pipelines/dwi/dti/test_dwi_dti_pipeline.py index 0d3a7e935..749c58619 100644 --- a/test/unittests/pipelines/dwi/dti/test_dwi_dti_pipeline.py +++ b/test/unittests/pipelines/dwi/dti/test_dwi_dti_pipeline.py @@ -6,7 +6,7 @@ def test_dwi_dti_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["dwi-dti"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"dwi-dti": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = DwiDti(caps_directory=str(caps)) @@ -45,7 +45,7 @@ def test_dwi_dti_dependencies(tmp_path, mocker): ) caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["dwi-dti"], "subjects": {"sub-01": ["ses-M000"]}}, + {"pipelines": {"dwi-dti": {}}, "subjects": {"sub-01": ["ses-M000"]}}, ) pipeline = DwiDti(caps_directory=str(caps)) diff --git a/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_fmap_pipeline.py b/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_fmap_pipeline.py index 8580a4898..517376ee9 100644 --- a/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_fmap_pipeline.py +++ b/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_fmap_pipeline.py @@ -9,7 +9,7 @@ def test_dwi_preprocessing_using_fmap_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["dwi-preprocessing-using-phasediff-fmap"], + "pipelines": {"dwi-preprocessing-using-phasediff-fmap": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) @@ -54,7 +54,7 @@ def test_dwi_preprocessing_using_fmap_dependencies(tmp_path, mocker): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["dwi-preprocessing-using-phasediff-fmap"], + "pipelines": {"dwi-preprocessing-using-phasediff-fmap": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) diff --git a/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_t1_pipeline.py b/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_t1_pipeline.py index fabcc86ba..e0b433e45 100644 --- a/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_t1_pipeline.py +++ b/test/unittests/pipelines/dwi/preprocessing/test_dwi_preprocessing_using_t1_pipeline.py @@ -13,7 +13,7 @@ def test_dwi_preprocessing_using_t1_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["dwi-preprocessing-using-t1"], + "pipelines": {"dwi-preprocessing-using-t1": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) @@ -66,7 +66,7 @@ def test_dwi_preprocessing_using_t1_dependencies(tmp_path, mocker): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["dwi-preprocessing-using-t1"], + "pipelines": {"dwi-preprocessing-using-t1": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) diff --git a/test/unittests/pipelines/pet/test_pet_linear_pipeline.py b/test/unittests/pipelines/pet/test_pet_linear_pipeline.py index 47ed37ccf..6e5558e2e 100644 --- a/test/unittests/pipelines/pet/test_pet_linear_pipeline.py +++ b/test/unittests/pipelines/pet/test_pet_linear_pipeline.py @@ -1,4 +1,7 @@ -from clinica.utils.testing_utils import build_bids_directory +from packaging.version import Version + +from clinica.utils.bids import Visit +from clinica.utils.testing_utils import build_bids_directory, build_caps_directory def test_pet_linear_info_loading(tmp_path): @@ -19,7 +22,6 @@ def test_pet_linear_info_loading(tmp_path): def test_pet_linear_dependencies(tmp_path, mocker): from packaging.specifiers import SpecifierSet - from packaging.version import Version from clinica.pipelines.pet.linear.pipeline import PETLinear from clinica.utils.check_dependency import SoftwareDependency, ThirdPartySoftware @@ -36,3 +38,157 @@ def test_pet_linear_dependencies(tmp_path, mocker): ThirdPartySoftware.ANTS, SpecifierSet(">=2.2.0"), Version("2.3.1") ) ] + + +def test_pet_linear_get_processed_visits_empty(tmp_path, mocker): + from clinica.pipelines.pet.linear.pipeline import PETLinear + from clinica.utils.pet import SUVRReferenceRegion, Tracer + + mocker.patch( + "clinica.utils.check_dependency._get_ants_version", + return_value=Version("2.3.1"), + ) + bids = build_bids_directory( + tmp_path / "bids", {"sub-01": ["ses-M000", "ses-M006"], "sub-02": ["ses-M000"]} + ) + caps = build_caps_directory(tmp_path / "caps", {}) + + pipeline = PETLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={ + "acq_label": Tracer.FDG, + "suvr_reference_region": SUVRReferenceRegion.PONS, + "uncropped_image": False, + }, + ) + assert pipeline.get_processed_visits() == [] + + +def test_pet_linear_get_processed_visits(tmp_path, mocker): + """Test the get_processed_visits for PETLinear. + + We build the following CAPS dataset: + + caps + ├── dataset_description.json + └── subjects + ├── sub-01 + │ ├── ses-M000 + │ │ └── pet_linear + │ │ ├── sub-01_ses-M000_trc-18FAV45_space-MNI152NLin2009cSym_desc-Crop_res-1x1x1_suvr-pons2_pet.nii.gz + │ │ └── sub-01_ses-M000_trc-18FAV45_space-T1w_rigid.mat + │ └── ses-M006 + │ └── pet_linear + │ ├── sub-01_ses-M006_trc-18FAV45_space-MNI152NLin2009cSym_desc-Crop_res-1x1x1_suvr-pons2_pet.nii.gz + │ ├── sub-01_ses-M006_trc-18FAV45_space-T1w_rigid.mat + │ ├── sub-01_ses-M006_trc-18FFDG_space-MNI152NLin2009cSym_desc-Crop_res-1x1x1_suvr-pons_pet.nii.gz + │ └── sub-01_ses-M006_trc-18FFDG_space-T1w_rigid.mat + └── sub-02 + ├── ses-M000 + │ └── pet_linear + │ ├── sub-02_ses-M000_trc-18FFDG_space-MNI152NLin2009cSym_desc-Crop_res-1x1x1_suvr-pons_pet.nii.gz + │ ├── sub-02_ses-M000_trc-18FFDG_space-T1w_pet.nii.gz + │ └── sub-02_ses-M000_trc-18FFDG_space-T1w_rigid.mat + └── ses-M006 + └── pet_linear + ├── sub-02_ses-M006_trc-18FAV45_space-MNI152NLin2009cSym_desc-Crop_res-1x1x1_suvr-pons2_pet.nii.gz + └── sub-02_ses-M006_trc-18FAV45_space-T1w_rigid.mat + + And make sure that a PETLinear pipeline with tracer 18FFDG et pons SUVR will only consider + (sub-01, ses-M006) and (sub-02, ses-M000) as already processed. The other folders contain + PET images for the pet-linear pipeline but they were obtained with different tracers and SUVR. + """ + from clinica.pipelines.pet.linear.pipeline import PETLinear + from clinica.utils.pet import SUVRReferenceRegion, Tracer + + mocker.patch( + "clinica.utils.check_dependency._get_ants_version", + return_value=Version("2.3.1"), + ) + bids = build_bids_directory( + tmp_path / "bids", {"sub-01": ["ses-M000", "ses-M006"], "sub-02": ["ses-M000"]} + ) + caps = build_caps_directory( + tmp_path / "caps", + { + "pipelines": { + "pet_linear": { + "acq_label": Tracer.FDG, + "suvr_reference_region": SUVRReferenceRegion.PONS, + "uncropped_image": False, + "save_PETinT1w": True, + } + }, + "subjects": { + "sub-01": ["ses-M006"], + "sub-02": ["ses-M000"], + }, + }, + ) + caps = build_caps_directory( + tmp_path / "caps", + { + "pipelines": { + "pet_linear": { + "acq_label": Tracer.AV45, + "suvr_reference_region": SUVRReferenceRegion.PONS2, + "uncropped_image": False, + } + }, + "subjects": { + "sub-01": ["ses-M000", "ses-M006"], + "sub-02": ["ses-M006"], + }, + }, + ) + # We remove the pet image registered on the T1w image for sub-01 and session M006 + ( + caps + / "subjects" + / "sub-01" + / "ses-M006" + / "pet_linear" + / "sub-01_ses-M006_trc-18FFDG_space-T1w_pet.nii.gz" + ).unlink() + pipeline = PETLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={ + "acq_label": Tracer.FDG, + "suvr_reference_region": SUVRReferenceRegion.PONS, + "uncropped_image": False, + "save_PETinT1w": True, + }, + ) + assert pipeline.get_processed_visits() == [Visit("sub-02", "ses-M000")] + + pipeline = PETLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={ + "acq_label": Tracer.FDG, + "suvr_reference_region": SUVRReferenceRegion.PONS, + "uncropped_image": False, + "save_PETinT1w": False, + }, + ) + + assert pipeline.get_processed_visits() == [ + Visit("sub-01", "ses-M006"), + Visit("sub-02", "ses-M000"), + ] + + # We remove the transformation matrix of the tracer of interest for sub-01 and session M006 + ( + caps + / "subjects" + / "sub-01" + / "ses-M006" + / "pet_linear" + / "sub-01_ses-M006_trc-18FFDG_space-T1w_rigid.mat" + ).unlink() + + # The corresponding visit is not considered as "processed" anymore because the transformation is + # missing (even though the pet image is still here...) + assert pipeline.get_processed_visits() == [Visit("sub-02", "ses-M000")] diff --git a/test/unittests/pipelines/t1_linear/test_anat_linear_pipeline.py b/test/unittests/pipelines/t1_linear/test_anat_linear_pipeline.py index 18b31294b..e8788a603 100644 --- a/test/unittests/pipelines/t1_linear/test_anat_linear_pipeline.py +++ b/test/unittests/pipelines/t1_linear/test_anat_linear_pipeline.py @@ -3,6 +3,7 @@ import pytest from packaging.version import Version +from clinica.utils.bids import Visit from clinica.utils.testing_utils import build_bids_directory, build_caps_directory @@ -115,3 +116,251 @@ def test_anat_linear_dependencies(tmp_path, mocker): ) assert pipeline.dependencies == [] + + +@pytest.mark.parametrize( + "config,expected", + [ + ({}, []), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": {"sub-01": ["ses-M006"]}, + }, + [Visit("sub-01", "ses-M006")], + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": {"sub-01": ["ses-M000", "ses-M006"]}, + }, + [Visit("sub-01", "ses-M000"), Visit("sub-01", "ses-M006")], + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": { + "sub-01": ["ses-M000", "ses-M006"], + "sub-02": ["ses-M000"], + }, + }, + [ + Visit("sub-01", "ses-M000"), + Visit("sub-01", "ses-M006"), + Visit("sub-02", "ses-M000"), + ], + ), + ], +) +def test_anat_linear_get_processed_visits_uncropped_images( + tmp_path, mocker, config, expected +): + from clinica.pipelines.t1_linear.anat_linear_pipeline import AnatLinear + + mocker.patch( + "clinica.utils.check_dependency._get_ants_version", + return_value=Version("2.2.1"), + ) + bids = build_bids_directory( + tmp_path / "bids", {"sub-01": ["ses-M000", "ses-M006"], "sub-02": ["ses-M000"]} + ) + caps = build_caps_directory(tmp_path / "caps", config) + pipeline = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": False}, + ) + pipeline2 = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": True}, + ) + + assert pipeline.get_processed_visits() == expected + # pipeline2 always find an empty list of processed images because we want un-cropped images + # and the CAPS folder only contains cropped images + assert pipeline2.get_processed_visits() == [] + + +@pytest.mark.parametrize( + "config,expected", + [ + ({}, []), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": True}}, + "subjects": {"sub-01": ["ses-M006"]}, + }, + [Visit("sub-01", "ses-M006")], + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": True}}, + "subjects": {"sub-01": ["ses-M000", "ses-M006"]}, + }, + [Visit("sub-01", "ses-M000"), Visit("sub-01", "ses-M006")], + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": True}}, + "subjects": { + "sub-01": ["ses-M000", "ses-M006"], + "sub-02": ["ses-M000"], + }, + }, + [ + Visit("sub-01", "ses-M000"), + Visit("sub-01", "ses-M006"), + Visit("sub-02", "ses-M000"), + ], + ), + ], +) +def test_anat_linear_get_processed_visits_cropped_images( + tmp_path, mocker, config, expected +): + from clinica.pipelines.t1_linear.anat_linear_pipeline import AnatLinear + + mocker.patch( + "clinica.utils.check_dependency._get_ants_version", + return_value=Version("2.2.1"), + ) + bids = build_bids_directory( + tmp_path / "bids", {"sub-01": ["ses-M000", "ses-M006"], "sub-02": ["ses-M000"]} + ) + caps = build_caps_directory(tmp_path / "caps", config) + pipeline = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": False}, + ) + pipeline2 = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": True}, + ) + + assert pipeline2.get_processed_visits() == expected + # pipeline always find an empty list of processed images because we want cropped images + # and the CAPS folder only contains un-cropped images + assert pipeline.get_processed_visits() == [] + + +@pytest.mark.parametrize( + "config,remaining_subjects,remaining_sessions", + [ + ({}, ["sub-01", "sub-01", "sub-02"], ["ses-M000", "ses-M006", "ses-M000"]), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": {"sub-01": ["ses-M006"]}, + }, + ["sub-01", "sub-02"], + ["ses-M000", "ses-M000"], + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": {"sub-01": ["ses-M000", "ses-M006"]}, + }, + ["sub-02"], + ["ses-M000"], + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": { + "sub-01": ["ses-M000", "ses-M006"], + "sub-02": ["ses-M000"], + }, + }, + [], + [], + ), + ], +) +def test_determine_subject_and_session_to_process( + tmp_path, mocker, config, remaining_subjects, remaining_sessions +): + from clinica.pipelines.t1_linear.anat_linear_pipeline import AnatLinear + + mocker.patch( + "clinica.utils.check_dependency._get_ants_version", + return_value=Version("2.2.1"), + ) + bids = build_bids_directory( + tmp_path / "bids", {"sub-01": ["ses-M000", "ses-M006"], "sub-02": ["ses-M000"]} + ) + caps = build_caps_directory(tmp_path / "caps", config) + pipeline = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": False}, + ) + pipeline.determine_subject_and_session_to_process() + pipeline2 = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": True}, + ) + pipeline2.determine_subject_and_session_to_process() + + assert pipeline.subjects == remaining_subjects + assert pipeline.sessions == remaining_sessions + assert pipeline2.subjects == ["sub-01", "sub-01", "sub-02"] + assert pipeline2.sessions == ["ses-M000", "ses-M006", "ses-M000"] + + +@pytest.mark.parametrize( + "configuration,expected_message", + [ + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": { + "sub-01": ["ses-M000", "ses-M006"], + "sub-02": ["ses-M000"], + }, + }, + ( + "Clinica found already processed images for 3 visit(s):" + "\n- sub-01 ses-M000\n- sub-01 ses-M006\n- sub-02 ses-M000" + "\nThose visits will be ignored by Clinica." + ), + ), + ( + { + "pipelines": {"t1_linear": {"uncropped_image": False}}, + "subjects": {"sub-01": ["ses-M000", "ses-M006"]}, + }, + ( + "Clinica found already processed images for 2 visit(s):" + "\n- sub-01 ses-M000\n- sub-01 ses-M006" + "\nThose visits will be ignored by Clinica." + ), + ), + ], +) +def test_determine_subject_and_session_to_process_warning( + tmp_path, mocker, configuration, expected_message +): + from clinica.pipelines.t1_linear.anat_linear_pipeline import AnatLinear + + mocker.patch( + "clinica.utils.check_dependency._get_ants_version", + return_value=Version("2.2.1"), + ) + bids = build_bids_directory( + tmp_path / "bids", {"sub-01": ["ses-M000", "ses-M006"], "sub-02": ["ses-M000"]} + ) + caps = build_caps_directory(tmp_path / "caps", configuration) + pipeline = AnatLinear( + bids_directory=str(bids), + caps_directory=str(caps), + parameters={"uncropped_image": False}, + ) + with pytest.warns( + UserWarning, + match=re.escape(f"In the provided CAPS folder {caps}, {expected_message}"), + ): + pipeline.determine_subject_and_session_to_process() diff --git a/test/unittests/pipelines/t1_volume_create_dartel/test_t1_volume_create_dartel_pipeline.py b/test/unittests/pipelines/t1_volume_create_dartel/test_t1_volume_create_dartel_pipeline.py index 0ca832f16..57e10f0dc 100644 --- a/test/unittests/pipelines/t1_volume_create_dartel/test_t1_volume_create_dartel_pipeline.py +++ b/test/unittests/pipelines/t1_volume_create_dartel/test_t1_volume_create_dartel_pipeline.py @@ -9,7 +9,7 @@ def test_t1_volume_create_dartel_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-volume-create-dartel"], + "pipelines": {"t1-volume-create-dartel": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) @@ -41,7 +41,7 @@ def test_t1_volume_create_dartel_dependencies(tmp_path, mocker): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-volume-create-dartel"], + "pipelines": {"t1-volume-create-dartel": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) diff --git a/test/unittests/pipelines/t1_volume_dartel2mni/test_t1_volume_dartel2mni_pipeline.py b/test/unittests/pipelines/t1_volume_dartel2mni/test_t1_volume_dartel2mni_pipeline.py index fa307b6a6..26f0d44e5 100644 --- a/test/unittests/pipelines/t1_volume_dartel2mni/test_t1_volume_dartel2mni_pipeline.py +++ b/test/unittests/pipelines/t1_volume_dartel2mni/test_t1_volume_dartel2mni_pipeline.py @@ -8,7 +8,10 @@ def test_t1_volume_dartel2mni_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["t1-volume-dartel2mni"], "subjects": {"sub-01": ["ses-M000"]}}, + { + "pipelines": {"t1-volume-dartel2mni": {}}, + "subjects": {"sub-01": ["ses-M000"]}, + }, ) pipeline = T1VolumeDartel2MNI(caps_directory=str(caps), group_label="test") @@ -37,7 +40,10 @@ def test_t1_volume_dartel2mni_dependencies(tmp_path, mocker): ) caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["t1-volume-dartel2mni"], "subjects": {"sub-01": ["ses-M000"]}}, + { + "pipelines": {"t1-volume-dartel2mni": {}}, + "subjects": {"sub-01": ["ses-M000"]}, + }, ) pipeline = T1VolumeDartel2MNI(caps_directory=str(caps), group_label="test") diff --git a/test/unittests/pipelines/t1_volume_parcellation/test_t1_volume_parcellation_pipeline.py b/test/unittests/pipelines/t1_volume_parcellation/test_t1_volume_parcellation_pipeline.py index 843409c32..ab9b09b5c 100644 --- a/test/unittests/pipelines/t1_volume_parcellation/test_t1_volume_parcellation_pipeline.py +++ b/test/unittests/pipelines/t1_volume_parcellation/test_t1_volume_parcellation_pipeline.py @@ -8,7 +8,10 @@ def test_t1_volume_parcellation_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["t1-volume-parcellation"], "subjects": {"sub-01": ["ses-M000"]}}, + { + "pipelines": {"t1-volume-parcellation": {}}, + "subjects": {"sub-01": ["ses-M000"]}, + }, ) pipeline = T1VolumeParcellation(caps_directory=str(caps), group_label="test") @@ -29,7 +32,10 @@ def test_t1_volume_parcellation_dependencies(tmp_path): caps = build_caps_directory( tmp_path / "caps", - {"pipelines": ["t1-volume-parcellation"], "subjects": {"sub-01": ["ses-M000"]}}, + { + "pipelines": {"t1-volume-parcellation": {}}, + "subjects": {"sub-01": ["ses-M000"]}, + }, ) pipeline = T1VolumeParcellation(caps_directory=str(caps), group_label="test") diff --git a/test/unittests/pipelines/t1_volume_register_dartel/test_t1_volume_register_dartel_pipeline.py b/test/unittests/pipelines/t1_volume_register_dartel/test_t1_volume_register_dartel_pipeline.py index a5fbb9981..c35fc0ca2 100644 --- a/test/unittests/pipelines/t1_volume_register_dartel/test_t1_volume_register_dartel_pipeline.py +++ b/test/unittests/pipelines/t1_volume_register_dartel/test_t1_volume_register_dartel_pipeline.py @@ -9,7 +9,7 @@ def test_t1_volume_register_dartel_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-volume-register-dartel"], + "pipelines": {"t1-volume-register-dartel": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) @@ -41,7 +41,7 @@ def test_t1_volume_register_dartel_dependencies(tmp_path, mocker): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-volume-register-dartel"], + "pipelines": {"t1-volume-register-dartel": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) diff --git a/test/unittests/pipelines/t1_volume_tissue_segmentation/test_t1_volume_tissue_segmentation_pipeline.py b/test/unittests/pipelines/t1_volume_tissue_segmentation/test_t1_volume_tissue_segmentation_pipeline.py index dc73842fb..a6f15be3b 100644 --- a/test/unittests/pipelines/t1_volume_tissue_segmentation/test_t1_volume_tissue_segmentation_pipeline.py +++ b/test/unittests/pipelines/t1_volume_tissue_segmentation/test_t1_volume_tissue_segmentation_pipeline.py @@ -9,7 +9,7 @@ def test_t1_volume_tissue_segmentation_info_loading(tmp_path): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-volume-tissue-segmentation"], + "pipelines": {"t1-volume-tissue-segmentation": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) @@ -42,7 +42,7 @@ def test_t1_volume_tissue_segmentation_dependencies(tmp_path, mocker): caps = build_caps_directory( tmp_path / "caps", { - "pipelines": ["t1-volume-tissue-segmentation"], + "pipelines": {"t1-volume-tissue-segmentation": {}}, "subjects": {"sub-01": ["ses-M000"]}, }, ) diff --git a/test/unittests/pydra/test_interfaces.py b/test/unittests/pydra/test_interfaces.py index f879f6a02..bcbdb2154 100644 --- a/test/unittests/pydra/test_interfaces.py +++ b/test/unittests/pydra/test_interfaces.py @@ -63,7 +63,7 @@ def test_caps_reader(tmp_path): structure = { "groups": ["UnitTest"], - "pipelines": ["t1"], + "pipelines": {"t1": {}}, "subjects": { "sub-01": ["ses-M00", "ses-M06"], "sub-03": ["ses-M00"], diff --git a/test/unittests/utils/test_input_files.py b/test/unittests/utils/test_input_files.py index 1625ca53e..1a16edb1b 100644 --- a/test/unittests/utils/test_input_files.py +++ b/test/unittests/utils/test_input_files.py @@ -87,3 +87,72 @@ def test_dwi_dti_query_error(): match="'foo' is not a valid DTIBasedMeasure", ): dwi_dti("foo") + + +@pytest.mark.parametrize( + "input_parameters,expected_description,expected_pattern", + [ + ( + {}, + ( + "Cropped PET nifti image affinely registered to the MNI152NLin2009cSym " + "template resulting from the pet-linear pipeline" + ), + "pet_linear/*_space-MNI152NLin2009cSym_desc-Crop*_pet.nii.gz", + ), + ( + { + "acq_label": "18FFDG", + }, + ( + "Cropped PET nifti image obtained with tracer 18FFDG affinely registered to the " + "MNI152NLin2009cSym template resulting from the pet-linear pipeline" + ), + "pet_linear/*_trc-18FFDG_space-MNI152NLin2009cSym_desc-Crop*_pet.nii.gz", + ), + ( + { + "acq_label": "18FAV45", + "suvr_reference_region": "pons", + "uncropped_image": True, + "resolution": 2, + }, + ( + "PET nifti image of resolution 2x2x2 obtained with tracer 18FAV45 for SUVR region " + "pons affinely registered to the MNI152NLin2009cSym template resulting from the pet-linear pipeline" + ), + "pet_linear/*_trc-18FAV45_space-MNI152NLin2009cSym*_res-2x2x2_suvr-pons_pet.nii.gz", + ), + ( + { + "suvr_reference_region": "pons2", + "resolution": 2, + "space": "T1w", + }, + ( + "PET nifti image of resolution 2x2x2 for SUVR region pons2 affinely " + "registered to the associated T1w image resulting from the pet-linear pipeline" + ), + "pet_linear/*_space-T1w*_res-2x2x2_suvr-pons2_pet.nii.gz", + ), + ( + { + "acq_label": "18FFDG", + "space": "T1w", + }, + ( + "PET nifti image obtained with tracer 18FFDG affinely registered to the " + "associated T1w image resulting from the pet-linear pipeline" + ), + "pet_linear/*_trc-18FFDG_space-T1w*_pet.nii.gz", + ), + ], +) +def test_pet_linear_nii(input_parameters, expected_description, expected_pattern): + from clinica.utils.input_files import pet_linear_nii + + query = pet_linear_nii(**input_parameters) + + assert query["description"] == expected_description + assert query["needed_pipeline"] == "pet-linear" + assert str(query["pattern"]) == expected_pattern diff --git a/test/unittests/utils/test_utils_inputs.py b/test/unittests/utils/test_utils_inputs.py index 6dbebc647..0ca618c1f 100644 --- a/test/unittests/utils/test_utils_inputs.py +++ b/test/unittests/utils/test_utils_inputs.py @@ -631,7 +631,7 @@ def test_clinica_file_reader_caps_directory(tmp_path): from clinica.utils.inputs import clinica_file_reader config = { - "pipelines": ["t1_linear"], + "pipelines": {"t1_linear": {"uncropped_image": True}}, "subjects": { "sub-01": ["ses-M00"], "sub-02": ["ses-M00", "ses-M06"],