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