diff --git a/clinica/iotools/bids_dataset_description.py b/clinica/iotools/bids_dataset_description.py index cb8d0e7fa..f39d20858 100644 --- a/clinica/iotools/bids_dataset_description.py +++ b/clinica/iotools/bids_dataset_description.py @@ -1,12 +1,21 @@ -from enum import Enum +import json +from pathlib import Path from typing import IO from attrs import define, fields from cattr.gen import make_dict_unstructure_fn, override from cattr.preconf.json import make_converter +from packaging.version import InvalidVersion, Version from clinica.utils.bids import BIDS_VERSION +from clinica.utils.exceptions import ClinicaBIDSError from clinica.utils.inputs import DatasetType +from clinica.utils.stream import log_and_raise + +__all__ = [ + "BIDSDatasetDescription", + "get_bids_version", +] @define @@ -17,7 +26,7 @@ class BIDSDatasetDescription: """ name: str - bids_version: str = BIDS_VERSION + bids_version: Version = BIDS_VERSION dataset_type: DatasetType = DatasetType.RAW def write(self, to: IO[str]): @@ -39,7 +48,7 @@ def _rename(name: str) -> str: # Register a JSON converter for the BIDS dataset description model. converter = make_converter() - +converter.register_unstructure_hook(Version, lambda dt: str(dt)) converter.register_unstructure_hook( BIDSDatasetDescription, make_dict_unstructure_fn( @@ -51,3 +60,40 @@ def _rename(name: str) -> str: }, ), ) + + +def get_bids_version(dataset_folder: Path) -> Version: + """Returns the BIDS version number of a BIDS or CAPS dataset.""" + try: + with open(dataset_folder / "dataset_description.json", "r") as fp: + bids_metadata = json.load(fp) + return Version(bids_metadata["BIDSVersion"]) + except InvalidVersion as e: + log_and_raise( + ( + f"File {dataset_folder / 'dataset_description.json'} has a " + f"BIDS version number not properly formatted:\n{e}" + ), + ClinicaBIDSError, + ) + except FileNotFoundError: + log_and_raise( + ( + f"File {dataset_folder / 'dataset_description.json'} is missing " + "while it is mandatory for a BIDS/CAPS dataset." + ), + ClinicaBIDSError, + ) + except KeyError: + log_and_raise( + ( + f"File {dataset_folder / 'dataset_description.json'} is missing a " + "'BIDSVersion' key while it is mandatory." + ), + ClinicaBIDSError, + ) + except json.JSONDecodeError as e: + log_and_raise( + f"File {dataset_folder / 'dataset_description.json'} is not formatted correctly:\n{e}.", + ClinicaBIDSError, + ) diff --git a/clinica/pipelines/anatomical/freesurfer/t1/cli.py b/clinica/pipelines/anatomical/freesurfer/t1/cli.py index c9a530ebf..37b22c5e9 100644 --- a/clinica/pipelines/anatomical/freesurfer/t1/cli.py +++ b/clinica/pipelines/anatomical/freesurfer/t1/cli.py @@ -34,6 +34,7 @@ @option.global_option_group @option.n_procs @click.pass_context +@cli_param.option.caps_name def cli( ctx: click.Context, bids_directory: str, @@ -45,6 +46,7 @@ def cli( overwrite_outputs: bool = False, yes: bool = False, atlas_path: Optional[str] = None, + caps_name: Optional[str] = None, ) -> None: """Cross-sectional pre-processing of T1w images with FreeSurfer. @@ -68,6 +70,7 @@ def cli( }, name=pipeline_name, overwrite_caps=overwrite_outputs, + caps_name=caps_name, ) exec_pipeline = ( pipeline.run(plugin="MultiProc", plugin_args={"n_procs": n_procs}) diff --git a/clinica/pipelines/cli_param/option.py b/clinica/pipelines/cli_param/option.py index e7a34804b..e0b08d1c3 100644 --- a/clinica/pipelines/cli_param/option.py +++ b/clinica/pipelines/cli_param/option.py @@ -16,6 +16,18 @@ ), ) +caps_name = option( + "-cn", + "--caps-name", + type=str, + help=( + "The name of the CAPS dataset that will be created by the pipeline. " + "This is not the name of the folder itself, but the name in the metadata, " + "which can be different if desired. If the CAPS folder already exists and " + "already has a name, this will have no effect and the existing name will be kept." + ), +) + dartel_tissues = option( "-dt", "--dartel_tissues", diff --git a/clinica/pipelines/dwi/preprocessing/fmap/cli.py b/clinica/pipelines/dwi/preprocessing/fmap/cli.py index 80bc65ad0..32e8cd19d 100644 --- a/clinica/pipelines/dwi/preprocessing/fmap/cli.py +++ b/clinica/pipelines/dwi/preprocessing/fmap/cli.py @@ -23,6 +23,7 @@ @cli_param.option_group.advanced_pipeline_options @cli_param.option.use_cuda @cli_param.option.initrand +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -32,6 +33,7 @@ def cli( n_procs: Optional[int] = None, use_cuda: bool = False, initrand: bool = False, + caps_name: Optional[str] = None, ) -> None: """Preprocessing of raw DWI datasets using a phase difference image. @@ -56,6 +58,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/dwi/preprocessing/t1/cli.py b/clinica/pipelines/dwi/preprocessing/t1/cli.py index 22c652e25..f59aa3e32 100644 --- a/clinica/pipelines/dwi/preprocessing/t1/cli.py +++ b/clinica/pipelines/dwi/preprocessing/t1/cli.py @@ -25,6 +25,7 @@ @cli_param.option.initrand @cli_param.option.delete_cache @cli_param.option.random_seed +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -36,6 +37,7 @@ def cli( initrand: bool = False, delete_cache: bool = False, random_seed: Optional[int] = None, + caps_name: Optional[str] = None, ) -> None: """Preprocessing of raw DWI datasets using a T1w image. @@ -63,6 +65,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/engine.py b/clinica/pipelines/engine.py index 01858088d..e90d918a2 100644 --- a/clinica/pipelines/engine.py +++ b/clinica/pipelines/engine.py @@ -384,6 +384,7 @@ def __init__( parameters: Optional[dict] = None, name: Optional[str] = None, ignore_dependencies: Optional[List[str]] = None, + caps_name: Optional[str] = None, ): """Init a Pipeline object. @@ -423,6 +424,11 @@ def __init__( from pathlib import Path from tempfile import mkdtemp + from clinica.utils.caps import ( + build_caps_dataset_description, + write_caps_dataset_description, + ) + from clinica.utils.exceptions import ClinicaCAPSError from clinica.utils.inputs import check_bids_folder, check_caps_folder self._is_built: bool = False @@ -454,6 +460,7 @@ def __init__( self._name = name or self.__class__.__name__ self._parameters = parameters or {} self._ignore_dependencies = ignore_dependencies or [] + self.caps_name = caps_name if not self._bids_directory: if not self._caps_directory: @@ -461,11 +468,37 @@ def __init__( f"The {self._name} pipeline does not contain " "BIDS nor CAPS directory at the initialization." ) - check_caps_folder(self._caps_directory) + try: + check_caps_folder(self._caps_directory) + except ClinicaCAPSError as e: + desc = build_caps_dataset_description( + input_dir=self._caps_directory, + output_dir=self._caps_directory, + processing_name=self._name, + dataset_name=self.caps_name, + ) + raise ClinicaCAPSError( + f"{e}\nYou might want to create a 'dataset_description.json' " + f"file with the following content:\n{desc}" + ) self.is_bids_dir = False else: check_bids_folder(self._bids_directory) self.is_bids_dir = True + if self._caps_directory is not None: + if ( + not self._caps_directory.exists() + or len([f for f in self._caps_directory.iterdir()]) == 0 + ): + self._caps_directory.mkdir(parents=True, exist_ok=True) + if self._caps_directory: + write_caps_dataset_description( + input_dir=self.input_dir, + output_dir=self._caps_directory, + processing_name=self._name, + dataset_name=self.caps_name, + ) + check_caps_folder(self._caps_directory) self._compute_subjects_and_sessions() self._init_nodes() diff --git a/clinica/pipelines/t1_linear/anat_linear_pipeline.py b/clinica/pipelines/t1_linear/anat_linear_pipeline.py index 49b4510c6..356ba8a92 100644 --- a/clinica/pipelines/t1_linear/anat_linear_pipeline.py +++ b/clinica/pipelines/t1_linear/anat_linear_pipeline.py @@ -35,6 +35,7 @@ def __init__( name: Optional[str] = None, ignore_dependencies: Optional[List[str]] = None, use_antspy: bool = False, + caps_name: Optional[str] = None, ): from clinica.utils.stream import cprint @@ -47,6 +48,7 @@ def __init__( parameters=parameters, ignore_dependencies=ignore_dependencies, name=name, + caps_name=caps_name, ) self.use_antspy = use_antspy if self.use_antspy: diff --git a/clinica/pipelines/t1_linear/flair_linear_cli.py b/clinica/pipelines/t1_linear/flair_linear_cli.py index 24a1ded25..621fccb08 100644 --- a/clinica/pipelines/t1_linear/flair_linear_cli.py +++ b/clinica/pipelines/t1_linear/flair_linear_cli.py @@ -31,6 +31,7 @@ is_flag=True, help="Use ANTsPy instead of ANTs.", ) +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -40,6 +41,7 @@ def cli( working_directory: Optional[str] = None, n_procs: Optional[int] = None, use_antspy: bool = False, + caps_name: Optional[str] = None, ) -> None: """Affine registration of Flair images to the MNI standard space. @@ -66,6 +68,7 @@ def cli( parameters=parameters, name=pipeline_name, use_antspy=use_antspy, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/t1_linear/t1_linear_cli.py b/clinica/pipelines/t1_linear/t1_linear_cli.py index 5eb94e15c..422ee3c57 100644 --- a/clinica/pipelines/t1_linear/t1_linear_cli.py +++ b/clinica/pipelines/t1_linear/t1_linear_cli.py @@ -31,6 +31,7 @@ is_flag=True, help="Use ANTsPy instead of ANTs.", ) +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -40,6 +41,7 @@ def cli( working_directory: Optional[str] = None, n_procs: Optional[int] = None, use_antspy: bool = False, + caps_name: Optional[str] = None, ) -> None: """Affine registration of T1w images to the MNI standard space. @@ -66,6 +68,7 @@ def cli( parameters=parameters, name=pipeline_name, use_antspy=use_antspy, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/t1_volume/t1_volume_cli.py b/clinica/pipelines/t1_volume/t1_volume_cli.py index 50664699d..5956e6a7b 100644 --- a/clinica/pipelines/t1_volume/t1_volume_cli.py +++ b/clinica/pipelines/t1_volume/t1_volume_cli.py @@ -32,6 +32,7 @@ @option.global_option_group @option.n_procs @click.pass_context +@cli_param.option.caps_name def cli( ctx: click.Context, bids_directory: str, @@ -50,6 +51,7 @@ def cli( working_directory: Optional[str] = None, n_procs: Optional[int] = None, yes: bool = False, + caps_name: Optional[str] = None, ) -> None: """Volume-based processing of T1-weighted MR images. @@ -103,6 +105,7 @@ def cli( working_directory=working_directory, n_procs=n_procs, yes=yes, + caps_name=caps_name, ) cprint("Part 2/4: Running t1-volume-create-dartel pipeline.") @@ -115,6 +118,7 @@ def cli( subjects_sessions_tsv=subjects_sessions_tsv, working_directory=working_directory, n_procs=n_procs, + caps_name=caps_name, ) cprint("Part 3/4: Running t1-volume-dartel2mni pipeline.") @@ -130,6 +134,7 @@ def cli( subjects_sessions_tsv=subjects_sessions_tsv, working_directory=working_directory, n_procs=n_procs, + caps_name=caps_name, ) cprint("Part 4/4: Running t1-volume-parcellation pipeline.") @@ -140,6 +145,7 @@ def cli( subjects_sessions_tsv=subjects_sessions_tsv, working_directory=working_directory, n_procs=n_procs, + caps_name=caps_name, ) diff --git a/clinica/pipelines/t1_volume_create_dartel/t1_volume_create_dartel_cli.py b/clinica/pipelines/t1_volume_create_dartel/t1_volume_create_dartel_cli.py index c8f888206..0635461b6 100644 --- a/clinica/pipelines/t1_volume_create_dartel/t1_volume_create_dartel_cli.py +++ b/clinica/pipelines/t1_volume_create_dartel/t1_volume_create_dartel_cli.py @@ -21,6 +21,7 @@ @option.n_procs @cli_param.option_group.advanced_pipeline_options @cli_param.option.dartel_tissues +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -29,6 +30,7 @@ def cli( subjects_sessions_tsv: Optional[str] = None, working_directory: Optional[str] = None, n_procs: Optional[int] = None, + caps_name: Optional[str] = None, ) -> None: """Inter-subject registration using Dartel (creating a new Dartel template). @@ -51,6 +53,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/t1_volume_dartel2mni/t1_volume_dartel2mni_cli.py b/clinica/pipelines/t1_volume_dartel2mni/t1_volume_dartel2mni_cli.py index 2dba89791..59b637551 100644 --- a/clinica/pipelines/t1_volume_dartel2mni/t1_volume_dartel2mni_cli.py +++ b/clinica/pipelines/t1_volume_dartel2mni/t1_volume_dartel2mni_cli.py @@ -25,6 +25,7 @@ @cli_param.option.tissues @cli_param.option.modulate @cli_param.option.voxel_size +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -36,6 +37,7 @@ def cli( subjects_sessions_tsv: Optional[str] = None, working_directory: Optional[str] = None, n_procs: Optional[int] = None, + caps_name: Optional[str] = None, ) -> None: """Register DARTEL template to MNI space. @@ -64,6 +66,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/t1_volume_parcellation/t1_volume_parcellation_cli.py b/clinica/pipelines/t1_volume_parcellation/t1_volume_parcellation_cli.py index dde925198..2ae362484 100644 --- a/clinica/pipelines/t1_volume_parcellation/t1_volume_parcellation_cli.py +++ b/clinica/pipelines/t1_volume_parcellation/t1_volume_parcellation_cli.py @@ -20,6 +20,7 @@ @option.n_procs @cli_param.option_group.advanced_pipeline_options @cli_param.option.modulate +@cli_param.option.caps_name def cli( caps_directory: str, group_label: str, @@ -27,6 +28,7 @@ def cli( working_directory: Optional[str] = None, n_procs: Optional[int] = None, modulate: bool = True, + caps_name: Optional[str] = None, ) -> None: """Computation of mean GM concentration for a set of regions. @@ -51,6 +53,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/t1_volume_register_dartel/t1_volume_register_dartel_cli.py b/clinica/pipelines/t1_volume_register_dartel/t1_volume_register_dartel_cli.py index 1d7f3e77a..c0f5fffea 100644 --- a/clinica/pipelines/t1_volume_register_dartel/t1_volume_register_dartel_cli.py +++ b/clinica/pipelines/t1_volume_register_dartel/t1_volume_register_dartel_cli.py @@ -21,6 +21,7 @@ @option.n_procs @cli_param.option_group.advanced_pipeline_options @cli_param.option.tissues +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -29,6 +30,7 @@ def cli( subjects_sessions_tsv: Optional[str] = None, working_directory: Optional[str] = None, n_procs: Optional[int] = None, + caps_name: Optional[str] = None, ) -> None: """Inter-subject registration using Dartel (using an existing Dartel template). @@ -51,6 +53,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/pipelines/t1_volume_tissue_segmentation/t1_volume_tissue_segmentation_cli.py b/clinica/pipelines/t1_volume_tissue_segmentation/t1_volume_tissue_segmentation_cli.py index 3c3eb3162..7fba06b85 100644 --- a/clinica/pipelines/t1_volume_tissue_segmentation/t1_volume_tissue_segmentation_cli.py +++ b/clinica/pipelines/t1_volume_tissue_segmentation/t1_volume_tissue_segmentation_cli.py @@ -25,6 +25,7 @@ @cli_param.option.tissue_probability_maps @cli_param.option.dont_save_warped_unmodulated @cli_param.option.save_warped_modulated +@cli_param.option.caps_name def cli( bids_directory: str, caps_directory: str, @@ -37,6 +38,7 @@ def cli( working_directory: Optional[str] = None, n_procs: Optional[int] = None, yes: bool = False, + caps_name: Optional[str] = None, ) -> None: """Tissue segmentation, bias correction and spatial normalization to MNI space of T1w images with SPM. @@ -64,6 +66,7 @@ def cli( base_dir=working_directory, parameters=parameters, name=pipeline_name, + caps_name=caps_name, ) exec_pipeline = ( diff --git a/clinica/utils/bids.py b/clinica/utils/bids.py index b24fcec78..6f6107c42 100644 --- a/clinica/utils/bids.py +++ b/clinica/utils/bids.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import Dict, Tuple, Union +from packaging.version import Version + __all__ = [ "BIDSLabel", "BIDSFileName", @@ -12,7 +14,7 @@ "Suffix", ] -BIDS_VERSION = "1.7.0" +BIDS_VERSION = Version("1.7.0") class Extension(str, Enum): diff --git a/clinica/utils/caps.py b/clinica/utils/caps.py new file mode 100644 index 000000000..8b34b0387 --- /dev/null +++ b/clinica/utils/caps.py @@ -0,0 +1,556 @@ +import json +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import IO, List, MutableSequence, NewType, Optional, Union + +from attrs import define, fields +from cattr.gen import make_dict_structure_fn, make_dict_unstructure_fn, override +from cattr.preconf.json import make_converter +from packaging.version import Version + +from clinica.iotools.bids_dataset_description import get_bids_version +from clinica.utils.bids import BIDS_VERSION +from clinica.utils.exceptions import ClinicaBIDSError, ClinicaCAPSError +from clinica.utils.inputs import DatasetType +from clinica.utils.stream import cprint, log_and_raise, log_and_warn + +__all__ = [ + "CAPS_VERSION", + "CAPSDatasetDescription", + "VersionComparisonPolicy", + "are_versions_compatible", + "write_caps_dataset_description", + "build_caps_dataset_description", +] + + +CAPS_VERSION = Version("1.0.0") + +IsoDate = NewType("IsoDate", datetime) + + +class VersionComparisonPolicy(str, Enum): + """Defines the different ways we can compare version numbers in Clinica. + + STRICT: version numbers have to match exactly. + MINOR : version numbers have to have the same major and minor numbers. + MAJOR: version numbers only need to share the same major number. + """ + + STRICT = "strict" + MINOR = "minor" + MAJOR = "major" + + +def are_versions_compatible( + version1: Union[str, Version], + version2: Union[str, Version], + policy: Optional[Union[str, VersionComparisonPolicy]] = None, +) -> bool: + """Returns whether the two provided versions are compatible or not depending on the policy. + + Parameters + ---------- + version1 : str or Version + The first version number to compare. + + version2 : str or Version + The second version number to compare. + + policy : str or VersionComparisonPolicy, optional + The policy under which to compare version1 with version2. + By default, a strict policy is used, meaning that version + numbers have to match exactly. + + Returns + ------- + bool : + True if version1 is 'compatible' with version2, False otherwise. + """ + if isinstance(version1, str): + version1 = Version(version1) + if isinstance(version2, str): + version2 = Version(version2) + if policy is None: + policy = VersionComparisonPolicy.STRICT + else: + policy = VersionComparisonPolicy(policy) + if policy == VersionComparisonPolicy.STRICT: + return version1 == version2 + if policy == VersionComparisonPolicy.MINOR: + return version1.major == version2.major and version1.minor == version2.minor + if policy == VersionComparisonPolicy.MAJOR: + return version1.major == version2.major + + +@define +class CAPSProcessingDescription: + """This class models a CAPS processing pipeline metadata. + + Attributes + ---------- + name : str + The name of the processing pipeline. + Example: 't1-linear'. + + date : IsoDate + The date at which the processing pipeline has been run. + More precisely, this is the date at which the dataset_description.json + file is written to disk, which precedes the date at which the pipeline + finishes processing. + + author : str + This is the name of the user who ran this processing pipeline. + + machine : str + This is the name of the machine of which the processing pipeline was run. + + input_path : str + This is the path to the input dataset. + """ + + name: str + date: IsoDate + author: str + machine: str + input_path: str + + @classmethod + def from_values(cls, name: str, input_path: str): + return cls( + name, + _get_current_timestamp(), + _get_username(), + _get_machine_name(), + input_path, + ) + + @classmethod + def from_dict(cls, values: dict): + return cls( + values["Name"], + values["Date"], + values["Author"], + values["Machine"], + values["InputPath"], + ) + + def match( + self, + processing_name: Optional[str] = None, + processing_input_path: Optional[str] = None, + ) -> bool: + return not ( + (processing_name is not None and self.name != processing_name) + or ( + processing_input_path is not None + and (self.input_path != processing_input_path) + ) + ) + + def write(self, to: IO[str]): + json.dump(converter.unstructure(self), to, indent=4) + + def __str__(self): + return json.dumps(converter.unstructure(self)) + + @classmethod + def from_file(cls, json_file: Path): + with open(json_file, "r") as fp: + content = json.load(fp) + return converter.structure(content, CAPSProcessingDescription) + + +@define +class CAPSDatasetDescription: + """Model representing a CAPS dataset description. + + Attributes + ---------- + name : str + The name of the CAPS dataset. + + bids_version : str + The version number of the BIDS specifications used. + + caps_version : str + The version number of the CAPS specifications used. + + dataset_type : DatasetType + The dataset type. + + processing : List of CAPSProcessingDescription + The list of processing pipelines that have been run. + """ + + name: str + bids_version: Version = BIDS_VERSION + caps_version: Version = CAPS_VERSION + dataset_type: DatasetType = DatasetType.DERIVATIVE + processing: MutableSequence[CAPSProcessingDescription] = [] + + def write(self, to: IO[str]): + json.dump(converter.unstructure(self), to, indent=4) + + def __str__(self): + return json.dumps(converter.unstructure(self)) + + def has_processing( + self, + processing_name: Optional[str] = None, + processing_input_path: Optional[str] = None, + ) -> bool: + return any( + [ + processing.match( + processing_name=processing_name, + processing_input_path=processing_input_path, + ) + for processing in self.processing + ] + ) + + def get_processing( + self, + processing_name: Optional[str] = None, + processing_input_path: Optional[str] = None, + ) -> List[CAPSProcessingDescription]: + return [ + processing + for processing in self.processing + if processing.match( + processing_name=processing_name, + processing_input_path=processing_input_path, + ) + ] + + def delete_processing( + self, + processing_name: Optional[str] = None, + processing_input_path: Optional[str] = None, + ): + for processing in self.processing: + if processing.match( + processing_name=processing_name, + processing_input_path=processing_input_path, + ): + self.processing.remove(processing) + + def add_processing( + self, + processing_name: str, + processing_input_path: str, + ): + new_processing = CAPSProcessingDescription.from_values( + processing_name, processing_input_path + ) + existing_processings = self.get_processing( + processing_name, processing_input_path + ) + if existing_processings: + existing_processing = existing_processings[0] + log_and_warn( + ( + f"The CAPS dataset '{self.name}' already has a processing named {processing_name} " + f"with an input folder set to {processing_input_path}:\n" + f"{existing_processing}\nIt will be overwritten with the following:\n{new_processing}" + ), + UserWarning, + ) + self.delete_processing(existing_processing.name) + self.processing.append(new_processing) + + @classmethod + def from_values( + cls, + name: Optional[str] = None, + bids_version: Optional[Version] = None, + caps_version: Optional[Version] = None, + processing: Optional[List[CAPSProcessingDescription]] = None, + ): + return cls( + name or _generate_random_name(), + bids_version or BIDS_VERSION, + caps_version or CAPS_VERSION, + DatasetType.DERIVATIVE, + processing or [], + ) + + @classmethod + def from_file(cls, json_file: Path): + with open(json_file, "r") as fp: + content = json.load(fp) + return converter.structure(content, CAPSDatasetDescription) + + @classmethod + def from_dict(cls, values: dict): + processing = [] + if "Processing" in values: + processing = [ + CAPSProcessingDescription.from_dict(processing) + for processing in values["Processing"] + ] + return cls( + values["Name"], + values["BIDSVersion"], + values["CAPSVersion"], + DatasetType(values["DatasetType"]), + processing, + ) + + def is_compatible_with( + self, other, policy: Optional[Union[str, VersionComparisonPolicy]] = None + ) -> bool: + return are_versions_compatible( + self.bids_version, other.bids_version, policy=policy + ) and are_versions_compatible( + self.caps_version, other.caps_version, policy=policy + ) + + +def _get_username() -> str: + import os + import pwd + + return pwd.getpwuid(os.getuid()).pw_name + + +def _get_machine_name() -> str: + import platform + + return platform.node() + + +def _get_current_timestamp() -> IsoDate: + return IsoDate(datetime.now()) + + +def _generate_random_name() -> str: + import uuid + + return str(uuid.uuid4()) + + +def _rename(name: str) -> str: + """Rename attributes following the specification for the JSON file. + + Basically pascal case with known acronyms such as CAPS fully capitalized. + """ + return "".join( + word.upper() if word in ("bids", "caps") else word.capitalize() + for word in name.split("_") + ) + + +# Register a JSON converter for the CAPS dataset description model. +converter = make_converter() + +# Unstructuring hooks first +converter.register_unstructure_hook(Version, lambda dt: str(dt)) +converter.register_unstructure_hook(IsoDate, lambda dt: dt.isoformat()) +caps_processing_field_renaming = { + a.name: override(rename=_rename(a.name)) for a in fields(CAPSProcessingDescription) +} +caps_processing_field_renaming_unstructure_hook = make_dict_unstructure_fn( + CAPSProcessingDescription, + converter, + **caps_processing_field_renaming, +) +converter.register_unstructure_hook( + CAPSProcessingDescription, + caps_processing_field_renaming_unstructure_hook, +) +caps_dataset_description_field_renaming = { + a.name: override(rename=_rename(a.name)) for a in fields(CAPSDatasetDescription) +} +caps_dataset_field_renaming_unstructure_hook = make_dict_unstructure_fn( + CAPSDatasetDescription, + converter, + **caps_dataset_description_field_renaming, +) +converter.register_unstructure_hook( + CAPSDatasetDescription, + caps_dataset_field_renaming_unstructure_hook, +) + +# And structuring hooks +converter.register_structure_hook(Version, lambda ts, _: Version(ts)) +converter.register_structure_hook(IsoDate, lambda ts, _: datetime.fromisoformat(ts)) +caps_processing_field_renaming_structure_hook = make_dict_structure_fn( + CAPSProcessingDescription, + converter, + **caps_processing_field_renaming, +) +converter.register_structure_hook( + CAPSProcessingDescription, + caps_processing_field_renaming_structure_hook, +) +caps_dataset_field_renaming_structure_hook = make_dict_structure_fn( + CAPSDatasetDescription, + converter, + **caps_dataset_description_field_renaming, +) +converter.register_structure_hook( + CAPSDatasetDescription, + caps_dataset_field_renaming_structure_hook, +) + + +def write_caps_dataset_description( + input_dir: Path, + output_dir: Path, + processing_name: str, + dataset_name: Optional[str] = None, + bids_version: Optional[str] = None, + caps_version: Optional[str] = None, +) -> None: + """Write `dataset_description.json` at the root of the CAPS directory. + + Parameters + ---------- + input_dir : Path + The path to the folder of the input dataset. + It can be a BIDS dataset or a CAPS dataset. + + output_dir : Path + The path to the folder of the output dataset. + This has to be a CAPS dataset, and this is where + the requested dataset_description.json file will be written. + + processing_name : str + The name of the processing performed. By default, pipelines of + Clinica will set this as the name of the pipeline, but any name + is possible. + + dataset_name : str, optional + The name of the CAPS dataset. If not specified, a random identifier will + be generated. If a dataset_description.json file already exists, the + existing name will be kept. + + bids_version : str, optional + The version of the BIDS specifications used. + By default, this will be set as the BIDS version currently supported by Clinica. + + caps_version : str, optional + The version of the CAPS specifications used. + By default, this will be set as the CAPS version currently supported by Clinica. + """ + description = build_caps_dataset_description( + input_dir, + output_dir, + processing_name, + dataset_name=dataset_name, + bids_version=bids_version, + caps_version=caps_version, + ) + with open(output_dir / "dataset_description.json", "w") as f: + description.write(to=f) + + +def build_caps_dataset_description( + input_dir: Path, + output_dir: Path, + processing_name: str, + dataset_name: Optional[str] = None, + bids_version: Optional[str] = None, + caps_version: Optional[str] = None, +) -> CAPSDatasetDescription: + """Generate the CAPSDatasetDescription for a given CAPS dataset. + + Parameters + ---------- + input_dir : Path + The path to the folder of the input dataset. + It can be a BIDS dataset or a CAPS dataset. + + output_dir : Path + The path to the folder of the output dataset. + This has to be a CAPS dataset, and this is where + the requested dataset_description.json file will be written. + + processing_name : str + The name of the processing performed. By default, pipelines of + Clinica will set this as the name of the pipeline, but any name + is possible. + + dataset_name : str, optional + The name of the CAPS dataset. If not specified, a random identifier will + be generated. If a dataset_description.json file already exists, the + existing name will be kept. + + bids_version : str, optional + The version of the BIDS specifications used. + By default, this will be set as the BIDS version currently supported by Clinica. + + caps_version : str, optional + The version of the CAPS specifications used. + By default, this will be set as the CAPS version currently supported by Clinica. + + Returns + ------- + CAPSDatasetDescription : + The CAPSDatasetDescription generated. + """ + bids_version_from_input_dir = None + try: + bids_version_from_input_dir = get_bids_version(input_dir) + except ClinicaBIDSError: + log_and_warn( + ( + f"Unable to retrieve the BIDS version from the input folder {input_dir}." + f"Please verify your input dataset. Clinica will assume a BIDS version of {BIDS_VERSION}." + ), + UserWarning, + ) + if ( + bids_version is not None + and bids_version_from_input_dir is not None + and bids_version != bids_version_from_input_dir + ): + log_and_raise( + f"The input dataset {input_dir} has BIDS specifications following " + f"version {bids_version_from_input_dir}, while the BIDS specifications version " + f"asked for the CAPS creation is {bids_version}. " + "Please make sure the versions are the same before processing.", + ClinicaBIDSError, + ) + new_description = CAPSDatasetDescription.from_values( + dataset_name, bids_version_from_input_dir, caps_version + ) + if (output_dir / "dataset_description.json").exists(): + cprint( + ( + f"The CAPS dataset '{dataset_name}', located at {output_dir}, already " + "contains a 'dataset_description.json' file." + ), + lvl="info", + ) + previous_description = CAPSDatasetDescription.from_file( + output_dir / "dataset_description.json" + ) + if not previous_description.is_compatible_with( + new_description, VersionComparisonPolicy.STRICT + ): + msg = ( + f"Impossible to write the 'dataset_description.json' file in {output_dir} " + "because it already exists and it contains incompatible metadata." + ) + log_and_raise(msg, ClinicaCAPSError) + if previous_description.name != new_description.name: + log_and_warn( + ( + f"The existing CAPS dataset, located at {output_dir} has a " + f"name '{previous_description.name}' different from the new " + f"name '{new_description.name}'. The old name will be kept." + ), + UserWarning, + ) + new_description.name = previous_description.name + for processing in previous_description.processing: + if not processing.match( + processing_name=processing_name, processing_input_path=str(input_dir) + ): + new_description.processing.append(processing) + new_description.add_processing(processing_name, str(input_dir)) + return new_description diff --git a/clinica/utils/inputs.py b/clinica/utils/inputs.py index dfcd1401b..319690eb5 100644 --- a/clinica/utils/inputs.py +++ b/clinica/utils/inputs.py @@ -252,6 +252,7 @@ def check_caps_folder(caps_directory: Union[str, os.PathLike]) -> None: from clinica.utils.exceptions import ClinicaCAPSError caps_directory = _validate_caps_folder_existence(caps_directory) + _check_dataset_description_exists_in_caps(caps_directory) sub_folders = [f for f in caps_directory.iterdir() if f.name.startswith("sub-")] if len(sub_folders) > 0: diff --git a/clinica/utils/testing_utils.py b/clinica/utils/testing_utils.py index 625e162f3..65322a88c 100644 --- a/clinica/utils/testing_utils.py +++ b/clinica/utils/testing_utils.py @@ -91,6 +91,11 @@ def build_caps_directory(directory: os.PathLike, configuration: dict) -> None: This function is a simple prototype for creating fake datasets for testing. """ directory = Path(directory) + with open(directory / "dataset_description.json", "w") as fp: + json.dump( + {"Name": "Example dataset", "BIDSVersion": "1.0.2", "CAPSVersion": "1.0.0"}, + fp, + ) _build_groups(directory, configuration) _build_subjects(directory, configuration) diff --git a/docs/CAPS/Introduction.md b/docs/CAPS/Introduction.md index 95935d282..f68cd159c 100644 --- a/docs/CAPS/Introduction.md +++ b/docs/CAPS/Introduction.md @@ -2,13 +2,16 @@ # Introduction The outputs of the Clinica pipelines are stored following a specific structure called **CAPS** (ClinicA Processed Structure), which is inspired from the [BIDS](../BIDS.md) structure and the upcoming BIDS Derivatives. -The CAPS specifications are described in detail [here](../Specifications). +The CAPS specifications are described in detail [here](./Specifications.md). +Similar to the BIDS specifications, the CAPS specifications are versioned such that it can evolve in time. +The version of the CAPS specifications is indicated in a `dataset_description.json` file, located at the root of the CAPS folder. +This file is similar to the `dataset_description.json` file of the BIDS specifications and provides other metadata on the dataset. ## Motivation When the development of Clinica started in 2015, the BIDS specifications did not provide specific rules for the processed data. As a result, the goal of CAPS (designed by the [Aramis Lab](http://www.aramislab.fr/)) was to define a hierarchy for the data processed using Clinica. -The idea is to include in a single folder all the results of the different pipelines and organize the data following the main patterns of the BIDS specification. +The idea is to include in a single folder all the results of the different pipelines and organize the data following the main patterns of the BIDS specifications. !!! note It is our objective for the CAPS and BIDS Derivatives specifications to converge. @@ -38,24 +41,22 @@ For instance, if the intra-subject template is computed on `M018` and `M000` ses Several differences exist between the BIDS and CAPS specifications. - Instead of the BIDS `derivatives/` folder, the processed data are stored in the CAPS folder. - - CAPS assumes that the session is always present in a BIDS dataset even though there is a single session. In other words, all datasets are considered longitudinal, even when they have only one session. - -- NIfTI files generated by a Clinica pipeline are always compressed. Compression is recommended by the BIDS specification but not mandatory. +- NIfTI files generated by a Clinica pipeline are always compressed. Compression is recommended by the BIDS specifications but not mandatory. ## Subject and group naming The naming convention for subjects (participants) is identical to the BIDS Specifications 1.0.0. - Groups should be assigned unique labels. Labels can consist of letters and/or numbers. ## Examples ### Subject level example -This CAPS folder contains the outputs of the [`dwi-preprocessing-*` pipeline](../../Pipelines/DWI_Preprocessing) and [`dwi-dti` pipeline](../../Pipelines/DWI_DTI) of a fictional participant `CLNC01` at session `M00`: +This CAPS folder contains the outputs of the [`dwi-preprocessing-*` pipeline](../Pipelines/DWI_Preprocessing.md) and [`dwi-dti` pipeline](../Pipelines/DWI_DTI.md) of a fictional participant `CLNC01` at session `M000`: ```Text +dataset_description.json subjects/ └─ sub-CLNC01/ └─ ses-M000/ @@ -89,10 +90,11 @@ subjects/ ### Group level example -This CAPS folder contains the outputs of a group comparison of patients with Alzheimer’s disease (`AD`) and healthy subjects (`HC`) thanks to the [`statistics-surface` pipeline](../../Pipelines/Stats_Surface). +This CAPS folder contains the outputs of a group comparison of patients with Alzheimer’s disease (`AD`) and healthy subjects (`HC`) thanks to the [`statistics-surface` pipeline](../Pipelines/Stats_Surface.md). Results are stored under the group ID `ADvsHC`: ```Text +dataset_description.json groups/ └─ group-ADvsHC/ └─ statistics/ @@ -119,14 +121,15 @@ groups/ ### Subject level example with longitudinal analysis -This CAPS folder contains the outputs of longitudinal segmentations performed with FreeSurfer for a fictional participant `CLNC01` at sessions `M000` and `M018`. -First, the [`t1-freesurfer` pipeline](../../Pipelines/T1_FreeSurfer) is run on the two sessions. -Then, the [`t1-freesurfer-longitudinal` pipeline](../../Pipelines/T1_FreeSurfer_Longitudinal) will compute the intra-subject template `sub-CLNC01_long-M000M018` using the `M000` and `M018` sessions. -This template is finally used to longitudinally correct the segmentations, whose results are stored in the `sub-CLNC01_ses-M00.long.sub-CLNC01_long-M000M018` and `sub-CLNC01_ses-M018.long.sub-CLNC01_long-M000M018` folders. +This CAPS folder contains the outputs of longitudinal segmentations performed with [FreeSurfer](../Third-party.md#freesurfer) for a fictional participant `CLNC01` at sessions `M000` and `M018`. +First, the [`t1-freesurfer` pipeline](../Pipelines/T1_FreeSurfer.md) is run on the two sessions. +Then, the [`t1-freesurfer-longitudinal` pipeline](../Pipelines/T1_FreeSurfer_Longitudinal.md) will compute the intra-subject template `sub-CLNC01_long-M000M018` using the `M000` and `M018` sessions. +This template is finally used to longitudinally correct the segmentations, whose results are stored in the `sub-CLNC01_ses-M000.long.sub-CLNC01_long-M000M018` and `sub-CLNC01_ses-M018.long.sub-CLNC01_long-M000M018` folders. -Of note, the `.long.` naming comes from FreeSurfer when running the longitudinal `recon-all` command. +Of note, the `.long.` naming comes from [FreeSurfer](../Third-party.md#freesurfer) when running the longitudinal `recon-all` command. ```Text +dataset_description.json subjects/ ├── sub-CLNC01/ │ ├── long-M000M018/ diff --git a/docs/CAPS/Specifications.md b/docs/CAPS/Specifications.md index 7b1f7faaf..aaee9434c 100644 --- a/docs/CAPS/Specifications.md +++ b/docs/CAPS/Specifications.md @@ -1,7 +1,93 @@ # Detailed file descriptions -In the following, brackets `[`/`]` will denote optional key/value pairs in the filename while accolades `{`/`}` will indicate a list of compulsory values (e.g. `hemi-{left|right}` means that the key `hemi` only accepts `left` or `right` as values). +This document present the specifications of the CAPS format version `1.0.0`. + +## Versions of Clinica, BIDS, and CAPS specifications + +| Clinica version | BIDS version supported | CAPS version supported | +|-----------------| ---------------------- | ---------------------- | +| 0.9.x | 1.7.0 | 1.0.0 | +| 0.8.x | 1.7.0 | no version | + +## The dataset_description.json file + +### Specifications + +This file MUST be present at the root of a CAPS dataset and MUST contain the following minimal information: + +```json +{ + "Name": "name identifier for the dataset", + "BIDSVersion": "1.7.0", + "CAPSVersion": "1.0.0", + "DatasetType": "derivative" +} +``` + +- `Name`: String identifier of the dataset. It can be the name of your study for example. By default Clinica generates a random UUID for this field. When running a pipeline which will create a new CAPS dataset, you can use the `--caps-name` option to provide a name. If the CAPS dataset already exist, the existing name will be kept. +- `BIDSVersion`: The version number of the BIDS specifications that the BIDS input dataset is using when this CAPS dataset was generated. +- `CAPSVersion`: The version number of the CAPS specifications used for this dataset. +- `DatasetType`: Either "raw" or "derivative". For a CAPS dataset this should always be "derivative" as it contains processed data. + +In addition, the `dataset_description.json` file MAY contain a `Processing` key which is a list of objects describing the different processing pipelines that were run on this CAPS. +Here is an example for a CAPS dataset containing the outputs of two pipelines: `t1-linear` and `pet-linear`: + +```json +{ + "Name": "e6719ef6-2411-4ad2-8abd-da1fd8fbdf32", + "BIDSVersion": "1.7.0", + "CAPSVersion": "1.0.0", + "DatasetType": "derivative", + "Processing": [ + { + "Name": "t1-linear", + "Date": "2024-08-06T10:28:21.848950", + "Author": "ci", + "Machine": "ubuntu", + "InputPath": "/mnt/data_ci/T1Linear/in/bids" + }, + { + "Name": "pet-linear", + "Date": "2024-08-06T10:36:27.403373", + "Author": "ci", + "Machine": "ubuntu", + "InputPath": "/mnt/data_ci/PETLinear/in/bids" + } + ] +} +``` + +A `Processing` is described with the following fields: + +- `Name`: The name of the processing. For Clinica pipelines, this is the name of the pipeline. +- `Date`: This date is in iso-format and indicates when the processing was run. +- `Author`: This indicates the user name which triggered the processing. +- `Machine`: This indicates the name of the machine on which the processing was run. +- `InputPath`: This is the full path (on the machine on which the processing was run) to the input dataset of the processing. + +### Potential problems + +The `dataset_description.json` file for CAPS datasets was introduced in Clinica `0.9.0`. + +This means that results obtained with prior versions of Clinica do not have this file automatically generated. +Clinica will interpret this as a `<1.0.0` dataset and should error with a suggestion of a minimal `dataset_description.json` that you should add to your dataset. +In this situation, create this new file with the suggested content and re-start the pipeline. + +You might also see the following error message: + +``` +Impossible to write the 'dataset_description.json' file in because it already exists and it contains incompatible metadata. +``` + +This means that you have version mismatch for the BIDS and/or CAPS specifications. +That is, the versions indicated in the input (or output) dataset(s) does not match the versions currently used by Clinica. +If this happens, it is recommended to re-run the conversion or the pipeline which initially generated the dataset with the current version of Clinica. + + +## Subjects and Groups folders + +In the following, brackets `[`/`]` will denote optional key/value pairs in the filename, while accolades `{`/`}` will indicate a list of compulsory values (e.g. `hemi-{left|right}` means that the key `hemi` only accepts `left` or `right` as values). Finally: @@ -15,6 +101,7 @@ Finally: ### `t1-linear` - Affine registration of T1w images to the MNI standard space ```Text +dataset_description.json subjects/ └─ / └─ / @@ -31,6 +118,7 @@ The `desc-Crop` indicates images of size 169×208×179 after cropping to remove #### Segmentation ```Text +dataset_description.json subjects/ └─ / └─ / @@ -47,15 +135,14 @@ subjects/ └─ _segm-_dartelinput.nii.gz ``` -The `modulated-{on|off}` key indicates if modulation has been used in SPM to compensate for the effect of spatial normalization. - -The possible values for the `segm-` key/value are: `graymatter`, `whitematter`, `csf`, `bone`, `softtissue`, and `background`. - -The T1 image in `Ixi549Space` (reference space of the TPM) is obtained by applying the transformation obtained from the SPM Segmentation routine to the T1 image in native space. +- The `modulated-{on|off}` key indicates if modulation has been used in [SPM](../Third-party.md#spm12) to compensate for the effect of spatial normalization. +- The possible values for the `segm-` key/value are: `graymatter`, `whitematter`, `csf`, `bone`, `softtissue`, and `background`. +- The T1 image in `Ixi549Space` (reference space of the TPM) is obtained by applying the transformation obtained from the [SPM](../Third-party.md#spm12) segmentation routine to the T1 image in native space. #### DARTEL ```Text +dataset_description.json groups/ └─ / ├─ _subjects_visits_list.tsv @@ -64,9 +151,8 @@ groups/ └─ _template.nii.gz ``` -The final group template is `_template.nii.gz`. - -The `_iteration-_template.nii.gz` obtained at each iteration will only be used when obtaining flow fields for registering a new image into an existing template (SPM DARTEL Existing templates procedure). +- The final group template is `_template.nii.gz`. +- The `_iteration-_template.nii.gz` obtained at each iteration will only be used when obtaining flow fields for registering a new image into an existing template ([SPM](../Third-party.md#spm12) DARTEL existing templates procedure). !!! Note "Note for SPM experts" The original name of `_iteration-_template.nii.gz` is `Template.nii`. @@ -74,6 +160,7 @@ The `_iteration-_template.nii.gz` obtained at each iteration wi #### DARTEL to MNI ```Text +dataset_description.json subjects/ └─ / └─ / @@ -88,6 +175,7 @@ subjects/ #### Atlas statistics ```Text +dataset_description.json subjects/ └─ / └─ / @@ -103,11 +191,12 @@ Statistics files (with `_statistics.tsv` suffix) are detailed in [appendix](#app ### `t1-freesurfer` - FreeSurfer-based processing of T1-weighted MR images -The outputs of the `t1-freesurfer` pipeline are split into two sub-folders, the first one containing the FreeSurfer outputs and a second with additional outputs specific to Clinica. +The outputs of the `t1-freesurfer` pipeline are split into two sub-folders, the first one containing the [FreeSurfer](../Third-party.md#freesurfer) outputs and a second with additional outputs specific to Clinica. FreeSurfer outputs: ```Text +dataset_description.json subjects/ └─ / └─ / @@ -124,6 +213,7 @@ subjects/ Clinica additional outputs: ```Text +dataset_description.json subjects/ └─ / └─ / @@ -144,10 +234,9 @@ For the file: `*_hemi-{left|right}_parcellation-_thickness.tsv`, ` | Cortical surface area | `_area` | Cortical surface area| | Cortical mean curvature | `_meancurv` | Mean curvature of cortical surface| -The `hemi-{left|right}` key/value stands for `left` or `right` hemisphere. - -The possible values for the `parcellation-` key/value are: `desikan` (Desikan-Killiany Atlas), `destrieux` (Destrieux Atlas) and `ba` (Brodmann Area Maps). -The TSV files for Brodmann areas contain a selection of regions, see this link for the content of this selection: [http://ftp.nmr.mgh.harvard.edu/fswiki/BrodmannAreaMaps](http://ftp.nmr.mgh.harvard.edu/fswiki/BrodmannAreaMaps)). +- The `hemi-{left|right}` key/value stands for `left` or `right` hemisphere. +- The possible values for the `parcellation-` key/value are: `desikan` (Desikan-Killiany Atlas), `destrieux` (Destrieux Atlas) and `ba` (Brodmann Area Maps). +- The [TSV](../glossary.md#tsv) files for Brodmann areas contain a selection of regions, see this link for the content of this selection: [http://ftp.nmr.mgh.harvard.edu/fswiki/BrodmannAreaMaps](http://ftp.nmr.mgh.harvard.edu/fswiki/BrodmannAreaMaps)). The details of the white matter parcellation of FreeSurfer can be found here: [https://surfer.nmr.mgh.harvard.edu/pub/articles/salat_2008.pdf](https://surfer.nmr.mgh.harvard.edu/pub/articles/salat_2008.pdf). @@ -181,6 +270,7 @@ The details of the white matter parcellation of FreeSurfer can be found here: [h #### FreeSurfer unbiased templates ```Text +dataset_description.json subjects/ └─ / └─ / @@ -195,9 +285,10 @@ subjects/ ### FreeSurfer longitudinal outputs -The outputs are split into two sub-folders, the first containing the FreeSurfer longitudinal outputs and a second with additional outputs specific to Clinica. +The outputs are split into two sub-folders, the first containing the [FreeSurfer](../Third-party.md#freesurfer) longitudinal outputs and a second with additional outputs specific to Clinica. ```Text +dataset_description.json subjects/ └─ / └─ / @@ -215,6 +306,7 @@ subjects/ Clinica additional outputs: ```Text +dataset_description.json subjects/ └─ / └─ / @@ -237,6 +329,7 @@ where each file is explained in the `t1-freesurfer` subsection. ### `dwi-preprocessing-*` - Preprocessing of raw diffusion weighted imaging (DWI) datasets ```Text +dataset_description.json subjects/ └─ / └─ / @@ -260,6 +353,7 @@ A brain mask of the preprocessed file is provided. ### `dwi-dti` - DTI-based processing of corrected DWI datasets ```Text +dataset_description.json subjects/ └─ / └─ / @@ -287,7 +381,7 @@ The different maps based on the DTI are: - `RD`: radial diffusivity. - `DECFA`: directionally-encoded colour (DEC) FA. -Current atlases used for statistics are the 1mm version of `JHUDTI81`, `JHUTract0` and `JHUTract25` (see [Atlases page](../../Atlases) for further details). +Current atlases used for statistics are the 1mm version of `JHUDTI81`, `JHUTract0` and `JHUTract25` (see [Atlases page](../Atlases.md) for further details). Statistics files (with `_statistics.tsv` suffix) are detailed in [appendix](#appendix-content-of-a-statistic-file). @@ -297,6 +391,7 @@ Statistics files (with `_statistics.tsv` suffix) are detailed in [appendix](#app ### `dwi-connectome` - Computation of structural connectome from corrected DWI datasets ```Text +dataset_description.json subjects/ └─ / └─ / @@ -307,17 +402,18 @@ subjects/ └─ _space-{b0|T1w}_model-CSD_parcellation-{desikan|destrieux}_connectivity.tsv ``` -The constrained spherical deconvolution (CSD) diffusion model is saved under the `*_model-CSD_diffmodel.nii.gz` filename. -The whole-brain tractography is saved under the `*_tractography.tck` filename. -The connectivity matrices are saved under `*_connectivity.tsv` filenames. +- The constrained spherical deconvolution (CSD) diffusion model is saved under the `*_model-CSD_diffmodel.nii.gz` filename. +- The whole-brain tractography is saved under the `*_tractography.tck` filename. +- The connectivity matrices are saved under `*_connectivity.tsv` filenames. -Current parcellations used for the computation of connectivity matrices are `desikan` and `desikan` (see [Atlases page](../../Atlases) for further details). +Current parcellations used for the computation of connectivity matrices are `desikan` and `desikan` (see [Atlases page](../Atlases.md) for further details). ## PET imaging data ### `pet-volume` - Volume-based processing of PET images ```Text +dataset_description.json subjects/ └─ / └─ / @@ -333,6 +429,7 @@ subjects/ ``` ```Text +dataset_description.json subjects/ └─ / └─ / @@ -342,17 +439,16 @@ subjects/ └─ _space-[_pvc-rbv]_suvr-_statistics.tsv ``` -The `_trc-