Skip to content

Commit

Permalink
Entities implementation (#14)
Browse files Browse the repository at this point in the history
* first ideas for entities

* first ideas for entities

* first ideas for entities

* clean code

* clean code
  • Loading branch information
camillebrianceau authored Nov 21, 2024
1 parent fcbf375 commit 8dd6315
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
Empty file.
215 changes: 215 additions & 0 deletions src/clinicaio/models/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@


from collections import UserString
from typing import Union, Optional
from pathlib import Path
from .enum import SUVRReferenceRegions, Tracer, AnatMRISuffix, PETSuffix, FMapSuffix, Modality, Extension
from enum import Enum
from dataclasses import dataclass
# questions: dependance à Pydantic ??


class Label(UserString):
def __init__(self, value: Union[str, Enum]):
super().__init__(self.validate(value))

@classmethod
def validate(cls, value: Union[str, Enum]) -> str:
if isinstance(value, Enum):
return value.value
elif isinstance(value, str) and value.isalnum():
return value
raise ValueError(
f"Label '{value}' is not a valid BIDS label: it must be string composed only by letters and/or numbers."
)

class Index(UserString):

def __init__(self, value: int, length_as_string: int = 1):
super().__init__(self.validate(value, length_as_string))

@classmethod
def validate(cls, value: int, length_as_string: int) -> str:
if not isinstance(value, int):
try:
value = int(value)
except TypeError as exc:
raise ValueError(
f"Index '{value}' is not a valid BIDS index: it must be a non-negative integer."
) from exc

return str(value).zfill(length_as_string)



class Entity:
key: Label
value : Union[Label, Index]

def __str__(self) -> str:
return f"{self.key}-{self.value}"


# BIDS Entities

class SubjectEntity(Entity):
"""
A person or animal participating in the study.
"""
key = Label("sub")

def __init__(self, value: str):
self.value = Label(value)


class SessionEntity(Entity):
"""
A logical grouping of neuroimaging and behavioral data consistent across subjects.
Session can (but doesn't have to) be synonymous to a visit in a longitudinal study.
In general, subjects will stay in the scanner during one session.
However, for example, if a subject has to leave the scanner room and then be re-positioned
on the scanner bed, the set of MRI acquisitions will still be considered as a session
and match sessions acquired in other subjects. Similarly, in situations where different
data types are obtained over several visits (for example fMRI on one day followed by DWI
the day after) those can be grouped in one session.
Defining multiple sessions is appropriate when several identical or similar data
acquisitions are planned and performed on all -or most- subjects, often in the
case of some intervention between sessions (for example, training).
"""
key = Label("ses")

def __init__(self, value: str):
self.value = Label(value)


class AcquisitionEntity(Entity):
"""
The acq-<label> entity corresponds to a custom label the user MAY use to distinguish a different
set of parameters used for acquiring the same modality.
For example, this should be used when a study includes two T1w images - one full brain low
resolution and one restricted field of view but high resolution. In such case two files
could have the following names: sub-01_acq-highres_T1w.nii.gz and sub-01_acq-lowres_T1w.nii.gz;
however, the user is free to choose any other label than highres and lowres as long as they are
consistent across subjects and sessions.
In case different sequences are used to record the same modality (for example, RARE and FLASH for T1w)
this field can also be used to make that distinction. The level of detail at which the distinction
is made (for example, just between RARE and FLASH, or between RARE, FLASH, and FLASHsubsampled) remains
at the discretion of the researcher.
"""
key = Label("acq")
def __init__(self, value: str):
self.value = Label(value)

class SpaceEntity(Entity):
"""
The space-<label> entity can be used to indicate the way in which electrode positions are interpreted
(for EEG/MEG/iEEG data) or the spatial reference to which a file has been aligned (for MRI data).
The <label> MUST be taken from one of the modality specific lists in the Coordinate Systems Appendix.
For example, for iEEG data, the restricted keywords listed under iEEG Specific Coordinate Systems are
acceptable for <label>.
For EEG/MEG/iEEG data, this entity can be applied to raw data, but for other data types, it is restricted to derivative data.
"""

key = Label("space")

def __init__(self, value: str):
self.value = Label(value)

class TracerEntity(Entity):
"""
This entity represents the "TracerName" metadata field.
Therefore, if the trc-<label> entity is present in a filename,
"TracerName" MUST be defined in the associated metadata.
Please note that the <label> does not need to match the actual value of the field.
"""
key = Label("trc")

def __init__(self, value: str):
self.value = Label(Tracer(value))

class DescriptionEntity(Entity):
"""
When necessary to distinguish two files that do not otherwise have a distinguishing entity,
the desc-<label> entity SHOULD be used.
This entity is only applicable to derivative data.
"""
key = Label("desc")

def __init__(self, value: str):
self.value = Label(value)


class ResolutionEntity(Entity):
"""
Resolution of regularly sampled N-dimensional data.
This entity represents the "Resolution" metadata field. Therefore, if the res-<label>
entity is present in a filename, "Resolution" MUST also be added in the JSON file,
to provide interpretation.
This entity is only applicable to derivative data.
"""
key = Label("res")

def __init__(self, value: str):
self.value = Label(value)


class RunEntity(Entity):
"""
The run-<index> entity is used to distinguish separate data acquisitions with the same
acquisition parameters and (other) entities.
If several data acquisitions (for example, MRI scans or EEG recordings) with the same acquisition
parameters are acquired in the same session, they MUST be indexed with the run-<index> entity:
_run-1, _run-2, _run-3, and so on (only nonnegative integers are allowed as run indices).
If different entities apply, such as a different session indicated by ses-<label>,
or different acquisition parameters indicated by acq-<label>, then run is not needed to distinguish
the scans and MAY be omitted.
"""

key = Label("run")

def __init__(self, value: int):
self.value = Index(value)


class SuffixEntity(Label):
pass

# CAPS Entities

class SUVREntity(Entity):
key = Label("suvr")
def __init__(self, value: str):
self.value = Label(SUVRReferenceRegions(value))


@dataclass
class BIDSPath:
subject: SubjectEntity
session: SessionEntity
modality: Modality
entities: Optional[list[Entity]]
suffix: Optional[SuffixEntity] # is suffix optional ?
extension: Optional[Extension]

def get_image(self) -> Path:
path_ = Path(str(self.subject)) / str(self.session) / str(self.modality)

filename = str(self.subject) + "_" + str(self.session)

for entity in self.entities:
filename += "_" + str(entity)

filename += f".{(str(self.suffix) if self.suffix else '')}"
filename += f".{(str(self.extension) if self.extension else '')}"

return path_ / filename
44 changes: 44 additions & 0 deletions src/clinicaio/models/enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@


from enum import Enum

class SUVRReferenceRegions(str, Enum):
"""Possible SUVR reference region for pet images in clinicaDL."""

PONS = "pons"
CEREBELLUMPONS = "cerebellumPons"
PONS2 = "pons2"
CEREBELLUMPONS2 = "cerebellumPons2"


class Tracer(str, Enum):
"""Possible tracer for pet images in clinicaDL."""

FFDG = "18FFDG"
FAV45 = "18FAV45"
CPIB = "11CPIB"

class AnatMRISuffix(str, Enum):
FLAIR = "FLAIR"
T1W = "T1w"
T2W = "T2w"


class PETSuffix(str, Enum):
PET = "pet"

class DWISuffix(str, Enum):
DWI = "dwi"

class FMapSuffix(str, Enum):
pass

class Modality(str, Enum):
pass


class Extension(str, Enum):
NIFTI = ".nii"
NIFTI_GZ = ".nii.gz"
DICOM = ".dcm"
PT = ".pt"
Empty file added src/clinicaio/models/request.py
Empty file.

0 comments on commit 8dd6315

Please sign in to comment.