Core - VoDEx Docs

# vodex.core module

This module contains classes to specify the information about the imaging data, about the experimental conditions and load the data based on the specified information.

The module contains the following classes:

- `FileManager` - Figures out stuff concerning the many files. For example in what order do stacks go? Will grab all the files with the provided file_extension in the provided folder and order them alphabetically.

- `TimeLabel` - Describes a particular time-located event during the experiment. Any specific aspect of the experiment that you want to document : temperature|light|sound|image on the screen|drug|behaviour ... etc.

- `Labels` - Describes a particular group of time labels.

- `Cycle` - Information about the repeated cycle of labels. Use it when you have some periodic conditions, like : light on , light off, light on, light off... will be made of list of labels [light_on, light_off] that repeat ...

- `Timeline` - Information about the sequence of labels. Use it when you have non-periodic conditions.

- `FrameManager` - Deals with frames. Which frames correspond to a volume / cycle/ condition.

- `VolumeManager` - Figures out how to get full volumes for certain time points.

- `Annotation` - Time annotation of the experiment.

- `Experiment` - Information about the experiment. Will use all the information you provided to figure out what frames to give you based on your request.

## `Annotation`

Time annotation of the experiment.

Either frame_to_label_dict or n_frames need to be provided to infer the number of frames. If both are provided , they need to agree.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`labels` | `Labels` | Labels used to build the annotation | required
`info` | `str` | a short description of the annotation | `None`
`frame_to_label` | `List[TimeLabel]` | what label it is for each frame | required
`frame_to_cycle` | `List[int]` | what cycle it is for each frame | `None`
`cycle` | `Cycle` | for annotation from cycles keeps the cycle | `None`
`n_frames` | `int` | total number of frames, will be inferred from frame_to_label if not provided | required

Source code in `src\vodex\core.py`

    726
    727
    728
    729
    730
    731
    732
    733
    734
    735
    736
    737
    738
    739
    740
    741
    742
    743
    744
    745
    746
    747
    748
    749
    750
    751
    752
    753
    754
    755
    756
    757
    758
    759
    760
    761
    762
    763
    764
    765
    766
    767
    768
    769
    770
    771
    772
    773
    774
    775
    776
    777
    778
    779
    780
    781
    782
    783
    784
    785
    786
    787
    788
    789
    790
    791
    792
    793
    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    811
    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    829
    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    843
    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    858
    859
    860
    861
    862
    863
    864
    865
    866
    867
    class Annotation:
        """
        Time annotation of the experiment.

        Either frame_to_label_dict or n_frames need to be provided to infer the number of frames.
        If both are provided , they need to agree.

        Args:
            labels: Labels used to build the annotation
            info: a short description of the annotation
            frame_to_label: what label it is for each frame
            frame_to_cycle: what cycle it is for each frame
            cycle: for annotation from cycles keeps the cycle
            n_frames: total number of frames, will be inferred from frame_to_label if not provided
        """

        def __init__(self, n_frames: int, labels: Labels, frame_to_label: List[TimeLabel], info: str = None,
                     cycle: Cycle = None, frame_to_cycle: List[int] = None):

            # get total experiment length in frames, check that it is consistent
            if frame_to_label is not None:
                assert n_frames == len(frame_to_label), f"The number of frames in the frame_to_label," \
                                                        f"{len(frame_to_label)}," \
                                                        f"and the number of frames provided," \
                                                        f"{n_frames}, do not match."
            self.n_frames = n_frames
            self.frame_to_label = frame_to_label
            self.labels = labels
            self.name = self.labels.group
            self.info = info
            self.cycle = None

            # None if the annotation is not from a cycle
            assert (frame_to_cycle is None) == (cycle is None), "Creating Annotation: " \
                                                                "You have to provide both cycle and frame_to_cycle."
            # if cycle is provided , check the input and add the info
            if cycle is not None and frame_to_cycle is not None:
                # check that frame_to_cycle is int
                assert all(
                    isinstance(n, (int, np.integer)) for n in frame_to_cycle), "frame_to_cycle should be a list of int"
                assert n_frames == len(frame_to_cycle), f"The number of frames in the frame_to_cycle," \
                                                        f"{len(frame_to_cycle)}," \
                                                        f"and the number of frames provided," \
                                                        f"{n_frames}, do not match."
                self.cycle = cycle
                self.frame_to_cycle = frame_to_cycle

        def __eq__(self, other):
            if isinstance(other, Annotation):
                is_same = [
                    self.n_frames == other.n_frames,
                    self.frame_to_label == other.frame_to_label,
                    self.labels == other.labels,
                    self.name == other.name,
                    self.info == other.info
                ]
                # if one of the annotations has a cycle but the other doesn't
                if (self.cycle is None) != (other.cycle is None):
                    return False
                # if both have a cycle, compare cycles as well
                elif self.cycle is not None:
                    is_same.extend([self.cycle == other.cycle,
                                    self.frame_to_cycle == other.frame_to_cycle])
                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {Annotation} and {type(other)}")
                return NotImplemented

        @classmethod
        def from_cycle(cls, n_frames: int, labels: Labels, cycle: Cycle, info: str = None):
            """
            Creates an Annotation object from Cycle.

            Args:
                n_frames: total number of frames, must be provided
                labels: Labels used to build the annotation
                cycle: the cycle to create annotation from
                info: a short description of the annotation
            Returns:
                Annotation object
            """
            frame_to_label = cycle.fit_labels_to_frames(n_frames)
            frame_to_cycle = cycle.fit_cycles_to_frames(n_frames)
            return cls(n_frames, labels, frame_to_label, info=info,
                       cycle=cycle, frame_to_cycle=frame_to_cycle)

        @classmethod
        def from_timeline(cls, n_frames: int, labels: Labels, timeline: Timeline, info: str = None):
            """
            Creates an Annotation object from Timeline.

            Args:
                n_frames: total number of frames, must be provided
                labels: Labels used to build the annotation
                timeline: the timeline to create annotation from
                info: a short description of the annotation
            Returns:
                Annotation object
            """
            assert n_frames == timeline.full_length, "number of frames and total timing should be the same"
            # make a fake cycle the length of the whole recording
            frame_to_label = timeline.per_frame_list
            return cls(n_frames, labels, frame_to_label, info=info)

        def get_timeline(self) -> Timeline:
            """
            Transforms frame_to_label to Timeline

            Returns:
                timeline of the resulting annotation
            """
            duration = []
            labels = []
            for label, group in groupby(self.frame_to_label):
                duration.append(sum(1 for _ in group))
                labels.append(label)
            return Timeline(labels, duration)

        def cycle_info(self) -> str:
            """
            Creates and returns a description of a cycle.

            Returns:
                human-readable information about the cycle.
            """
            if self.cycle is None:
                cycle_info = "Annotation doesn't have a cycle"
            else:
                cycle_info = f"{self.cycle.fit_frames(self.n_frames)} full cycles" \
                             f" [{self.n_frames / self.cycle.full_length} exactly]\n"
                cycle_info = cycle_info + self.cycle.__str__()
            return cycle_info

        def __str__(self):
            description = f"Annotation type: {self.name}\n"
            if self.info is not None:
                description = description + f"{self.info}\n"
            description = description + f"Total frames : {self.n_frames}\n"
            return description

        def __repr__(self):
            return self.__str__()

### `cycle_info()`

Creates and returns a description of a cycle.

Returns:

Type | Description
--- | ---
`str` | human-readable information about the cycle.

Source code in `src\vodex\core.py`

    844
    845
    846
    847
    848
    849
    850
    851
    852
    853
    854
    855
    856
    857
    def cycle_info(self) -> str:
        """
        Creates and returns a description of a cycle.

        Returns:
            human-readable information about the cycle.
        """
        if self.cycle is None:
            cycle_info = "Annotation doesn't have a cycle"
        else:
            cycle_info = f"{self.cycle.fit_frames(self.n_frames)} full cycles" \
                         f" [{self.n_frames / self.cycle.full_length} exactly]\n"
            cycle_info = cycle_info + self.cycle.__str__()
        return cycle_info

### `from_cycle(n_frames, labels, cycle, info=None)` `classmethod`

Creates an Annotation object from Cycle.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`n_frames` | `int` | total number of frames, must be provided | required
`labels` | `Labels` | Labels used to build the annotation | required
`cycle` | `Cycle` | the cycle to create annotation from | required
`info` | `str` | a short description of the annotation | `None`

Returns:

Type | Description
--- | ---
| Annotation object

Source code in `src\vodex\core.py`

    794
    795
    796
    797
    798
    799
    800
    801
    802
    803
    804
    805
    806
    807
    808
    809
    810
    @classmethod
    def from_cycle(cls, n_frames: int, labels: Labels, cycle: Cycle, info: str = None):
        """
        Creates an Annotation object from Cycle.

        Args:
            n_frames: total number of frames, must be provided
            labels: Labels used to build the annotation
            cycle: the cycle to create annotation from
            info: a short description of the annotation
        Returns:
            Annotation object
        """
        frame_to_label = cycle.fit_labels_to_frames(n_frames)
        frame_to_cycle = cycle.fit_cycles_to_frames(n_frames)
        return cls(n_frames, labels, frame_to_label, info=info,
                   cycle=cycle, frame_to_cycle=frame_to_cycle)

### `from_timeline(n_frames, labels, timeline, info=None)` `classmethod`

Creates an Annotation object from Timeline.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`n_frames` | `int` | total number of frames, must be provided | required
`labels` | `Labels` | Labels used to build the annotation | required
`timeline` | `Timeline` | the timeline to create annotation from | required
`info` | `str` | a short description of the annotation | `None`

Returns:

Type | Description
--- | ---
| Annotation object

Source code in `src\vodex\core.py`

    812
    813
    814
    815
    816
    817
    818
    819
    820
    821
    822
    823
    824
    825
    826
    827
    828
    @classmethod
    def from_timeline(cls, n_frames: int, labels: Labels, timeline: Timeline, info: str = None):
        """
        Creates an Annotation object from Timeline.

        Args:
            n_frames: total number of frames, must be provided
            labels: Labels used to build the annotation
            timeline: the timeline to create annotation from
            info: a short description of the annotation
        Returns:
            Annotation object
        """
        assert n_frames == timeline.full_length, "number of frames and total timing should be the same"
        # make a fake cycle the length of the whole recording
        frame_to_label = timeline.per_frame_list
        return cls(n_frames, labels, frame_to_label, info=info)

### `get_timeline()`

Transforms frame_to_label to Timeline

Returns:

Type | Description
--- | ---
`Timeline` | timeline of the resulting annotation

Source code in `src\vodex\core.py`

    830
    831
    832
    833
    834
    835
    836
    837
    838
    839
    840
    841
    842
    def get_timeline(self) -> Timeline:
        """
        Transforms frame_to_label to Timeline

        Returns:
            timeline of the resulting annotation
        """
        duration = []
        labels = []
        for label, group in groupby(self.frame_to_label):
            duration.append(sum(1 for _ in group))
            labels.append(label)
        return Timeline(labels, duration)

## `Cycle`

Information about the repeated cycle of labels. Use it when you have some periodic conditions, like : light on , light off, light on, light off... will be made of list of labels [light_on, light_off] that repeat to cover the whole tie of the experiment. All labels must be from the same label group!

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`label_order` | `List[TimeLabel]` | a list of labels in the right order in which they follow | required
`duration` | `Union[np.array, List[int]]` | duration of the corresponding labels, in frames (based on your imaging). Note that these are frames, not volumes ! | required

Source code in `src\vodex\core.py`

    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398
    399
    400
    401
    402
    403
    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    415
    416
    417
    418
    419
    420
    421
    422
    423
    424
    425
    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    437
    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    451
    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    467
    468
    469
    470
    471
    472
    473
    474
    475
    476
    477
    478
    479
    480
    481
    482
    483
    484
    485
    486
    487
    class Cycle:
        """
        Information about the repeated cycle of labels. Use it when you have some periodic conditions, like : light
        on , light off, light on, light off... will be made of list of labels [light_on, light_off]
        that repeat to cover the whole tie of the experiment. All labels must be from the same label group!

        Args:
            label_order: a list of labels in the right order in which they follow
            duration: duration of the corresponding labels, in frames (based on your imaging).
                Note that these are frames, not volumes !
        """

        def __init__(self, label_order: List[TimeLabel], duration: Union[np.array, List[int]]):
            # check that all labels are from the same group
            label_group = label_order[0].group
            for label in label_order:
                assert label.group == label_group, \
                    f"All labels should be from the same group, but got {label.group} and {label_group}"

            # check that timing is int
            assert all(isinstance(n, (int, np.integer)) for n in duration), "timing should be a list of int"

            self.name = label_group
            self.label_order = label_order
            self.duration = list_of_int(duration)
            self.full_length = sum(self.duration)
            # list the length of the cycle, each element is the TimeLabel
            # TODO : turn it into an index ?
            self.per_frame_list = self.get_label_per_frame()

        def __eq__(self, other):
            if isinstance(other, Cycle):
                is_same = [
                    self.name == other.name,
                    self.label_order == other.label_order,
                    self.duration == other.duration,
                    self.full_length == other.full_length,
                    self.per_frame_list == other.per_frame_list
                ]

                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {Cycle} and {type(other)}")
                return NotImplemented

        def get_label_per_frame(self) -> List[TimeLabel]:
            """
            A list of labels per frame for one cycle only.

            Returns:
                labels per frame for one full cycle
            """
            per_frame_label_list = []
            for (label_time, label) in zip(self.duration, self.label_order):
                per_frame_label_list.extend(label_time * [label])
            return per_frame_label_list

        def __str__(self):
            description = f"Cycle : {self.name}\n"
            description = description + f"Length: {self.full_length}\n"
            for (label_time, label) in zip(self.duration, self.label_order):
                description = description + f"Label {label.name}: for {label_time} frames\n"
            return description

        def __repr__(self):
            return self.__str__()

        def fit_frames(self, n_frames: int) -> int:
            """
            Calculates how many cycles you need to fully cover n_frames.

            Args:
                n_frames: number of frames to cover
            Returns:
                number of cycle
            """
            n_cycles = int(np.ceil(n_frames / self.full_length))
            return n_cycles

        def fit_labels_to_frames(self, n_frames: int) -> List[TimeLabel]:
            """
            Create a list of labels corresponding to each frame in the range of n_frames

            Args:
                n_frames: number of frames to fit labels to
            Returns:
                label_per_frame_list
            """
            n_cycles = self.fit_frames(n_frames)
            label_per_frame_list = np.tile(self.per_frame_list, n_cycles)
            # crop the tail
            return list(label_per_frame_list[0:n_frames])

        def fit_cycles_to_frames(self, n_frames: int) -> List[int]:
            """
            Create a list of integers (what cycle iteration it is) corresponding to each frame in the range of n_frames

            Args:
                n_frames: number of frames to fit cycle iterations to
            Returns:
                cycle_per_frame_list
            """
            n_cycles = self.fit_frames(n_frames)
            cycle_per_frame_list = []
            for n in np.arange(n_cycles):
                cycle_per_frame_list.extend([int(n)] * self.full_length)
            # crop the tail
            return cycle_per_frame_list[0:n_frames]

        def to_dict(self):
            label_order = [label.to_dict() for label in self.label_order]
            d = {'timing': self.duration, 'label_order': label_order}
            return d

        def to_json(self):
            return json.dumps(self.to_dict())

        @classmethod
        def from_dict(cls, d):
            label_order = [TimeLabel.from_dict(ld) for ld in d['label_order']]
            return cls(label_order, d['timing'])

        @classmethod
        def from_json(cls, j: str):
            """
            j : json string
            """
            d = json.loads(j)
            return cls.from_dict(d)

### `fit_cycles_to_frames(n_frames)`

Create a list of integers (what cycle iteration it is) corresponding to each frame in the range of n_frames

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`n_frames` | `int` | number of frames to fit cycle iterations to | required

Returns:

Type | Description
--- | ---
`List[int]` | cycle_per_frame_list

Source code in `src\vodex\core.py`

    452
    453
    454
    455
    456
    457
    458
    459
    460
    461
    462
    463
    464
    465
    466
    def fit_cycles_to_frames(self, n_frames: int) -> List[int]:
        """
        Create a list of integers (what cycle iteration it is) corresponding to each frame in the range of n_frames

        Args:
            n_frames: number of frames to fit cycle iterations to
        Returns:
            cycle_per_frame_list
        """
        n_cycles = self.fit_frames(n_frames)
        cycle_per_frame_list = []
        for n in np.arange(n_cycles):
            cycle_per_frame_list.extend([int(n)] * self.full_length)
        # crop the tail
        return cycle_per_frame_list[0:n_frames]

### `fit_frames(n_frames)`

Calculates how many cycles you need to fully cover n_frames.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`n_frames` | `int` | number of frames to cover | required

Returns:

Type | Description
--- | ---
`int` | number of cycle

Source code in `src\vodex\core.py`

    426
    427
    428
    429
    430
    431
    432
    433
    434
    435
    436
    def fit_frames(self, n_frames: int) -> int:
        """
        Calculates how many cycles you need to fully cover n_frames.

        Args:
            n_frames: number of frames to cover
        Returns:
            number of cycle
        """
        n_cycles = int(np.ceil(n_frames / self.full_length))
        return n_cycles

### `fit_labels_to_frames(n_frames)`

Create a list of labels corresponding to each frame in the range of n_frames

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`n_frames` | `int` | number of frames to fit labels to | required

Returns:

Type | Description
--- | ---
`List[TimeLabel]` | label_per_frame_list

Source code in `src\vodex\core.py`

    438
    439
    440
    441
    442
    443
    444
    445
    446
    447
    448
    449
    450
    def fit_labels_to_frames(self, n_frames: int) -> List[TimeLabel]:
        """
        Create a list of labels corresponding to each frame in the range of n_frames

        Args:
            n_frames: number of frames to fit labels to
        Returns:
            label_per_frame_list
        """
        n_cycles = self.fit_frames(n_frames)
        label_per_frame_list = np.tile(self.per_frame_list, n_cycles)
        # crop the tail
        return list(label_per_frame_list[0:n_frames])

### `from_json(j)` `classmethod`

j : json string

Source code in `src\vodex\core.py`

    481
    482
    483
    484
    485
    486
    487
    @classmethod
    def from_json(cls, j: str):
        """
        j : json string
        """
        d = json.loads(j)
        return cls.from_dict(d)

### `get_label_per_frame()`

A list of labels per frame for one cycle only.

Returns:

Type | Description
--- | ---
`List[TimeLabel]` | labels per frame for one full cycle

Source code in `src\vodex\core.py`

    404
    405
    406
    407
    408
    409
    410
    411
    412
    413
    414
    def get_label_per_frame(self) -> List[TimeLabel]:
        """
        A list of labels per frame for one cycle only.

        Returns:
            labels per frame for one full cycle
        """
        per_frame_label_list = []
        for (label_time, label) in zip(self.duration, self.label_order):
            per_frame_label_list.extend(label_time * [label])
        return per_frame_label_list

## `FileManager`

Collects and stores the information about all the image files. By default, it will search for all the files with the provided file extension in the provided data director and order them alphabetically. If a list of file names is provided, will use these files in the provided order.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`data_dir` | `Union[str, Path]` | path to the folder with the files, ends with "/" or "\" | required
`file_names` | `List[str]` | names of files relative to the data_dir (TODO:?) | `None`
`frames_per_file` | `List[int]` | number of frames in each file. Will be used ONLY if the file_names were provided. | `None`
`file_type` | `str` | file type to search for (if files are not provided). Must be a key in the VX_SUPPORTED_TYPES dict. | `'TIFF'`

Attributes:

Name | Type | Description
--- | --- | ---
`data_dir` | `Path` | the directory with all the imaging data
`file_names` | `List[str]` | names of files relative to the data_dir (TODO:?)
`loader` | `ImageLoader` | initialised image loader (see loaders.ImageLoader for more details)
`num_frames` |  | a number of frames per file
`n_files` | `int` | total number of image files

Source code in `src\vodex\core.py`

     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    class FileManager:
        """
        Collects and stores the information about all the image files.
        By default, it will search for all the files with the provided file extension
        in the provided data director and order them alphabetically.
        If a list of file names is provided, will use these files in the provided order.

        Args:
            data_dir: path to the folder with the files, ends with "/" or "\\"
            file_names: names of files relative to the data_dir (TODO:?)
            frames_per_file: number of frames in each file. Will be used ONLY if the file_names were provided.
            file_type: file type to search for (if files are not provided). Must be a key in the VX_SUPPORTED_TYPES dict.

        Attributes:
            data_dir: the directory with all the imaging data
            file_names: names of files relative to the data_dir (TODO:?)
            loader: initialised image loader (see loaders.ImageLoader for more details)
            num_frames: a number of frames per file
            n_files: total number of image files
        """

        def __init__(self, data_dir: Union[str, Path], file_type: str = "TIFF",
                     file_names: List[str] = None, frames_per_file: List[int] = None):

            # 1. get data_dir and check it exists
            self.data_dir: Path = Path(data_dir)
            assert self.data_dir.is_dir(), f"No directory {self.data_dir}"

            # 2. get files
            if file_names is not None:
                tags = [name.split(".")[-1] for name in file_names]
                # check that all the elements of the list are same
                assert len(set(tags)) == 1, "Error initializing FileManager: " \
                                            f"file_names must be files with the same resolution, but got {set(tags)}"
                file_type = VX_EXTENSION_TO_TYPE[tags[0]]
            self.file_type = file_type
            file_extensions = VX_SUPPORTED_TYPES[self.file_type]

            self.num_frames = None
            # TODO : check in accordance with the file extension/ figure out file type from files when provided
            if file_names is None:
                # if files are not provided , search for tiffs in the data_dir
                self.file_names: List[str] = self.find_files(file_extensions)
            else:
                # if a list of files is provided, check it's in the folder
                self.file_names: List[str] = self.check_files(file_names)
                if frames_per_file is not None:
                    # not recommended! this information is taken as is and is not verified...
                    self.num_frames: List[int] = frames_per_file

            assert len(self.file_names) > 0, f"Error when initialising FileManager:\n" \
                                             f"No files of type {file_type} [extensions {file_extensions}]\n" \
                                             f" in {data_dir}"
            # 3. Initialise ImageLoader
            #    will pick the image loader that works with the provided file type
            self.loader: ImageLoader = ImageLoader(self.data_dir.joinpath(self.file_names[0]))

            # 4. Get number of frames per file (if it wasn't entered manually)
            if self.num_frames is None:
                # if number of frames not provided , search for tiffs in the data_dir
                self.num_frames: List[int] = self.get_frames_per_file()

            # check that the type is int
            assert all(isinstance(n, (int, np.integer)) for n in self.num_frames), \
                "self.num_frames should be a list of int"

            self.n_files: int = len(self.file_names)

        def __str__(self):
            description = f"Image files information :\n\n"
            description = description + f"files directory: {self.data_dir}\n"
            description = description + f"files [number of frames]: \n"
            for (i_file, (file_name, num_frames)) in enumerate(zip(self.file_names, self.num_frames)):
                description = description + f"{i_file}) {file_name} [{num_frames}]\n"
            return description

        def __repr__(self):
            return self.__str__

        def __eq__(self, other):
            if isinstance(other, FileManager):
                is_same = [
                    self.data_dir == other.data_dir,
                    self.file_names == other.file_names,
                    self.loader == other.loader,
                    self.num_frames == other.num_frames,
                    self.n_files == other.n_files
                ]

                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {FileManager} and {type(other)}")
                return NotImplemented

        def find_files(self, file_extensions: Tuple[str]) -> List[str]:
            """
            Searches for files ending with the provided file extension in the data directory.

            Args:
                file_extensions: extensions of files to search for
            Returns:
                A list of file names. File names are with the extension, relative to the data directory
                (names only, not full paths to files)
            """
            files = (p.resolve() for p in Path(self.data_dir).glob("**/*") if p.suffix in file_extensions)
            file_names = [file.name for file in files]
            return file_names

        def check_files(self, file_names: List[str]) -> List[str]:
            """
            Given a list of files checks that files are in the data directory.
            Throws an error if any of the files are missing.

            Args:
                file_names: list of filenames to check.

            Returns:
                If the files are all present in the directory, returns the file_names.
            """
            # TODO: List all the missing files, not just the first encountered.
            files = [self.data_dir.joinpath(file) for file in file_names]
            for file in files:
                assert file.is_file(), f"File {file} is not found"
            return file_names

        def get_frames_per_file(self) -> List[int]:
            """
            Get the number of frames per file.

            Returns:
                a list with number of frames per file.
            """
            frames_per_file = []
            for file in self.file_names:
                n_frames = self.loader.get_frames_in_file(self.data_dir.joinpath(file))
                frames_per_file.append(n_frames)
            return frames_per_file

        def change_files_order(self, order: List[int]) -> None:
            """
            Changes the order of the files. If you notice that files are in the wrong order, provide the new order.
            If you wish to exclude any files, get rid of them ( don't include their IDs into the new order ).

            Args:
                order: The new order in which the files follow. Refer to file by it's position in the original list.
                Should be the same length as the number of files in the original list or smaller, no duplicates.
            """
            # TODO : test it
            assert len(order) <= self.n_files, \
                "Number of files is smaller than elements in the new order list! "
            assert len(order) == len(set(order)), \
                "All elements in the new order list must be unique! "

            self.file_names = [self.file_names[i] for i in order]
            self.num_frames = [self.num_frames[i] for i in order]
            self.n_files = len(self.file_names)

### `change_files_order(order)`

Changes the order of the files. If you notice that files are in the wrong order, provide the new order. If you wish to exclude any files, get rid of them ( don't include their IDs into the new order ).

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`order` | `List[int]` | The new order in which the files follow. Refer to file by it's position in the original list. | required

Source code in `src\vodex\core.py`

    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    def change_files_order(self, order: List[int]) -> None:
        """
        Changes the order of the files. If you notice that files are in the wrong order, provide the new order.
        If you wish to exclude any files, get rid of them ( don't include their IDs into the new order ).

        Args:
            order: The new order in which the files follow. Refer to file by it's position in the original list.
            Should be the same length as the number of files in the original list or smaller, no duplicates.
        """
        # TODO : test it
        assert len(order) <= self.n_files, \
            "Number of files is smaller than elements in the new order list! "
        assert len(order) == len(set(order)), \
            "All elements in the new order list must be unique! "

        self.file_names = [self.file_names[i] for i in order]
        self.num_frames = [self.num_frames[i] for i in order]
        self.n_files = len(self.file_names)

### `check_files(file_names)`

Given a list of files checks that files are in the data directory. Throws an error if any of the files are missing.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`file_names` | `List[str]` | list of filenames to check. | required

Returns:

Type | Description
--- | ---
`List[str]` | If the files are all present in the directory, returns the file_names.

Source code in `src\vodex\core.py`

    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    def check_files(self, file_names: List[str]) -> List[str]:
        """
        Given a list of files checks that files are in the data directory.
        Throws an error if any of the files are missing.

        Args:
            file_names: list of filenames to check.

        Returns:
            If the files are all present in the directory, returns the file_names.
        """
        # TODO: List all the missing files, not just the first encountered.
        files = [self.data_dir.joinpath(file) for file in file_names]
        for file in files:
            assert file.is_file(), f"File {file} is not found"
        return file_names

### `find_files(file_extensions)`

Searches for files ending with the provided file extension in the data directory.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`file_extensions` | `Tuple[str]` | extensions of files to search for | required

Returns:

Type | Description
--- | ---
`List[str]` | A list of file names. File names are with the extension, relative to the data directory
`List[str]` | (names only, not full paths to files)

Source code in `src\vodex\core.py`

    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    def find_files(self, file_extensions: Tuple[str]) -> List[str]:
        """
        Searches for files ending with the provided file extension in the data directory.

        Args:
            file_extensions: extensions of files to search for
        Returns:
            A list of file names. File names are with the extension, relative to the data directory
            (names only, not full paths to files)
        """
        files = (p.resolve() for p in Path(self.data_dir).glob("**/*") if p.suffix in file_extensions)
        file_names = [file.name for file in files]
        return file_names

### `get_frames_per_file()`

Get the number of frames per file.

Returns:

Type | Description
--- | ---
`List[int]` | a list with number of frames per file.

Source code in `src\vodex\core.py`

    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    def get_frames_per_file(self) -> List[int]:
        """
        Get the number of frames per file.

        Returns:
            a list with number of frames per file.
        """
        frames_per_file = []
        for file in self.file_names:
            n_frames = self.loader.get_frames_in_file(self.data_dir.joinpath(file))
            frames_per_file.append(n_frames)
        return frames_per_file

## `FrameManager`

Deals with frames. Which frames correspond to a volume / cycle/ condition.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`file_manager` | `FileManager` | info about the files. | required

Source code in `src\vodex\core.py`

    557
    558
    559
    560
    561
    562
    563
    564
    565
    566
    567
    568
    569
    570
    571
    572
    573
    574
    575
    576
    577
    578
    579
    580
    581
    582
    583
    584
    585
    586
    587
    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    609
    610
    611
    612
    613
    614
    class FrameManager:
        """
        Deals with frames. Which frames correspond to a volume / cycle/ condition.

        Args:
            file_manager: info about the files.
        """

        def __init__(self, file_manager: FileManager):
            self.file_manager = file_manager
            self.n_frames: int = int(np.sum(self.file_manager.num_frames))
            self.frame_to_file, self.frame_in_file = self.get_frame_mapping()

        def __eq__(self, other):
            if isinstance(other, FrameManager):
                is_same = [
                    self.file_manager == other.file_manager,
                    self.frame_to_file == other.frame_to_file,
                    self.frame_in_file == other.frame_in_file
                ]

                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {FrameManager} and {type(other)}")
                return NotImplemented

        @classmethod
        def from_dir(cls, data_dir, file_names=None, frames_per_file=None):
            file_manager = FileManager(data_dir, file_names=file_names, frames_per_file=frames_per_file)
            return cls(file_manager)

        def get_frame_mapping(self) -> (List[int], List[int]):
            """
            Calculates frame range in each file and returns a file index for each frame and frame index in the file.
            Used to figure out in which stack the requested frames is.
            Frame number starts at 0.

            Returns:
                Two lists mapping frames to files. 'frame_to_file' is a list of length equal to the total number of
                frames in all the files, where each element corresponds to a frame and contains the file index,
                of the file where that frame can be found. 'in_file_frame' is a list of length equal to the total number of
                frames in all the files, where each element corresponds to the index of the frame inside the file.
            """
            frame_to_file = []
            frame_in_file = []

            for file_idx in range(self.file_manager.n_files):
                n_frames = self.file_manager.num_frames[file_idx]
                frame_to_file.extend(n_frames * [file_idx])
                frame_in_file.extend(range(n_frames))

            return frame_to_file, frame_in_file

        def __str__(self):
            return f"Total {np.sum(self.file_manager.num_frames)} frames."

        def __repr__(self):
            return self.__str__()

### `get_frame_mapping()`

Calculates frame range in each file and returns a file index for each frame and frame index in the file. Used to figure out in which stack the requested frames is. Frame number starts at 0.

Returns:

Type | Description
--- | ---
`List[int], List[int]` | Two lists mapping frames to files. 'frame_to_file' is a list of length equal to the total number of
`List[int], List[int]` | frames in all the files, where each element corresponds to a frame and contains the file index,
`List[int], List[int]` | of the file where that frame can be found. 'in_file_frame' is a list of length equal to the total number of
`List[int], List[int]` | frames in all the files, where each element corresponds to the index of the frame inside the file.

Source code in `src\vodex\core.py`

    588
    589
    590
    591
    592
    593
    594
    595
    596
    597
    598
    599
    600
    601
    602
    603
    604
    605
    606
    607
    608
    def get_frame_mapping(self) -> (List[int], List[int]):
        """
        Calculates frame range in each file and returns a file index for each frame and frame index in the file.
        Used to figure out in which stack the requested frames is.
        Frame number starts at 0.

        Returns:
            Two lists mapping frames to files. 'frame_to_file' is a list of length equal to the total number of
            frames in all the files, where each element corresponds to a frame and contains the file index,
            of the file where that frame can be found. 'in_file_frame' is a list of length equal to the total number of
            frames in all the files, where each element corresponds to the index of the frame inside the file.
        """
        frame_to_file = []
        frame_in_file = []

        for file_idx in range(self.file_manager.n_files):
            n_frames = self.file_manager.num_frames[file_idx]
            frame_to_file.extend(n_frames * [file_idx])
            frame_in_file.extend(range(n_frames))

        return frame_to_file, frame_in_file

## `Labels`

Describes a particular group of time labels. TODO : also responsible for colors for plotting these labels?

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`group` |  | the name of the group | required
`group_info` |  | description of what this group is about. Just for storing the information. | `None`
`state_names` | `List[str]` | the state names | required
`state_info` | `dict` | description of each individual state {state name : description}. Just for storing the information. | `None`

Attributes:

Name | Type | Description
--- | --- | ---
`group` | `str` | the name of the group
`group_info` | `str` | description of what this group is about. Just for storing the information.
`state_names` | `List[str]` | the state names
`states` | `List[TimeLabel]` | list of states, each state as a TimeLabel object

Source code in `src\vodex\core.py`

    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    class Labels:
        """
        Describes a particular group of time labels.
        TODO : also responsible for colors for plotting these labels?

        Args:
            group : the name of the group
            group_info : description of what this group is about. Just for storing the information.
            state_names: the state names
            state_info: description of each individual state {state name : description}. Just for storing the information.

        Attributes:
            group (str): the name of the group
            group_info (str): description of what this group is about. Just for storing the information.
            state_names (List[str]): the state names
            states (List[TimeLabel]): list of states, each state as a TimeLabel object

        """

        def __init__(self, group: str, state_names: List[str], group_info: str = None, state_info: dict = None):

            if state_info is None:
                state_info = {}
            self.group = group
            self.group_info = group_info
            self.state_names = state_names
            # create states
            self.states = []
            for state_name in self.state_names:
                if state_name in state_info:
                    state = TimeLabel(state_name, description=state_info[state_name], group=self.group)
                    setattr(self, state_name, state)
                else:
                    state = TimeLabel(state_name, group=self.group)
                    setattr(self, state_name, state)
                self.states.append(state)

        def __eq__(self, other):
            if isinstance(other, Labels):
                is_same = [
                    self.group == other.group,
                    self.group_info == other.group_info,
                    self.state_names == other.state_names,
                    self.states == other.states
                ]

                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {Labels} and {type(other)}")
                return NotImplemented

        def __str__(self):
            description = f"Label group : {self.group}\n"
            description = description + f"States:\n"
            for state_name in self.state_names:
                description = description + f"{getattr(self, state_name)}\n"
            return description

        def __repr__(self):
            return self.__str__()

## `TimeLabel`

Describes a particular time-located event during the experiment. Any specific aspect of the experiment that you may want to document : temperature|light|sound|image on the screen|drug|behaviour ... etc.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`name` | `str` | the name for the time label. This is a unique identifier of the label. Different labels must have different names. Different labels are compared based on their names, so the same name means it is the same event. | required
`description` | `str` | a detailed description of the label. This is to give you more info, but it is not used for anything else. | `None`
`group` | `str` | the group that the label belongs to. | `None`

Attributes:

Name | Type | Description
--- | --- | ---
`name` | `str` | the name for the time label. This is a unique identifier of the label. Different labels must have different names. Different labels are compared based on their names, so the same name means it is the same event.
`description` | `str` | a detailed description of the label. This is to give you more info, but it is not used for anything else.
`group` | `str` | the group that the label belongs to.

Source code in `src\vodex\core.py`

    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    class TimeLabel:
        """
        Describes a particular time-located event during the experiment.
        Any specific aspect of the experiment that you may want to document :
        temperature|light|sound|image on the screen|drug|behaviour ... etc.

        Args:
            name: the name for the time label. This is a unique identifier of the label.
                        Different labels must have different names.
                        Different labels are compared based on their names, so the same name means it is the same event.
            description: a detailed description of the label. This is to give you more info, but it is not used for
                anything else.
            group: the group that the label belongs to.

        Attributes:
            name: the name for the time label. This is a unique identifier of the label.
                        Different labels must have different names.
                        Different labels are compared based on their names, so the same name means it is the same event.
            description: a detailed description of the label. This is to give you more info, but it is not used for
                anything else.
            group: the group that the label belongs to.
        """

        def __init__(self, name: str, description: str = None, group: str = None):
            self.name: str = name
            self.group: str = group
            self.description: str = description

        def __str__(self):
            description = self.name
            if self.description is not None:
                description = description + " : " + self.description
            if self.group is not None:
                description = description + ". Group: " + self.group
            return description

        def __repr__(self):
            return self.__str__()

        def __hash__(self):
            # necessary for instances to behave sanely in dicts and sets.
            return hash((self.name, self.description))

        def __eq__(self, other):
            if isinstance(other, TimeLabel):
                # comparing by name
                same_name = self.name == other.name
                if self.group is not None or other.group is not None:
                    same_group = self.group == other.group
                    return same_name and same_group
                else:
                    return same_name
            else:
                print(f"__eq__ is Not Implemented for {TimeLabel} and {type(other)}")
                return NotImplemented

        def __ne__(self, other):
            return not self.__eq__(other)

        def to_dict(self) -> dict:
            """
            Put all the information about a TimeLabel object into a dictionary.

            Returns:
                a dictionary with fields 'name', 'group', 'description' storing the corresponding attributes.
            """
            d = {'name': self.name}
            if self.group is not None:
                d['group'] = self.group
            if self.description is not None:
                d['description'] = self.description
            return d

        @classmethod
        def from_dict(cls, d):
            """
            Create a TimeLabel object from a dictionary.

            Returns:
                (TimeLabel): a TimeLabel object with attributes 'name', 'group', 'description'
                filled from the dictionary fields.
            """
            description = None
            group = None
            if 'description' in d:
                description = d['description']
            if 'group' in d:
                group = d['group']
            return cls(d['name'], description=description, group=group)

### `from_dict(d)` `classmethod`

Create a TimeLabel object from a dictionary.

Returns:

Type | Description
--- | ---
`TimeLabel` | a TimeLabel object with attributes 'name', 'group', 'description'
| filled from the dictionary fields.

Source code in `src\vodex\core.py`

    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    @classmethod
    def from_dict(cls, d):
        """
        Create a TimeLabel object from a dictionary.

        Returns:
            (TimeLabel): a TimeLabel object with attributes 'name', 'group', 'description'
            filled from the dictionary fields.
        """
        description = None
        group = None
        if 'description' in d:
            description = d['description']
        if 'group' in d:
            group = d['group']
        return cls(d['name'], description=description, group=group)

### `to_dict()`

Put all the information about a TimeLabel object into a dictionary.

Returns:

Type | Description
--- | ---
`dict` | a dictionary with fields 'name', 'group', 'description' storing the corresponding attributes.

Source code in `src\vodex\core.py`

    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    def to_dict(self) -> dict:
        """
        Put all the information about a TimeLabel object into a dictionary.

        Returns:
            a dictionary with fields 'name', 'group', 'description' storing the corresponding attributes.
        """
        d = {'name': self.name}
        if self.group is not None:
            d['group'] = self.group
        if self.description is not None:
            d['description'] = self.description
        return d

## `Timeline`

Information about the sequence of labels. Use it when you have non-periodic conditions.

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`label_order` | `List[TimeLabel]` | a list of labels in the right order in which they follow | required
`duration` | `List[int]` | duration of the corresponding labels, in frames (based on your imaging). Note that these are frames, not volumes ! | required

Source code in `src\vodex\core.py`

    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512
    513
    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    545
    546
    547
    548
    549
    550
    551
    552
    553
    554
    class Timeline:
        """
        Information about the sequence of labels. Use it when you have non-periodic conditions.

        Args:
            label_order: a list of labels in the right order in which they follow
            duration: duration of the corresponding labels, in frames (based on your imaging). Note that these are
                frames, not volumes !
        """

        def __init__(self, label_order: List[TimeLabel], duration: List[int]):

            # check that all labels are from the same group
            label_group = label_order[0].group
            for label in label_order:
                assert label.group == label_group, \
                    f"All labels should be from the same group, but got {label.group} and {label_group}"

            # check that timing is int
            assert all(isinstance(n, int) for n in duration), "duration should be a list of int"

            self.name = label_group
            self.label_order = label_order
            self.duration = list(duration)
            self.full_length = sum(self.duration)
            # list the length of the cycle, each element is the TimeLabel
            # TODO : turn it into an index ?
            self.per_frame_list = self.get_label_per_frame()

        def __eq__(self, other):
            if isinstance(other, Timeline):
                is_same = [
                    self.name == other.name,
                    self.label_order == other.label_order,
                    self.duration == other.duration,
                    self.full_length == other.full_length,
                    self.per_frame_list == other.per_frame_list
                ]

                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {Timeline} and {type(other)}")
                return NotImplemented

        def get_label_per_frame(self) -> List[TimeLabel]:
            """
            A list of labels per frame for the duration of the experiment.

            Returns:
                labels per frame for the experiment.
            """
            per_frame_label_list = []
            for (label_time, label) in zip(self.duration, self.label_order):
                per_frame_label_list.extend(label_time * [label])
            return per_frame_label_list

        def __str__(self):
            description = f"Timeline : {self.name}\n"
            description = description + f"Length: {self.full_length}\n"
            for (label_time, label) in zip(self.duration, self.label_order):
                description = description + f"Label {label.name}: for {label_time} frames\n"
            return description

        def __repr__(self):
            return self.__str__()

### `get_label_per_frame()`

A list of labels per frame for the duration of the experiment.

Returns:

Type | Description
--- | ---
`List[TimeLabel]` | labels per frame for the experiment.

Source code in `src\vodex\core.py`

    534
    535
    536
    537
    538
    539
    540
    541
    542
    543
    544
    def get_label_per_frame(self) -> List[TimeLabel]:
        """
        A list of labels per frame for the duration of the experiment.

        Returns:
            labels per frame for the experiment.
        """
        per_frame_label_list = []
        for (label_time, label) in zip(self.duration, self.label_order):
            per_frame_label_list.extend(label_time * [label])
        return per_frame_label_list

## `VolumeManager`

Figures out how to get full volumes for certain time points.

Learning Resourses

maybe I should do type checking automatically, something like here: https://stackoverflow.com/questions/9305751/how-to-force-ensure-class-attributes-are-a-specific-type

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`fpv` | `int` | frames per volume, number of frames in one volume | required
`fgf` | `int` | first good frame, the first frame in the imaging session that is at the top of a volume. For example if you started imaging at the top of the volume, fgf = 0, but if you started somewhere in the middle, the first good frame is , for example, 23 ... | `0`
`frame_manager` | `FrameManager` | the info about the frames | required

Source code in `src\vodex\core.py`

    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679
    680
    681
    682
    683
    684
    685
    686
    687
    688
    689
    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    703
    704
    705
    706
    707
    708
    709
    710
    711
    712
    713
    714
    715
    716
    717
    718
    719
    720
    721
    722
    723
    class VolumeManager:
        """
        Figures out how to get full volumes for certain time points.

        Learning Resourses:
            maybe I should do type checking automatically, something like here:
            https://stackoverflow.com/questions/9305751/how-to-force-ensure-class-attributes-are-a-specific-type

        Args:
            fpv: frames per volume, number of frames in one volume
            fgf: first good frame, the first frame in the imaging session that is at the top of a volume.
                For example if you started imaging at the top of the volume, fgf = 0,
                but if you started somewhere in the middle, the first good frame is , for example, 23 ...
            frame_manager: the info about the frames

        Attributes:

        """

        def __init__(self, fpv: int, frame_manager: FrameManager, fgf: int = 0):

            assert isinstance(fpv, int) or (isinstance(fpv, float) and fpv.is_integer()), "fpv must be an integer"
            assert isinstance(fgf, int) or (isinstance(fgf, float) and fgf.is_integer()), "fgf must be an integer"

            # frames per volume
            self.fpv: int = int(fpv)

            # get total number of frames
            self.frame_manager: FrameManager = frame_manager
            self.file_manager: FileManager = frame_manager.file_manager
            self.n_frames: int = int(np.sum(self.file_manager.num_frames))

            # prepare info about frames at the beginning, full volumes and frames at the end
            # first good frame, start counting from 0 : 0, 1, 2, 3, ...
            # n_head is the number of frames before the first frame of the first full volume
            # n_tail is the number of frames after the last frame of the last full volume
            self.n_head: int = int(fgf)
            full_volumes, n_tail = divmod((self.n_frames - self.n_head), self.fpv)
            self.full_volumes: int = int(full_volumes)
            self.n_tail: int = int(n_tail)

            # map frames to slices an full volumes:
            self.frame_to_z: List[int] = self.get_frames_to_z_mapping()
            self.frame_to_vol: List[int] = self.get_frames_to_volumes_mapping()

        def __eq__(self, other):
            if isinstance(other, VolumeManager):
                is_same = [
                    self.fpv == other.fpv,
                    self.frame_manager == other.frame_manager,
                    self.file_manager == other.file_manager,
                    self.n_frames == other.n_frames,
                    self.n_head == other.n_head,
                    self.full_volumes == other.full_volumes,
                    self.n_tail == other.n_tail,
                    self.frame_to_z == other.frame_to_z,
                    self.frame_to_vol == other.frame_to_vol
                ]

                return np.all(is_same)
            else:
                print(f"__eq__ is Not Implemented for {VolumeManager} and {type(other)}")
                return NotImplemented

        def get_frames_to_z_mapping(self):
            z_per_frame_list = np.arange(self.fpv).astype(int)
            # set at what z the imaging starts and ends
            i_from = self.fpv - self.n_head
            i_to = self.n_tail - self.fpv
            # map frames to z
            frame_to_z = np.tile(z_per_frame_list, self.full_volumes + 2)[i_from:i_to]
            return frame_to_z.tolist()

        def get_frames_to_volumes_mapping(self):
            """
            maps frames to volumes
            -1 for head ( not full volume at the beginning )
            volume number for full volumes : 0, 1, ,2 3, ...
            -2 for tail (not full volume at the end )
            """
            # TODO : make sure n_head is not larger than full volume?
            frame_to_vol = [-1] * self.n_head
            for vol in np.arange(self.full_volumes):
                frame_to_vol.extend([int(vol)] * self.fpv)
            frame_to_vol.extend([-2] * self.n_tail)
            return frame_to_vol

        def __str__(self):
            description = ""
            description = description + f"Total frames : {self.n_frames}\n"
            description = description + f"Volumes start on frame : {self.n_head}\n"
            description = description + f"Total good volumes : {self.full_volumes}\n"
            description = description + f"Frames per volume : {self.fpv}\n"
            description = description + f"Tailing frames (not a full volume , at the end) : {self.n_tail}\n"
            return description

        def __repr__(self):
            return self.__str__()

        @classmethod
        def from_dir(cls, data_dir, fpv, fgf=0, file_names=None, frames_per_file=None):
            """
            Creates a VolumeManager object from directory.
            """
            file_manager = FileManager(data_dir, file_names=file_names, frames_per_file=frames_per_file)
            frame_manager = FrameManager(file_manager)
            return cls(fpv, frame_manager, fgf=fgf)

### `from_dir(data_dir, fpv, fgf=0, file_names=None, frames_per_file=None)` `classmethod`

Creates a VolumeManager object from directory.

Source code in `src\vodex\core.py`

    716
    717
    718
    719
    720
    721
    722
    723
    @classmethod
    def from_dir(cls, data_dir, fpv, fgf=0, file_names=None, frames_per_file=None):
        """
        Creates a VolumeManager object from directory.
        """
        file_manager = FileManager(data_dir, file_names=file_names, frames_per_file=frames_per_file)
        frame_manager = FrameManager(file_manager)
        return cls(fpv, frame_manager, fgf=fgf)

### `get_frames_to_volumes_mapping()`

maps frames to volumes -1 for head ( not full volume at the beginning ) volume number for full volumes : 0, 1, ,2 3, ... -2 for tail (not full volume at the end )

Source code in `src\vodex\core.py`

    690
    691
    692
    693
    694
    695
    696
    697
    698
    699
    700
    701
    702
    def get_frames_to_volumes_mapping(self):
        """
        maps frames to volumes
        -1 for head ( not full volume at the beginning )
        volume number for full volumes : 0, 1, ,2 3, ...
        -2 for tail (not full volume at the end )
        """
        # TODO : make sure n_head is not larger than full volume?
        frame_to_vol = [-1] * self.n_head
        for vol in np.arange(self.full_volumes):
            frame_to_vol.extend([int(vol)] * self.fpv)
        frame_to_vol.extend([-2] * self.n_tail)
        return frame_to_vol

#### Keyboard Shortcuts

Keys | Action
--- | ---
`?` | Open this help
`n` | Next page
`p` | Previous page
`s` | Search