Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Caps generator #9

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 165 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ classifiers = [

[tool.poetry.dependencies]
python = ">=3.10,<3.13"
nibabel = "^5"
scipy = "^1.12"

[tool.poetry.group.dev.dependencies]
pytest = "^8.3.3"
Expand Down
Empty file.
Empty file.
1 change: 1 addition & 0 deletions src/clinicaio/generators/caps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .generator import CAPSGenerator
42 changes: 42 additions & 0 deletions src/clinicaio/generators/caps/filename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from pathlib import Path
from typing import Optional

from clinicaio.models.bids_entities import (
DescriptionEntity,
ResolutionEntity,
SessionEntity,
SpaceEntity,
SubjectEntity,
SUVREntity,
TracerEntity,
)
from clinicaio.models.caps import Description, Extension, Suffix


def _get_caps_filename(
subject: int,
session: int,
suffix: str,
extension: str,
tracer: Optional[str] = None,
space: Optional[str] = None,
crop: bool = False,
resolution: Optional[str] = None,
suvr_ref_region: Optional[str] = None,
) -> Path:
"""Returns a BIDS-compliant filename from entity values, a suffix and a extension."""
entities_list = []
for entity in [ # order matters
SubjectEntity(subject),
SessionEntity(session),
TracerEntity(tracer) if tracer else None,
SpaceEntity(space) if space else None,
DescriptionEntity(Description.CROP) if crop else None,
ResolutionEntity(resolution) if resolution else None,
SUVREntity(suvr_ref_region) if suvr_ref_region else None,
Suffix(suffix).value,
]:
if entity is not None:
entities_list.append(entity)

return Path("_".join(entities_list)).with_suffix(Extension(extension).value)
67 changes: 67 additions & 0 deletions src/clinicaio/generators/caps/flair_linear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from pathlib import Path
from typing import Union

import nibabel as nib
import numpy as np
from scipy.io import savemat

from clinicaio.models.bids_entities import SessionEntity, SubjectEntity
from clinicaio.models.caps import Extension, Resolution, Space, Suffix

from .filename import _get_caps_filename
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably means that this function should be public

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it that uncommon to import private functions that are located in another file, but very closely (i.e. in the same module)?
What I mean is that I split caps/generator into several .py files only not to have one huge script, but I would like CAPSGenerator to be the only public object so I turned all the rest into private objects.



def _build_flair_linear(
root: Union[str, Path], subject: int, session: int, crop: bool = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a private function called by the public CAPSGenerator.build_pipeline() method. You data should be validated already and you can use the rich data type directly here:

Suggested change
root: Union[str, Path], subject: int, session: int, crop: bool = True
root: Path, subject: SubjectEntity, session: SessionEntity, crop: bool = True

The validation would be done in CAPSGenerator.build_pipeline. WDYT ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed, at first those functions were public so the validation was made in there, but if they are private, I do agree that it should be done in CAPSGenerator.

):
"""
Simulates flair-linear by creating fake output files in `root`.
"""
dummy_nifti_img = nib.Nifti1Image(np.ones((1, 1, 1)).astype(np.int8), np.eye(4))
dummy_mat = {
"AffineTransform_double_3_3": np.ones((1, 1)).astype(np.int8),
"fixed": np.ones((1, 1)).astype(np.int8),
}

space = Space.MNI
resolution = Resolution.ONE
directory = (
Path(root)
/ "subjects"
/ SubjectEntity(subject)
/ SessionEntity(session)
/ "flair_linear"
)
directory.mkdir(parents=True, exist_ok=True)

uncropped_file = directory / _get_caps_filename(
subject,
session,
space=space,
resolution=resolution,
suffix=Suffix.FLAIR,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, uncropped_file)

mat_file = directory / _get_caps_filename(
subject,
session,
space=space,
resolution=resolution,
suffix=Suffix.AFFINE,
extension=Extension.MAT,
)
savemat(mat_file, dummy_mat)

if crop:
cropped_file = directory / _get_caps_filename(
subject,
session,
space=space,
crop=True,
resolution=resolution,
suffix=Suffix.FLAIR,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, cropped_file)
113 changes: 113 additions & 0 deletions src/clinicaio/generators/caps/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import shutil
from pathlib import Path
from typing import List, Union

from clinicaio.models.bids_entities import SessionEntity, SubjectEntity
from clinicaio.models.caps import Pipeline

from .flair_linear import _build_flair_linear
from .pet_linear import _build_pet_linear
from .t1_linear import _build_t1_linear


class CAPSGenerator:
"""
To build fake CAPS. A CAPSGenerator simulates preprocessing pipelines
by creating fake output files.

Parameters
----------
directory : Union[str, Path]
the directory of the CAPS.
"""

def __init__(self, directory: Union[str, Path]) -> None:
self.dir = Path(directory)

def build_pipeline(
self,
pipeline: Union[str, Pipeline],
subjects: List[int],
sessions: List[int],
**kwargs,
) -> None:
"""
Simulates a preprocessing pipeline for every subject in `subjects` and every session
in `sessions`.

Parameters
----------
pipeline : Union[str, Pipeline]
the pipeline to simulate. Supported pipelines are `t1-linear`,
`flair-linear` and `pet-linear`.
subjects : List[int]
the list of subject IDs
sessions : List[int]
the list of session IDs.
**kwargs
any argument accepted by the pipeline:
- `t1_linear`: `crop`;
- `flair_linear`: `crop`;
- `pet-linear`: `tracer`, `suvr_ref_region`, `crop` and `save_pet_in_t1w_space`.

Raises
------
ValueError
if `pipeline` is not in `t1-linear`, `flair-linear` and `pet-linear`.
"""
pipeline = Pipeline(pipeline)
if pipeline == Pipeline.T1_LINEAR:
builder = _build_t1_linear
elif pipeline == Pipeline.FLAIR_LINEAR:
builder = _build_flair_linear
elif pipeline == Pipeline.PET_LINEAR:
builder = _build_pet_linear
else:
raise ValueError(f"pipeline {pipeline} is not yet implemented.")

for subject in subjects:
for session in sessions:
builder(self.dir, subject, session, **kwargs)

def remove_pipeline(
self, pipeline: Union[str, Pipeline], subject: int, session: int
) -> None:
"""
Removes a preprocessing pipeline for a specific (subject, session).

Parameters
----------
pipeline : Union[str, Pipeline]
the pipeline to remove. Supported pipelines are `t1-linear`,
`flair-linear` and `pet-linear`.
subject : int
the subject ID.
session : int
the session ID.

Raises
------
ValueError
if `pipeline` is not in `t1-linear`, `flair-linear` and `pet-linear`.
"""
pipeline = Pipeline(pipeline)
if pipeline == Pipeline.T1_LINEAR:
directory = "t1_linear"
elif pipeline == Pipeline.FLAIR_LINEAR:
directory = "flair_linear"
elif pipeline == Pipeline.PET_LINEAR:
directory = "pet_linear"
else:
raise ValueError(f"pipeline {pipeline} is not yet implemented.")

full_dir = (
self.dir
/ "subjects"
/ SubjectEntity(subject)
/ SessionEntity(session)
/ directory
)
try:
shutil.rmtree(full_dir)
except FileNotFoundError: # there is not such subject/session for this pipeline
pass
91 changes: 91 additions & 0 deletions src/clinicaio/generators/caps/pet_linear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from pathlib import Path
from typing import Union

import nibabel as nib
import numpy as np
from scipy.io import savemat

from clinicaio.models.bids_entities import SessionEntity, SubjectEntity
from clinicaio.models.caps import Extension, Resolution, Space, Suffix
from clinicaio.models.pet import SUVRReferenceRegion, Tracer

from .filename import _get_caps_filename


def _build_pet_linear(
root: Union[str, Path],
subject: int,
session: int,
tracer: str = Tracer.FDG,
suvr_ref_region: str = SUVRReferenceRegion.PONS,
crop: bool = True,
save_pet_in_t1w_space: bool = False,
):
"""
Simulates pet-linear by creating fake output files in `root`.
"""
dummy_nifti_img = nib.Nifti1Image(np.ones((1, 1, 1)).astype(np.int8), np.eye(4))
dummy_mat = {
"AffineTransform_double_3_3": np.ones((1, 1)).astype(np.int8),
"fixed": np.ones((1, 1)).astype(np.int8),
}

resolution = Resolution.ONE
trc = Tracer(tracer)
suvr = SUVRReferenceRegion(suvr_ref_region)
directory = (
Path(root)
/ "subjects"
/ SubjectEntity(subject)
/ SessionEntity(session)
/ "pet_linear"
)
directory.mkdir(parents=True, exist_ok=True)

uncropped_file = directory / _get_caps_filename(
subject,
session,
tracer=trc,
space=Space.MNI,
crop=False,
resolution=resolution,
suvr_ref_region=suvr,
suffix=Suffix.PET,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, uncropped_file)

mat_file = directory / _get_caps_filename(
subject,
session,
tracer=trc,
space=Space.T1W,
suffix=Suffix.RIGID,
extension=Extension.MAT,
)
savemat(mat_file, dummy_mat)

if crop:
cropped_file = directory / _get_caps_filename(
subject,
session,
tracer=trc,
space=Space.MNI,
crop=True,
resolution=resolution,
suvr_ref_region=suvr,
suffix=Suffix.PET,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, cropped_file)

if save_pet_in_t1w_space:
nifti_file = directory / _get_caps_filename(
subject,
session,
tracer=trc,
space=Space.T1W,
suffix=Suffix.PET,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, nifti_file)
67 changes: 67 additions & 0 deletions src/clinicaio/generators/caps/t1_linear.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from pathlib import Path
from typing import Union

import nibabel as nib
import numpy as np
from scipy.io import savemat

from clinicaio.models.bids_entities import SessionEntity, SubjectEntity
from clinicaio.models.caps import Extension, Resolution, Space, Suffix

from .filename import _get_caps_filename


def _build_t1_linear(
root: Union[str, Path], subject: int, session: int, crop: bool = True
):
"""
Simulates t1-linear by creating fake output files in `root`.
"""
dummy_nifti_img = nib.Nifti1Image(np.ones((1, 1, 1)).astype(np.int8), np.eye(4))
dummy_mat = {
"AffineTransform_double_3_3": np.ones((1, 1)).astype(np.int8),
"fixed": np.ones((1, 1)).astype(np.int8),
}

space = Space.MNI
resolution = Resolution.ONE
directory = (
Path(root)
/ "subjects"
/ SubjectEntity(subject)
/ SessionEntity(session)
/ "t1_linear"
)
directory.mkdir(parents=True, exist_ok=True)

uncropped_file = directory / _get_caps_filename(
subject,
session,
space=space,
resolution=resolution,
suffix=Suffix.T1W,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, uncropped_file)

mat_file = directory / _get_caps_filename(
subject,
session,
space=space,
resolution=resolution,
suffix=Suffix.AFFINE,
extension=Extension.MAT,
)
savemat(mat_file, dummy_mat)

if crop:
cropped_file = directory / _get_caps_filename(
subject,
session,
space=space,
crop=True,
resolution=resolution,
suffix=Suffix.T1W,
extension=Extension.NIIGZ,
)
nib.save(dummy_nifti_img, cropped_file)
Empty file.
Loading
Loading