"""
@file   utils.py
@author Jianfei Guo, Shanghai AI Lab
@brief  Utilities for (voxel)-grid-based neural network modules.
"""

import numpy as np
from enum import Enum
from typing import Union
from numbers import Number

import torch
import torch.nn.functional as F

def gridsample1d_by2d(input: torch.Tensor, grid: torch.Tensor, padding_mode: str = 'zeros', align_corners=False) -> torch.Tensor:
    """ 1D version of F.grid_sample by using 2D F.grid_sample
    Modifed from https://github.com/luo3300612/grid_sample1d

    Args:
        input (torch.Tensor): [B, C, M]
        grid (torch.Tensor): [B, N]
        padding_mode (str, optional): `padding_mode` defined the same way in F.grid_sample. Defaults to 'zeros'.
        align_corners (bool, optional): `align_cornders` defined the same way in F.grid_sample. Defaults to False.

    Returns:
        torch.Tensor: [B, C, N]
    """
    shape = grid.shape
    input = input.unsqueeze(-1)  # batch_size * C * L_in * 1
    grid = grid.unsqueeze(1)  # batch_size * 1 * L_out
    grid = torch.stack([grid.new_zeros(grid.shape), grid], dim=-1) # NOTE: pseudo dim should be zeros !
    z = F.grid_sample(input, grid, padding_mode=padding_mode, align_corners=align_corners)
    C = input.shape[1]
    z = z.view(shape[0], C, shape[1])  # batch_size * C * L_out
    return z

#----------------------------------------------------
#--------------- Dealing with voxels ----------------
#----------------------------------------------------
class VoxelMode(Enum):
    XYZ=0
    ZYX=1
    PT3D=2

def offset_voxel(ref_coords: torch.Tensor=None, _0: float=0., _1: float=1., mode: VoxelMode=VoxelMode.ZYX) -> torch.Tensor:
    """ Calculate vertex coords from given voxel center / corner / etc.

    Args:
        ref_coords (torch.Tensor, optional): An optional input coords in voxel, could be center or corner or anything. Defaults to None.
        _0 (float, optional): Lower-bound offset value. Defaults to 0.
        _1 (float, optional): Upper-bound offset value. Defaults to 1.
        mode (VoxelMode, optional): Specific pattern of voxel vertex arrangement. Defaults to VoxelMode.ZYX.

    Returns:
        torch.Tensor: Voxel vertices
    """
    if mode==VoxelMode.XYZ:
        """
        (row-major)row-col-depth  ==  x-y-z;

         ↖ z
          `.
        (1) +---------+. (5)
            | ` .     |  ` .
            | (0) o---+-----+ (4) -> x
            |     |   |     |
        (3) +-----+---+. (7)|
            ` .   |     ` . |
            (2) ` +---------+ (6)
                  |
                  v y
        """
        off = torch.tensor([
            [_0, _0, _0],
            [_0, _0, _1],
            [_0, _1, _0],
            [_0, _1, _1],
            [_1, _0, _0],
            [_1, _0, _1],
            [_1, _1, _0],
            [_1, _1, _1]
        ])
    elif mode==VoxelMode.PT3D:
        """
         ↖ z
          `.
        (4) +---------+. (5)
            | ` .     |  ` .
            | (0) o---+-----+ (1) -> x
            |     |   |     |
        (7) +-----+---+. (6)|
            ` .   |     ` . |
            (3) ` +---------+ (2)
                  |
                  v y
        """
        off = torch.tensor([
            [_0, _0, _0],
            [_1, _0, _0],
            [_1, _1, _0],
            [_0, _1, _0],
            [_0, _0, _1],
            [_1, _0, _1],
            [_1, _1, _1],
            [_0, _1, _1],
        ])
    elif mode==VoxelMode.ZYX:
        """
        (row-major)row-col-depth  ==  z-y-x

         ↖ z
          `.
        (4) +---------+. (5)
            | ` .     |  ` .
            | (0) o---+-----+ (1) -> x
            |     |   |     |
        (6) +-----+---+. (7)|
            ` .   |     ` . |
            (2) ` +---------+ (3)
                  |
                  v y
        """
        off = torch.tensor([
            [_0, _0, _0],
            [_1, _0, _0],
            [_0, _1, _0],
            [_1, _1, _0],
            [_0, _0, _1],
            [_1, _0, _1],
            [_0, _1, _1],
            [_1, _1, _1]
        ])
    else:
        raise NotImplementedError
    if ref_coords is None:
        return off
    else:
        return off.to(ref_coords).reshape([*[1]*(ref_coords.dim()-1), 8, 3])

def offset_voxel_unflatten(ref_coords: torch.Tensor=None):
    off = torch.tensor(
        [[[[0, 0, 0],
          [0, 0, 1]],
         [[0, 1, 0],
          [0, 1, 1]]],
        [[[1, 0, 0],
          [1, 0, 1]],
         [[1, 1, 0],
          [1, 1, 1]]]])
    if ref_coords is None:
        return off
    else:
        return off.to(ref_coords).reshape([*[1]*(ref_coords.dim()-1), 2,2,2, 3])

def trilinear_voxel(rel_coords: torch.Tensor, verts_feats: torch.Tensor, _0: float=0., _1: float=1., mode=VoxelMode.ZYX) -> torch.Tensor:
    """ Trilinear interpolation given relative position and vertex features

    rel_coords: [*prefix, 3], range [0->1]
    verts_feats:[*prefix, 8, F]

    return:     [*prefix, F]

    Args:
        rel_coords (torch.Tensor): [..., 3] in range [_0,_1], relative 3D position in voxel
        verts_feats (torch.Tensor): [..., 8, F] voxel vertex features
        _0 (float, optional): Lower bound of rel_coords. Defaults to 0.
        _1 (float, optional): Upper bound of rel_coords. Defaults to 1.
        mode (VoxelMode, optional): Specific pattern of voxel vertex arrangement of `verts_feats`. Defaults to VoxelMode.ZYX.

    Returns:
        torch.Tensor: [..., F] The interpolated features on the given relative position
    """
    p = rel_coords.unsqueeze(-2)
    q = offset_voxel(rel_coords, _0=_0, _1=_1, mode=mode)

    # [*prefix, 8, 1]
    weights = (p * q + (1 - p) * (1 - q)).prod(dim=-1, keepdim=True)
    feats = (weights * verts_feats).sum(-2)
    return feats

def points_to_corners(
    points: Union[torch.Tensor, np.ndarray],
    _0: Number=None, _1: Number=None, spacing: Union[torch.Tensor, np.ndarray, Number]=None):

    if _0 is None:
        _0 = 0
    if _1 is None:
        _1 = 1
    """
    (row-major)row-col-depth  ==  x-y-z;

    ↖ z
      `.
    (1) +---------+. (5)
        | ` .     |  ` .
        | (0) o---+-----+ (4) -> x
        |     |   |     |
    (3) +-----+---+. (7)|
        ` .   |     ` . |
        (2) ` +---------+ (6)
              |
              v y
    """
    if isinstance(points, torch.Tensor):
        off = torch.tensor([
            [_0, _0, _0],
            [_0, _0, _1],
            [_0, _1, _0],
            [_0, _1, _1],
            [_1, _0, _0],
            [_1, _0, _1],
            [_1, _1, _0],
            [_1, _1, _1]
        ], device=points.device, dtype=points.dtype)
    elif isinstance(points, np.ndarray):
        off = np.array([
            [_0, _0, _0],
            [_0, _0, _1],
            [_0, _1, _0],
            [_0, _1, _1],
            [_1, _0, _0],
            [_1, _0, _1],
            [_1, _1, _0],
            [_1, _1, _1]
        ], dtype=points.dtype)
    else:
        raise RuntimeError(f"Invalid `points` type={points}")

    if spacing is not None:
        off = off * spacing
    return points[..., None, :] + off

# def offset_patch(ref_coords: torch.Tensor=None):
#     off = torch.tensor([
#         [0., 0.],
#         [0., 1.],
#         [1., 0.],
#         [1., 1.],
#     ])
#     if ref_coords is None:
#         return off
#     else:
#         return off.to(ref_coords).reshape([*[1]*(ref_coords.dim()-1), 4, 3])

# def bilinear_patch(rel_coords: torch.Tensor, verts_feats: torch.Tensor):
#     """
#     rel_coords: [*prefix, 2], range [0->1]
#     verts_feats:[*prefix, 4, F]

#     return:     [*prefix, F]
#     """
#     p = rel_coords.unsqueeze(-2)
#     q = offset_patch(rel_coords)

#     # [*prefix, 4, 1]
#     weights = (p * q + (1 - p) * (1 - q)).prod(dim=-1, keepdim=True)
#     feats = (weights * verts_feats).sum(-2)
#     return feats


from django import forms
from .models import Loan

class LoanAddForm(forms.ModelForm):

    class Meta:
        model = Loan
        fields = [
            'loan_name',
            'loan_start_date',
            'loan_starting_principal',
            'loan_principal',
            'loan_last_action_date',
            'loan_interest',
            'loan_payment'
        ]

class LoanEmailForm(forms.Form):
    name = forms.CharField(max_length=100)
    email = forms.EmailField()


import numpy as np
from enum import IntEnum
import matplotlib.pyplot as plt

global_variable_counter = 0

class Operation(IntEnum):
    S_ADD = 0
    S_SUB = 1
    S_MUL = 2
    S_DIV = 3
    M_ADD = 4
    M_SUB = 5
    M_MUL = 6
    M_ELM = 7
    M_ELD = 8
    S_SQR = 9
    M_ESQ = 10
    M_RDS = 11
    M_TNP = 12

S_OPS = {Operation.S_ADD, Operation.S_SUB, Operation.S_MUL, Operation.S_DIV}
M_ELE = {Operation.M_ADD, Operation.M_SUB, Operation.M_ELM, Operation.M_ELD}
names = ['add', 'sub', 'mul', 'div', 'matadd', 'matsub', 'matmul', 'matelemul', 'matelediv', 'sqr', 'matlelesqr', 'reduce', 'transpose']
specific_names = ['Scalar Addition', 'Scalar Subtraction', 'Scalar Multiplication', 'Scalar Division', 'Matrix Addition', 'Matrix Subtraction', 'Matrix Multiplication', 'Matrix Element-wise Multiplication', 'Matrix Division', 'Scalar Square', 'Matrix Element-wise Square', 'Matrix Reduce Sum']

counts = {name: 0 for name in names}

names_to_ops = {'add': [Operation.S_ADD, Operation.M_ADD],
                'subtract': [Operation.S_SUB, Operation.M_SUB],
                'multiply': [Operation.S_MUL, Operation.M_ELM],
                'divide': [Operation.S_DIV, Operation.M_ELD],
                'square': [Operation.S_SQR, Operation.M_ESQ]
}

used_names = set()

class Variable:
    def __init__(self, operation: Operation = None, value=None, a=None, b=None, trainable=True):
        self.trainable = trainable

        if value is not None:
            if type(value) == np.ndarray:
                self.value = value
                self.shape = value.shape
            elif type(value) == float:
                self.value = value
                self.shape = ()
            elif type(value) == list:
                self.value = np.array(value)
                self.shape = self.value.shape
            else:
                raise TypeError("Supplied value is not of type np.ndarray, list, or float")

            if len(self.shape) > 2:
                raise ValueError("Cannot create tensors of greater than 2 dimensions")

            self.op = None
            self.a = None
            self.b = None
            global global_variable_counter
            self.name = f"Variable {global_variable_counter}"
            global_variable_counter += 1
            return
        else:
            self.value = value

        self.op = operation
        self.name = f"{names[int(self.op)]} {counts[names[int(self.op)]]}"
        counts[names[int(self.op)]] += 1

        if self.op in S_OPS:
            if not (a.is_scalar() and b.is_scalar()):
                raise ValueError("One or both operands is not a scalar")
            self.a = a
            self.b = b
            self.shape = ()
        elif self.op in M_ELE:
            if not a.is_matrix() or not b.is_matrix():
                raise ValueError("One or both operands is not a matrix")
            if not a.shape == b.shape:
                raise ValueError("Matrices are not of same shape")
            self.a = a
            self.b = b
            self.shape = a.shape
        elif self.op == Operation.M_MUL:
            if not len(a.shape) > 0 or not len(b.shape) > 0:
                raise ValueError("One or both operands is not a matrix")
            if not b.shape[1] == 1:
                raise ValueError("Only matrix * column vector is supported for differentiation")
            if not a.shape[1] == b.shape[0]:
                raise ValueError("Incompatible shapes for matrix multiplication")
            self.a = a
            self.b = b
            self.shape = (a.shape[0], b.shape[1])
        elif self.op == Operation.S_SQR:
            if not a.is_scalar():
                raise ValueError("Operand is not a scalar")
            self.a = a
            self.b = None
            self.shape = ()
        elif self.op == Operation.M_ESQ:
            if not a.is_matrix():
                raise ValueError("Operand is not a matrix")
            self.a = a
            self.b = None
            self.shape = self.a.shape
        elif self.op == Operation.M_RDS:
            if not a.is_matrix():
                raise ValueError("Operand is not a matrix")
            self.a = a
            self.b = None
            self.shape = ()
        elif self.op == Operation.M_TNP:
            if not a.is_matrix():
                raise ValueError("Operand is not a matrix")
            self.a = a
            self.b = None
            self.shape = (self.a.shape[1], self.a.shape[0])
        else:
            raise ValueError("Something has gone horribly wrong!")

    def __str__(self):
        if self.a is not None:
            return f"Placeholder(\"{specific_names[int(self.op)]}\" shape={self.shape})"
        else:
            return f"Variable(\"{self.value}\")"

    def is_scalar(self):
        return self.shape == ()

    def is_matrix(self):
        return not self.is_scalar()

    def eval(self):
        if self.op is None:
            return self.value
        elif self.op == Operation.S_ADD:
            self.value = self.a.eval() + self.b.eval()
        elif self.op == Operation.S_SUB:
            self.value = self.a.eval() - self.b.eval()
        elif self.op == Operation.S_MUL:
            self.value = self.a.eval() * self.b.eval()
        elif self.op == Operation.S_DIV:
            self.value = self.a.eval() / self.b.eval()
        elif self.op == Operation.S_SQR:
            self.value = self.a.eval() * self.a.eval()
        elif self.op == Operation.M_ADD:
            self.value = self.a.eval() * self.b.eval()
        elif self.op == Operation.M_SUB:
            self.value = self.a.eval() - self.b.eval()
        elif self.op == Operation.M_ELM:
            self.value = self.a.eval() * self.b.eval()
        elif self.op == Operation.M_ELD:
            self.value = self.a.eval() / self.b.eval()
        elif self.op == Operation.M_ESQ:
            self.value = np.square(self.a.eval())
        elif self.op == Operation.M_MUL:
            self.value = np.matmul(self.a.eval(), self.b.eval())
        elif self.op == Operation.M_RDS:
            self.value = np.sum(self.a.eval())
        elif self.op == Operation.M_TNP:
            self.value = self.a.eval().T
        else:
            print("something has gone horribly wrong")
        return self.value

    def differentiate(self):
        if self.op is None:
            return None
        elif self.op == Operation.S_ADD:
            self.a.grad = self.grad * np.float32(1.)
            self.b.grad = self.grad * np.float32(1.)
        elif self.op == Operation.S_SUB:
            self.a.grad = self.grad * np.float32(1.)
            self.b.grad = self.grad * np.float32(-1.)
        elif self.op == Operation.S_MUL:
            self.a.grad = self.grad * self.b.value
            self.b.grad = self.grad * self.a.value
        elif self.op == Operation.S_DIV:
            self.a.grad = self.grad / self.b.value
            self.b.grad = -self.grad * self.a.value / (self.b.value * self.b.value)
        elif self.op == Operation.S_SQR:
            self.a.grad = 2 * self.grad * self.a.value
        elif self.op == Operation.M_ADD:
            self.a.grad = self.grad * np.ones_like(self.a.value)
            self.b.grad = self.grad * np.ones_like(self.a.value)
        elif self.op == Operation.M_SUB:
            self.a.grad = self.grad * np.ones_like(self.a.value)
            self.b.grad = - self.grad * np.ones_like(self.a.value)
        elif self.op == Operation.M_ELM:
            self.a.grad = self.grad * self.b.value
            self.b.grad = self.grad * self.a.value
        elif self.op == Operation.M_ELD:
            self.a.grad = self.grad / self.b.value
            self.b.grad = -self.grad * self.a.value / (self.b.value * self.b.value)
        elif self.op == Operation.M_ESQ:
            self.a.grad = 2 * self.grad * self.a.value
        elif self.op == Operation.M_MUL:
            diag_d_du = np.diag(self.grad)
            # print(self.grad)
            # print(diag_d_du, np.tile(self.b.value, (self.a.value.shape[0], 1)))
            # self.a.grad = np.matmul(diag_d_du, np.tile(self.b.value, (self.a.value.shape[0], 1)))
            self.a.grad = np.matmul(np.tile(self.b.value, (self.a.value.shape[0], 1)), diag_d_du)
            self.b.grad = np.matmul(self.a.value.T, self.grad)
        elif self.op == Operation.M_RDS:
            # print(self.a)
            self.a.grad = self.grad * np.ones_like(self.a.value)
        elif self.op == Operation.M_TNP:
            self.a.grad = self.grad.T
        else:
            print("something has gone horribly wrong")

    def s_or_ele_ops(self, other, op):
        if not type(other) == Variable:
            raise TypeError(f"Cannot {names[int(op)]} Variable to something else")
        if self.is_scalar():
            if not other.is_scalar():
                raise ValueError(f"Cannot {names[int(op)]} matrix to scalar")
            return Variable(names_to_ops[op][0], a=self, b=other, trainable=False)
        else:
            if other.is_scalar():
                raise ValueError(f"Cannot {names[int(op)]} scalar to matrix")
            if not self.shape == other.shape:
                raise ValueError(f"Cannot {names[int(op)]} matrices of different shapes")
            return Variable(names_to_ops[op][1], a=self, b=other, trainable=False)

    def __add__(self, other):
        return self.s_or_ele_ops(other, 'add')

    def __sub__(self, other):
        return self.s_or_ele_ops(other, 'subtract')

    def __mul__(self, other):
        return self.s_or_ele_ops(other, 'multiply')

    def __truediv__(self, other):
        return self.s_or_ele_ops(other, 'divide')

    def __matmul__(self, other):
        return Variable(Operation.M_MUL, a=self, b=other, trainable=False)

def square(x: Variable):
    if x.is_scalar():
        return Variable(Operation.S_SQR, a=x, trainable=False)
    else:
        return Variable(Operation.M_ESQ, a=x, trainable=False)

def reduce_sum(x: Variable):
    if x.is_scalar():
        return x
    else:
        return Variable(Operation.M_RDS, a=x, trainable=False)

def matmul(x1: Variable, x2: Variable):
    return x1 @ x2

def transpose(x: Variable):
    return Variable(Operation.M_TNP, a=x, trainable=False)

def MSE(y: Variable, y_: Variable):
    if not (y.shape == y_.shape):
        raise ValueError("Arguments must have same shape")

    return reduce_sum(square(y - y_))

def backprop(loss, learning_rate):
    queue = [loss]
    loss.grad = 1.
    while queue:
        cur = queue.pop()
        if cur.trainable:
            cur.value -= learning_rate * cur.grad
        cur.differentiate()
        if cur.a is not None:
            queue.append(cur.a)
        if cur.b is not None:
            queue.append(cur.b)

def training_loop():
    X = np.ones((100, 5), dtype=np.float32)
    Y = np.zeros((100, 1), dtype=np.float32)

    W = Variable(value=np.random.rand(Y.shape[1], X.shape[1]))
    b = Variable(value=np.random.rand(1, 1))

    epochs = 10

    loss_history = np.empty(epochs * 100)
    pred_history = np.empty(epochs * 100)
    c = 0

    for epoch in range(epochs):
        for example, output in zip(X, Y):
            out = matmul(W, transpose(Variable(value=example[np.newaxis], trainable=False))) + b
            loss = MSE(out, Variable(value=output[np.newaxis], trainable=False))
            loss_history[c] = loss.eval()
            pred_history[c] = out.value[0][0]
            c += 1
            backprop(loss, 0.01)

    plt.plot(np.arange(loss_history.shape[0]), loss_history)#title='loss history')
    plt.figure()
    plt.plot(np.arange(loss_history.shape[0]), pred_history)# title='prediction history (ground truth=0')
    plt.show()

def main():
    training_loop()

if __name__ == "__main__":
    main()


from typing import Tuple, List, Dict, Optional, Union
from pathlib import Path, PosixPath
import math
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
from scipy.signal import savgol_filter

def process_all_dlc_tracking_h5s_with_default_settings(input_directory_with_dlc_tracking_files: Path,
                                                       week_id: int,
                                                       output_directory_to_save_results: Path) -> None:
    filepaths_dlc_unfiltered_h5_trackings = []
    for filepath in input_directory_with_dlc_tracking_files.iterdir():
        if filepath.name.endswith('.h5'):
            if 'filtered' not in filepath.name:
                filepaths_dlc_unfiltered_h5_trackings.append(filepath)
    for filepath in tqdm(filepaths_dlc_unfiltered_h5_trackings):
        recording = RecordingTop(filepath = filepath, week_id = week_id)
        if recording.df_successfully_loaded:
            recording.preprocess()
            if recording.logs['coverage_critical_markers'] >= recording.logs['coverage_threshold']:
                recording.run_behavioral_analyses()
                recording.export_results(out_dir_path = output_directory_to_save_results)
                recording.inspect_processing()

class EventBout2D():
    """
    Class, that contains start_index, end_index, duration and position of an event.
    It doesn't differ to class EventBout for now, so for the future this classes could be merged.

    Attributes:
        self.start_index(int): index of event onset
        self.end_index(int): index of event ending
    """

    def __init__(self, event_id: int, start_idx: int, end_idx: int, fps: int, event_type: str) -> None:
        """
        Constructor of class EventBout that sets the attributes start_ and end_index.

        Parameters:
            start_index(int): index of event onset
            end_index(Optional[int]): index of event ending (if event is not only a single frame)
        """
        self.id = event_id
        self.event_type = event_type
        self.start_idx = start_idx
        self.end_idx = end_idx
        self.duration = ((self.end_idx + 1) - self.start_idx)/fps

class RecordingTop():
    """
    Class for Analysing 2D-Position Data of mice in the OpeningTrack.

    Attributes:
        full_df_from_file(pandas.DataFrame): the Dataframe containing all bodyparts with x, y-coordinates and likelihood as returned by DLC
        fps(int): fps of the recording
        metadata(Dict): dictionary containing information read from the filename, such as animal_id, recording_date and Opening Track paradigm
    """

    @property
    def valid_paradigms(self) -> List[str]:
        return ['OTR', 'OTT', 'OTE']

    @property
    def valid_mouse_lines(self) -> List[str]:
        return ['194', '195', '196', '206', '209']

    @property
    def valid_recording_weeks(self) -> List[int]:
        return [1, 4, 8, 12, 14]

    @property
    def marker_ids_to_exclude_for_smoothing_and_interpolation(self) -> List[str]:
        return ['LED5', 'MazeCornerClosedRight', 'MazeCornerClosedLeft', 'MazeCornerOpenRight', 'MazeCornerOpenLeft']

    def __init__(self, filepath: Path, week_id: int)->None:
        """
        Constructor for the Recording2D class.

        This function calls functions to get the Dataframe from the csv, that is given as filepath argument and extracts and checks metadata from the filename.

        Parameters:
            filepath(pathlib.Path): the filepath to the h5 or csv containing DLC data
            week_id(int): the experimental week in which the recording was performed (1, 4, 8, 12, or 14)
        """
        assert type(filepath) == PosixPath, '"filepath" has to be a pathlib.Path object'
        assert week_id in self.valid_recording_weeks, f'"week_id" = {week_id} is not listed in "valid_recording_weeks": {self.valid_recording_weeks}'
        self.filepath = filepath
        self.week_id = week_id
        self.df_successfully_loaded, self.full_df_from_file = self._attempt_loading_df_from_file(filepath = filepath)
        if self.df_successfully_loaded:
            self.fps = self._get_correct_fps()
            self.framerate = 1/self.fps
            self.metadata = self._retrieve_metadata(filename = filepath.name)

    def _attempt_loading_df_from_file(self, filepath: Path) -> Tuple[bool, Optional[pd.DataFrame]]:
        """
        Reads the Dataframe from the h5-file and drops irrelevant columns and rows.

        Parameters:
            filepath(pathlib.Path): the path linked to the h5.file
        Returns:
            pandas.DataFrame: the Dataframe containing all bodyparts with x, y-coordinates and likelihood as returned by DLC
        """
        assert filepath.name.endswith('.csv') or filepath.name.endswith('.h5'), 'The filepath you specified is not referring to a .csv or a .h5 file!'
        try:
            if filepath.name.endswith('.csv'):
                df = pd.read_csv(filepath, low_memory = False)
                df = df.drop('scorer', axis=1)
                df.columns = df.iloc[0, :]+ '_' + df.iloc[1, :]
                df = df.drop([0, 1], axis=0)
                df = df.reset_index()
                df = df.drop('index', axis=1)
                df = df.astype(float)
            else:
                df = pd.read_hdf(filepath)
                target_column_names = []
                for marker_id, data_id in zip(df.columns.get_level_values(1), df.columns.get_level_values(2)):
                    target_column_names.append(f'{marker_id}_{data_id}')
                df.columns = target_column_names
                df = df.astype(float)
            successfully_loaded = True
        except:
            successfully_loaded = False
            df = None
        return successfully_loaded, df

    def _get_correct_fps(self) -> int:
        if self.full_df_from_file.shape[0] > 25_000:
            fps = 80
        else:
            fps = 30
        return fps

    def _retrieve_metadata(self, filename: str)->Dict:
        """
        Function, that slices the Filename to get the encoded metadata.

        Relying on file naming like this: 196_F7-27_220826_OTT_Bottom_synchronizedDLC_resnet152_OT_BottomCam_finalSep20shuffle1_550000filtered.h5

        Parameters:
            filepath(pathlib.Path): the path linked to the h5.file
        Returns:
            Dict: containing date of recording, animal_id and OT paradigm
        """
        filename_slices = filename.split('_')
        animal_line, animal_id, recording_date, paradigm, cam_id = filename_slices[0], filename_slices[1], filename_slices[2], filename_slices[3][0:3], 'Top'
        self._check_metadata(metadata = (animal_line, animal_id, recording_date, paradigm, cam_id))
        return {'recording_date': self.recording_date, 'animal': f'{self.mouse_line}_{self.mouse_id}', 'paradigm': self.paradigm, 'cam': self.cam_id}

    def _check_metadata(self, metadata = Tuple[str]) -> None:
        animal_line, animal_id, recording_date, paradigm, cam_id = metadata[0], metadata[1], metadata[2], metadata[3], metadata[4]
        self.cam_id = cam_id
        if animal_line not in self.valid_mouse_lines:
            while True:
                entered_input = input(f'Mouse line for {self.filepath}')
                if entered_input in self.valid_mouse_lines:
                    self.mouse_line = entered_input
                    break
                else:
                    print(f'Entered mouse line does not match any of the defined mouse lines. \nPlease enter one of the following lines: {self.valid_mouse_lines}')
        else:
            self.mouse_line = animal_line
        if not animal_id.startswith('F'):
            while True:
                entered_input = input(f'Mouse ID for {self.filepath}')
                if entered_input.startswith('F'):
                    self.mouse_id = entered_input
                    break
                else:
                    print(f'Animal ID has to start with F. Example: F2-14')
        else:
            self.mouse_id = animal_id
        if paradigm not in self.valid_paradigms:
            while True:
                entered_input = input(f'Paradigm for {self.filepath}')
                if entered_input in self.valid_paradigms:
                    self.paradigm = entered_input
                    break
                else:
                    print(f'Entered paradigm does not match any of the defined paradigms. \nPlease enter one of the following paradigms: {self.valid_paradigms}')
        else:
            self.paradigm = paradigm
        try:
            int(recording_date)
            self.recording_date = recording_date
        except:
            while True:
                entered_input = input(f'Recording date for {self.filepath}')
                try:
                    int(recording_date)
                    self.recording_date = recording_date
                    break
                except:
                    print(f'Entered recording date has to be an integer in shape YYMMDD. Example: 220812')

    def preprocess(self,
                   marker_ids_to_compute_coverage: List[str]=['TailBase', 'Snout'],
                   coverage_threshold: float=0.75,
                   max_seconds_to_interpolate: float=0.5,
                   likelihood_threshold: float=0.5,
                   marker_ids_to_compute_center_of_gravity: List[str]=['TailBase', 'Snout'],
                   relative_maze_normalization_error_tolerance: float=0.25
                   ) -> None:
        initial_logs_to_add = {'critical_markers_to_compute_coverage': marker_ids_to_compute_coverage,
                               'coverage_threshold': coverage_threshold,
                               'max_seconds_to_interpolate': max_seconds_to_interpolate,
                               'min_likelihood_threshold': likelihood_threshold,
                               'center_of_gravity_based_on': marker_ids_to_compute_center_of_gravity,
                               'relative_error_tolerance_corner_detection': relative_maze_normalization_error_tolerance}
        window_length = self._get_max_odd_n_frames_for_time_interval(fps = self.fps, time_interval = max_seconds_to_interpolate)
        marker_ids_to_preprocess = self._get_preprocessing_relevant_marker_ids(df = self.full_df_from_file)
        smoothed_df = self._smooth_tracked_coords_and_likelihood(marker_ids = marker_ids_to_preprocess, window_length = window_length, polyorder = 3)
        interpolated_df = self._interpolate_low_likelihood_intervals(df = smoothed_df, marker_ids = marker_ids_to_preprocess, max_interval_length = window_length)
        interpolated_df_with_cog = self._add_new_marker_derived_from_existing_markers(df = interpolated_df,
                                                                                existing_markers = marker_ids_to_compute_center_of_gravity,
                                                                                new_marker_id = 'CenterOfGravity',
                                                                                likelihood_threshold = likelihood_threshold)
        preprocessed_df = self._interpolate_low_likelihood_intervals(df = interpolated_df_with_cog,
                                                                     marker_ids = ['CenterOfGravity'],
                                                                     max_interval_length = window_length)
        coverage_critical_markers = self._compute_coverage(df = preprocessed_df,
                                                           critical_marker_ids = marker_ids_to_compute_coverage,
                                                           likelihood_threshold = likelihood_threshold)
        initial_logs_to_add['coverage_critical_markers'] = coverage_critical_markers
        self._add_to_logs(logs_to_add = initial_logs_to_add)
        if coverage_critical_markers >= coverage_threshold:
            normalization_params = self._get_parameters_to_normalize_maze_coordinates(df = preprocessed_df,
                                                                                relative_error_tolerance = relative_maze_normalization_error_tolerance)
            self.normalized_df = self._normalize_df(df = preprocessed_df, normalization_parameters = normalization_params)
            self.bodyparts = self._create_bodypart_objects()
            normalized_maze_corner_coords = self._get_normalized_maze_corners(normalization_parameters = normalization_params)
            coverage_center_of_gravity = self._compute_coverage(df = preprocessed_df,
                                                                critical_marker_ids = ['CenterOfGravity'],
                                                                likelihood_threshold = likelihood_threshold)
            additional_logs_to_add = {'coverage_CenterOfGravity': coverage_center_of_gravity}
            for key, value in normalization_params.items():
                additional_logs_to_add[key] = value
            for key, value in normalized_maze_corner_coords.items():
                additional_logs_to_add[f'normalized_{key}_coords'] = value
            self._add_to_logs(logs_to_add = additional_logs_to_add)

    def _add_to_logs(self, logs_to_add: Dict) -> None:
        if hasattr(self, 'logs') == False:
            self.logs = {}
        for key, value in logs_to_add.items():
            assert key not in self.logs.keys(), f'{key} is already in self.logs.keys - adding it would result in overwriting the previous entry; please ensure unique naming'
            self.logs[key] = value

    def _get_max_odd_n_frames_for_time_interval(self, fps: int, time_interval: 0.5) -> int:
        assert type(fps) == int, '"fps" has to be an integer!'
        frames_per_time_interval = fps * time_interval
        if frames_per_time_interval % 2 == 0:
            max_odd_frame_count = frames_per_time_interval - 1
        elif frames_per_time_interval == int(frames_per_time_interval):
            max_odd_frame_count = frames_per_time_interval
        else:
            frames_per_time_interval = int(frames_per_time_interval)
            if frames_per_time_interval % 2 == 0:
                max_odd_frame_count = frames_per_time_interval - 1
            else:
                max_odd_frame_count = frames_per_time_interval
        assert max_odd_frame_count > 0, f'The specified time interval is too short to fit an odd number of frames'
        return int(max_odd_frame_count)

    def _get_preprocessing_relevant_marker_ids(self, df: pd.DataFrame) -> List[str]:
        all_marker_ids = self._get_all_unique_marker_ids(df = df)
        relevant_marker_ids = all_marker_ids
        for marker_id_to_exclude in self.marker_ids_to_exclude_for_smoothing_and_interpolation:
            relevant_marker_ids.remove(marker_id_to_exclude)
        return relevant_marker_ids

    def _get_all_unique_marker_ids(self, df: pd.DataFrame) -> List[str]:
        unique_marker_ids = []
        for column_name in df.columns:
            marker_id, _ = column_name.split('_')
            if marker_id not in unique_marker_ids:
                unique_marker_ids.append(marker_id)
        return unique_marker_ids

    def _smooth_tracked_coords_and_likelihood(self, marker_ids: List[str], window_length: int, polyorder: int=3) -> pd.DataFrame:
        """
        Smoothes the DataFrame basically using the implementation from DLC2kinematics:
        https://github.com/AdaptiveMotorControlLab/DLC2Kinematics/blob/82e7e60e00e0efb3c51e024c05a5640c91032026/src/dlc2kinematics/preprocess.py#L64
        However, with one key change: likelihoods will also be smoothed.
        In addition, we will not smooth the columns for the tracked LEDs and the MazeCorners.

        Note: window_length has to be an odd integer!
        """
        smoothed_df = self.full_df_from_file.copy()
        column_names = self._get_column_names(df = smoothed_df,
                                              column_identifiers = ['x', 'y', 'likelihood'],
                                              marker_ids = marker_ids)
        column_idxs_to_smooth = smoothed_df.columns.get_indexer(column_names)
        smoothed_df.iloc[:, column_idxs_to_smooth] = savgol_filter(x = smoothed_df.iloc[:, column_idxs_to_smooth],
                                                                   window_length = window_length,
                                                                   polyorder = polyorder,
                                                                   axis = 0)
        return smoothed_df

    def _get_column_names(self, df: pd.DataFrame, column_identifiers: List[str], marker_ids: List[str]) -> List[str]:
        matching_column_names = []
        for column_name in df.columns:
            marker_id, column_identifier = column_name.split('_')
            if (marker_id in marker_ids) and (column_identifier in column_identifiers):
                matching_column_names.append(column_name)
        return matching_column_names

    def _interpolate_low_likelihood_intervals(self, df: pd.DataFrame, marker_ids: List[str], max_interval_length: int) -> pd.DataFrame:
        interpolated_df = df.copy()
        for marker_id in marker_ids:
            low_likelihood_interval_border_idxs = self._get_low_likelihood_interval_border_idxs(likelihood_series = interpolated_df[f'{marker_id}_likelihood'],
                                                                                max_interval_length = max_interval_length)
            for start_idx, end_idx in low_likelihood_interval_border_idxs:
                if (start_idx - 1 >= 0) and (end_idx + 2 < interpolated_df.shape[0]):
                    interpolated_df[f'{marker_id}_x'][start_idx - 1 : end_idx + 2] = interpolated_df[f'{marker_id}_x'][start_idx - 1 : end_idx + 2].interpolate()
                    interpolated_df[f'{marker_id}_y'][start_idx - 1 : end_idx + 2] = interpolated_df[f'{marker_id}_y'][start_idx - 1 : end_idx + 2].interpolate()
                    interpolated_df[f'{marker_id}_likelihood'][start_idx : end_idx + 1] = 0.5
        return interpolated_df

    def _get_low_likelihood_interval_border_idxs(self, likelihood_series: pd.Series, max_interval_length: int, min_likelihood_threshold: float=0.5) -> List[Tuple[int, int]]:
        all_low_likelihood_idxs = np.where(likelihood_series.values < min_likelihood_threshold)[0]
        short_low_likelihood_interval_border_idxs = self._get_interval_border_idxs(all_matching_idxs = all_low_likelihood_idxs,
                                                                                max_interval_duration = max_interval_length*self.framerate)
        return short_low_likelihood_interval_border_idxs

    def _get_interval_border_idxs(self,
                                  all_matching_idxs: np.ndarray,
                                  min_interval_duration: Optional[float]=None,
                                  max_interval_duration: Optional[float]=None,
                                 ) -> List[Tuple[int, int]]:
        interval_border_idxs = []
        if all_matching_idxs.shape[0] >= 1:
            step_idxs = np.where(np.diff(all_matching_idxs) > 1)[0]
            step_end_idxs = np.concatenate([step_idxs, np.array([all_matching_idxs.shape[0] - 1])])
            step_start_idxs = np.concatenate([np.array([0]), step_idxs + 1])
            interval_start_idxs = all_matching_idxs[step_start_idxs]
            interval_end_idxs = all_matching_idxs[step_end_idxs]
            for start_idx, end_idx in zip(interval_start_idxs, interval_end_idxs):
                interval_frame_count = (end_idx+1) - start_idx
                interval_duration = interval_frame_count * self.framerate
                if (min_interval_duration != None) and (max_interval_duration != None):
                    append_interval = min_interval_duration <= interval_duration <= max_interval_duration
                elif min_interval_duration != None:
                    append_interval = min_interval_duration <= interval_duration
                elif max_interval_duration != None:
                    append_interval = interval_duration <= max_interval_duration
                else:
                    append_interval = True
                if append_interval:
                    interval_border_idxs.append((start_idx, end_idx))
        return interval_border_idxs

    def _add_new_marker_derived_from_existing_markers(self, df: pd.DataFrame, existing_markers: List[str], new_marker_id: str, likelihood_threshold: float = 0.5)->None:
        df_with_new_marker = df.copy()
        for coordinate in ['x', 'y']:
            df_with_new_marker[f'{new_marker_id}_{coordinate}'] = (sum([df_with_new_marker[f'{marker_id}_{coordinate}'] for marker_id in existing_markers]))/len(existing_markers)
        df_with_new_marker[f'{new_marker_id}_likelihood'] = 0
        row_idxs_where_all_likelihoods_exceeded_threshold = self._get_idxs_where_all_markers_exceed_likelihood(df = df_with_new_marker,
                                                                                marker_ids = existing_markers,
                                                                                likelihood_threshold = 0.5)
        df_with_new_marker.iloc[row_idxs_where_all_likelihoods_exceeded_threshold, -1] = 1
        return df_with_new_marker

    def _get_idxs_where_all_markers_exceed_likelihood( self, df: pd.DataFrame, marker_ids: List[str], likelihood_threshold: float=0.5) -> np.ndarray:
        valid_idxs_per_marker_id = []
        for marker_id in marker_ids:
            valid_idxs_per_marker_id.append(df.loc[df[f'{marker_id}_likelihood'] >= likelihood_threshold].index.values)
        shared_valid_idxs_for_all_markers = valid_idxs_per_marker_id[0]
        if len(valid_idxs_per_marker_id) > 1:
            for i in range(1, len(valid_idxs_per_marker_id)):
                shared_valid_idxs_for_all_markers = np.intersect1d(shared_valid_idxs_for_all_markers, valid_idxs_per_marker_id[i])
        return shared_valid_idxs_for_all_markers

    def _compute_coverage(self, df: pd.DataFrame, critical_marker_ids: List[str], likelihood_threshold: float=0.5) -> float:
        idxs_where_all_markers_exceed_likelihood_threshold = self._get_idxs_where_all_markers_exceed_likelihood(df = df,
                                                                                marker_ids = critical_marker_ids,
                                                                                likelihood_threshold = likelihood_threshold)
        return idxs_where_all_markers_exceed_likelihood_threshold.shape[0] / df.shape[0]

    def _get_parameters_to_normalize_maze_coordinates(self, df: pd.DataFrame, relative_error_tolerance: float) -> Dict:
        corners = self._get_corner_coords_with_likelihoods(df = df)
        translation_vector = self._get_translation_vector(coords_closed_left = corners['MazeCornerClosedLeft']['coords'])
        best_result = self._evaluate_maze_shape_using_open_corners(corners_and_likelihoods = corners, tolerance = relative_error_tolerance)
        if best_result['valid']:
            side_id = best_result['side_id']
            logs_to_add = {'maze_normalization_based_on': f'MazeCornerClosed{side_id}_and_MazeCornerOpen{side_id}'}
            conversion_factor = self._get_conversion_factor_px_to_cm(coords_point_a = corners[f'MazeCornerClosed{side_id}']['coords'],
                                                                     coords_point_b = corners[f'MazeCornerOpen{side_id}']['coords'],
                                                                     distance_in_cm = 50)
            rotation_angle = self._get_rotation_angle_with_open_corner(corners = corners,
                                                                       side_id = best_result['side_id'],
                                                                       translation_vector = translation_vector,
                                                                       conversion_factor = conversion_factor)
        else:
            logs_to_add = {'maze_normalization_based_on': f'MazeCornerClosedRight_and_MazeCornerClosedLeft'}
            conversion_factor = self._get_conversion_factor_px_to_cm(coords_point_a = corners['MazeCornerClosedLeft']['coords'],
                                                                     coords_point_b = corners['MazeCornerClosedRight']['coords'],
                                                                     distance_in_cm = 4)
            rotation_angle = self._get_rotation_angle_with_closed_corners_only(corners = corners,
                                                                               translation_vector = translation_vector,
                                                                               conversion_factor = conversion_factor)
        self._add_to_logs(logs_to_add = logs_to_add)
        return {'translation_vector': translation_vector, 'rotation_angle': rotation_angle, 'conversion_factor': conversion_factor}

    def _get_corner_coords_with_likelihoods(self, df: pd.DataFrame) -> Dict:
        corner_coords_with_likelihood = {}
        for corner_marker_id in ['MazeCornerClosedRight', 'MazeCornerClosedLeft', 'MazeCornerOpenRight', 'MazeCornerOpenLeft']:
            xy_coords, min_likelihood = self._get_most_reliable_marker_position_with_likelihood(df = df, marker_id = corner_marker_id)
            corner_coords_with_likelihood[corner_marker_id] = {'coords': xy_coords, 'min_likelihood': min_likelihood}
        return corner_coords_with_likelihood

    def _get_most_reliable_marker_position_with_likelihood(self, df: pd.DataFrame, marker_id: str, percentile: float=99.95) -> Tuple[np.array, float]:
        likelihood_threshold = np.nanpercentile(df[f'{marker_id}_likelihood'].values, percentile)
        df_most_reliable_frames = df.loc[df[f'{marker_id}_likelihood'] >= likelihood_threshold].copy()
        most_reliable_x, most_reliable_y = df_most_reliable_frames[f'{marker_id}_x'].median(), df_most_reliable_frames[f'{marker_id}_y'].median()
        return np.array([most_reliable_x, most_reliable_y]), likelihood_threshold

    def _get_translation_vector(self, coords_closed_left: np.ndarray) -> np.ndarray:
        """
        Function that calculates the offset of the left closed mazecorner to (0, 0).

        Parameters:
            coords_closed_right (np.ndarray): containing the coordinat (i.e. vector) of the right closed maze corner that shall become 0/0

        Returns:
            translation_vector(np.array): vector with offset in each dimension
        """
        return -coords_closed_left

    def _evaluate_maze_shape_using_open_corners(self, corners_and_likelihoods: Dict, tolerance: float) -> Dict:
        best_result = {'valid': False, 'mean_error': tolerance + 1, 'open_corner_id': None, 'side_id': None}
        for open_corner_marker_id in [corner_marker_id for corner_marker_id in corners_and_likelihoods.keys() if 'Open' in corner_marker_id]:
            valid_positions = False
            side_id = open_corner_marker_id[open_corner_marker_id.find('Open') + 4:]
            if side_id == 'Left': opposite_side_id = 'Right'
            else: opposite_side_id = 'Left'
            closed_corner_opposite_side = f'MazeCornerClosed{opposite_side_id}'
            angle_error = self._compute_angle_error(a = corners_and_likelihoods[f'MazeCornerClosed{opposite_side_id}']['coords'],
                                              b = corners_and_likelihoods[f'MazeCornerClosed{side_id}']['coords'],
                                              c = corners_and_likelihoods[open_corner_marker_id]['coords'])
            distance_ratio_error = self._compute_distance_ratio_error(corners_and_likelihoods = corners_and_likelihoods,
                                                                open_corner_marker_id = open_corner_marker_id,
                                                                side_id = side_id)
            if (angle_error <= tolerance) & (distance_ratio_error <= tolerance):
                valid_positions = True
            mean_error = (angle_error + distance_ratio_error) / 2
            if mean_error < best_result['mean_error']:
                best_result['valid'] = valid_positions
                best_result['mean_error'] = mean_error
                best_result['open_corner_id'] = open_corner_marker_id
                best_result['side_id'] = side_id
        return best_result

    def _compute_angle_error(self, a: np.ndarray, b: np.ndarray, c: np.ndarray) -> float:
        # b is point at the joint that connects the other two
        ba = a - b
        bc = c - b
        cosine_angle = np.dot(ba, bc) / (np.linalg.norm(ba) * np.linalg.norm(bc))
        angle = np.degrees(np.arccos(cosine_angle))
        return self._compute_error_proportion(query_value = angle, target_value = 90)

    def _compute_error_proportion(self, query_value: float, target_value: float) -> float:
        return abs(query_value - target_value) / target_value

    def _compute_distance_ratio_error(self, corners_and_likelihoods: Dict, open_corner_marker_id: str, side_id: str) -> float:
        maze_width = self._get_distance_between_two_points(corners_and_likelihoods['MazeCornerClosedLeft']['coords'],
                                                           corners_and_likelihoods['MazeCornerClosedRight']['coords'])
        maze_length = self._get_distance_between_two_points(corners_and_likelihoods[f'MazeCornerClosed{side_id}']['coords'],
                                                           corners_and_likelihoods[open_corner_marker_id]['coords'])
        distance_ratio = maze_length/maze_width
        return self._compute_error_proportion(query_value = distance_ratio, target_value = 50/4)

    def _get_distance_between_two_points(self, coords_point_a: np.ndarray, coords_point_b: np.ndarray) -> float:
        return ((coords_point_a[0] - coords_point_b[0])**2 + (coords_point_a[1] - coords_point_b[1])**2)**0.5

    def _get_conversion_factor_px_to_cm(self, coords_point_a: np.ndarray, coords_point_b: np.ndarray, distance_in_cm: float) -> float:
        distance = self._get_distance_between_two_points(coords_point_a, coords_point_b)
        return distance_in_cm / distance

    def _get_rotation_angle_with_open_corner(self, corners: Dict, side_id: str, translation_vector: np.ndarray, conversion_factor: float) -> float:
        """
        Function, that calculates the rotation angle of the maze considering the best matching open corner
        and the corresponding closed corner on the same side.

        Returns:
            float: angle in radians
        """
        if side_id == 'Left':
            side_specific_y = 0
        else:
            side_specific_y = 4
        translated_closed_corner = corners[f'MazeCornerClosed{side_id}']['coords'] + translation_vector
        translated_open_corner = corners[f'MazeCornerOpen{side_id}']['coords'] + translation_vector
        target_rotated_open_corner = np.asarray([50 / conversion_factor, side_specific_y / conversion_factor])
        length_a = self._get_distance_between_two_points(translated_open_corner, target_rotated_open_corner) * conversion_factor
        length_b = self._get_distance_between_two_points(translated_open_corner, translated_closed_corner) * conversion_factor
        length_c = 50
        angle = math.acos((length_b**2 + length_c**2 - length_a**2) / (2 * length_b * length_c))
        return angle

    def _get_rotation_angle_with_closed_corners_only(self, corners: Dict, translation_vector: np.ndarray, conversion_factor: float) -> float:
        translated_closed_left = corners['MazeCornerClosedLeft']['coords'] + translation_vector
        translated_closed_right = corners['MazeCornerClosedRight']['coords'] + translation_vector
        target_rotated_closed_right = np.asarray([0, 4 / conversion_factor])

        length_a = self._get_distance_between_two_points(translated_closed_right, target_rotated_closed_right) * conversion_factor
        length_b = self._get_distance_between_two_points(translated_closed_left, translated_closed_right) * conversion_factor
        length_c = 4
        angle = math.acos((length_b**2 + length_c**2 - length_a**2) / (2 * length_b * length_c))
        return angle

    def _normalize_df(self, df: pd.DataFrame, normalization_parameters)->None:
        unadjusted_df = df.copy()
        translated_df = self._translate_df(df = unadjusted_df, translation_vector = normalization_parameters['translation_vector'])
        rotated_and_translated_df = self._rotate_df(df = translated_df, rotation_angle = normalization_parameters['rotation_angle'])
        final_df = self._convert_df_to_cm(df = rotated_and_translated_df, conversion_factor = normalization_parameters['conversion_factor'])
        return final_df

    def _translate_df(self, df: pd.DataFrame, translation_vector: np.array) -> pd.DataFrame:
        """
        Function that translates the raw dataframe to the null space.

        Parameter:
            translation_vector(np.Array): vector with offset of xy to XY
        Returns:
            translated_df(pandas.DataFrame): the dataframe translated to (0, 0)
        """
        for marker_id in self._get_all_unique_marker_ids(df = df):
            df.loc[:, [f'{marker_id}_x', f'{marker_id}_y']] += translation_vector
        return df

    def _rotate_df(self, df: pd.DataFrame, rotation_angle: float) -> pd.DataFrame:
        """
        Function, that rotates the dataframe in 2D.

        Parameter:
            rotation_angle: angle in radians of rotation of the xy to XY coordinate system around the z-axis
            df(pandas.DataFrame): the dataframe that will be rotated.
        Returns:
            rotated_df(pandas.DataFrame): the rotated dataframe
        """
        df_rotated = df.copy()
        cos_theta, sin_theta = math.cos(rotation_angle), math.sin(rotation_angle)
        for marker_id in self._get_all_unique_marker_ids(df = df):
            df_rotated[f'{marker_id}_x'] = df[f'{marker_id}_x'] * cos_theta - df[f'{marker_id}_y']  * sin_theta
            df_rotated[f'{marker_id}_y'] = df[f'{marker_id}_x'] * sin_theta + df[f'{marker_id}_y']  * cos_theta
        return df_rotated

    def _convert_df_to_cm(self, df: pd.DataFrame, conversion_factor: float) -> pd.DataFrame:
        """
        The coordinates are converted to cm.

        Parameters:
            conversion_factor(float): factor to convert the unspecified unit into cm.
            df(pandas.DataFrame): dataframe with unspecified unit

        Returns:
            df(pandas.DataFrame): dataframe with values in cm
        """
        for marker_id in self._get_all_unique_marker_ids(df = df):
            df.loc[:, [f'{marker_id}_x', f'{marker_id}_y']] *= conversion_factor
        return df

    def _create_bodypart_objects(self) -> Dict:
        bodyparts = {}
        for marker_id in self._get_all_unique_marker_ids(df = self.normalized_df):
            bodyparts[marker_id] = Bodypart2D(bodypart_id = marker_id, df = self.normalized_df, fps = self.fps)
        return bodyparts

    def _get_normalized_maze_corners(self, normalization_parameters: Dict) -> Dict:
        normalized_maze_corner_coordinates = {}
        corners = self._get_corner_coords_with_likelihoods(df = self.normalized_df)
        for corner_marker_id in corners.keys():
            normalized_maze_corner_coordinates[corner_marker_id] = corners[corner_marker_id]['coords']
        return normalized_maze_corner_coordinates

    def run_behavioral_analyses(self,
                                bodyparts_critical_for_freezing: List[str]=['Snout', 'CenterOfGravity'],
                                bodyparts_for_direction_front_to_back: List[str]=['Snout', 'CenterOfGravity'],
                                immobility_max_rolling_speed: float=2.0,
                                immobility_min_duration: float=0.1,
                                freezing_min_duration: float=0.5,
                                gait_min_rolling_speed: float=3.0,
                                gait_min_duration: float=0.5,
                                gait_disruption_max_time_to_immobility: float=0.15,
                                merge_events_max_inbetween_time: float=0.15,
                                bodyparts_to_include_in_behavior_df = ['CenterOfGravity', 'Snout', 'TailBase'],
                               ) -> None:
        # parameter selection for immobility speed threshold and freezing min duration based on:
        # https://www.sciencedirect.com/science/article/pii/S0960982216306182
        sliding_window_size = int(round(self._get_max_odd_n_frames_for_time_interval(fps = self.fps, time_interval = 0.5) / 2, 0))
        logs_to_add = {'bodyparts_checked_to_infer_immobility': bodyparts_critical_for_freezing,
                       'bodypart_used_to_identify_front': bodyparts_for_direction_front_to_back[0],
                       'bodypart_used_to_identify_back': bodyparts_for_direction_front_to_back[1],
                       'immobility_max_rolling_speed' : immobility_max_rolling_speed,
                       'immobility_min_duration': immobility_min_duration,
                       'freezing_min_duration': freezing_min_duration,
                       'gait_min_rolling_speed': gait_min_rolling_speed,
                       'gait_min_duration': gait_min_duration,
                       'gait_disruption_max_time_to_immobility': gait_disruption_max_time_to_immobility,
                       #'merge_events_max_inbetween_time': merge_events_max_inbetween_time,
                       'sliding_window_size_to_compute_speed': sliding_window_size}
        self._add_to_logs(logs_to_add = logs_to_add)
        for bodypart in self.bodyparts.values():
            bodypart.calculate_speed_and_identify_immobility(sliding_window_size = sliding_window_size, immobility_threshold = immobility_max_rolling_speed)
        self.behavior_df = self._create_behavior_df(bodyparts_to_include = bodyparts_to_include_in_behavior_df)
        self._add_orientation_to_behavior_df(bodyparts_for_direction_front_to_back = bodyparts_for_direction_front_to_back)
        self._add_immobility_based_on_several_bodyparts_to_behavior_df(bodyparts_critical_for_freezing = bodyparts_critical_for_freezing)
        immobility_events = self._get_immobility_related_events(min_interval_duration = immobility_min_duration, event_type = 'immobility_bout')
        self._add_event_bouts_to_behavior_df(event_type = 'immobility_bout', events = immobility_events)
        freezing_events = self._get_immobility_related_events(min_interval_duration = freezing_min_duration, event_type = 'freezing_bout')
        self._add_event_bouts_to_behavior_df(event_type = 'freezing_bout', events = freezing_events)
        gait_events = self._get_gait_events(gait_min_rolling_speed = gait_min_rolling_speed, gait_min_duration = gait_min_duration)
        self._add_event_bouts_to_behavior_df(event_type = 'gait_bout', events = gait_events)
        gait_disruption_events = self._get_gait_disruption_events(gait_events = gait_events,
                                                                  gait_disruption_max_time_to_immobility = gait_disruption_max_time_to_immobility)
        self._add_event_bouts_to_behavior_df(event_type = 'gait_disruption_bout', events = gait_disruption_events)

    def _create_behavior_df(self, bodyparts_to_include: List[str]) -> pd.DataFrame:
        column_names = self._get_column_names(df = self.normalized_df, column_identifiers = ['x', 'y', 'likelihood'], marker_ids = bodyparts_to_include)
        return self.normalized_df[column_names].copy()

    def _add_orientation_to_behavior_df(self, bodyparts_for_direction_front_to_back: List[str]) -> None:
        assert len(bodyparts_for_direction_front_to_back) ==2, '"bodyparts_for_direction_front_to_back" must be a list of exact 2 marker_ids!'
        front_marker_id = bodyparts_for_direction_front_to_back[0]
        back_marker_id = bodyparts_for_direction_front_to_back[1]
        self.behavior_df.loc[self.bodyparts[front_marker_id].df['x'] > self.bodyparts[back_marker_id].df['x'], 'facing_towards_open_end'] = True

    def _add_immobility_based_on_several_bodyparts_to_behavior_df(self, bodyparts_critical_for_freezing: List[str]) -> None:
        # very similar to
        # can they be combined? different operators for check, though
        valid_idxs_per_marker_id = []
        for bodypart_id in bodyparts_critical_for_freezing:
            tmp_df = self.bodyparts[bodypart_id].df.copy()
            valid_idxs_per_marker_id.append(tmp_df.loc[tmp_df['immobility'] == True].index.values)
        shared_valid_idxs_for_all_markers = valid_idxs_per_marker_id[0]
        if len(valid_idxs_per_marker_id) > 1:
            for next_set_of_valid_idxs in valid_idxs_per_marker_id[1:]:
                shared_valid_idxs_for_all_markers = np.intersect1d(shared_valid_idxs_for_all_markers, next_set_of_valid_idxs)
        self.behavior_df.loc[shared_valid_idxs_for_all_markers, 'immobility'] = True

    def _get_immobility_related_events(self, min_interval_duration: float, event_type: str) -> List[EventBout2D]:
        all_immobility_idxs = np.where(self.behavior_df['immobility'].values == True)[0]
        immobility_interval_border_idxs = self._get_interval_border_idxs(all_matching_idxs = all_immobility_idxs, min_interval_duration = min_interval_duration)
        immobility_related_events = self._create_event_objects(interval_border_idxs = immobility_interval_border_idxs, event_type = event_type)
        return immobility_related_events

    def _create_event_objects(self, interval_border_idxs: List[Tuple[int, int]], event_type: str) -> List[EventBout2D]:
        events = []
        event_id = 0
        for start_idx, end_idx in interval_border_idxs:
            single_event = EventBout2D(event_id = event_id, start_idx = start_idx, end_idx = end_idx, fps = self.fps, event_type = event_type)
            events.append(single_event)
            event_id += 1
        return events

    def _add_event_bouts_to_behavior_df(self, event_type: str, events: List[EventBout2D]) -> None:
        assert event_type not in list(self.behavior_df.columns), f'{event_type} was already a column in self.behavior_df!'
        self.behavior_df[event_type] = np.nan
        self.behavior_df[f'{event_type}_id'] = np.nan
        self.behavior_df[f'{event_type}_duration'] = np.nan
        if len(events) > 0:
            for event_bout in events:
                assert event_bout.event_type == event_type, f'Event types didn´t match! Expected {event_type} but found {event_bout.event_type}.'
                self.behavior_df.iloc[event_bout.start_idx : event_bout.end_idx + 1, -3] = True
                self.behavior_df.iloc[event_bout.start_idx : event_bout.end_idx + 1, -2] = event_bout.id
                self.behavior_df.iloc[event_bout.start_idx : event_bout.end_idx + 1, -1] = event_bout.duration

    def _get_gait_events(self, gait_min_rolling_speed: float, gait_min_duration: float) -> List[EventBout2D]:
        idxs_with_sufficient_speed = np.where(self.bodyparts['CenterOfGravity'].df['rolling_speed_cm_per_s'].values >= gait_min_rolling_speed)[0]
        gait_interval_border_idxs = self._get_interval_border_idxs(all_matching_idxs = idxs_with_sufficient_speed, min_interval_duration = gait_min_duration)
        gait_events = self._create_event_objects(interval_border_idxs = gait_interval_border_idxs, event_type = 'gait_bout')
        return gait_events

    def _get_gait_disruption_events(self, gait_events: List[EventBout2D], gait_disruption_max_time_to_immobility: float) -> List[EventBout2D]:
        n_frames_max_distance = int(gait_disruption_max_time_to_immobility * self.fps)
        gait_disruption_interval_border_idxs = []
        for gait_bout in gait_events:
            end_idx = gait_bout.end_idx
            unique_immobility_bout_values = self.behavior_df.loc[end_idx : end_idx + n_frames_max_distance + 1, 'immobility_bout'].unique()
            if True in unique_immobility_bout_values:
                closest_immobility_bout_id = self.behavior_df.loc[end_idx : end_idx + n_frames_max_distance + 1, 'immobility_bout_id'].dropna().unique().min()
                immobility_interval_border_idxs = self._get_interval_border_idxs_from_event_type_and_id(event_type = 'immobility_bout', event_id = closest_immobility_bout_id)
                gait_disruption_interval_border_idxs.append(immobility_interval_border_idxs)
        gait_disruption_events = self._create_event_objects(interval_border_idxs = gait_disruption_interval_border_idxs, event_type = 'gait_disruption_bout')
        return gait_disruption_events

    def _get_interval_border_idxs_from_event_type_and_id(self, event_type: str, event_id: int) -> Tuple[int, int]:
        interval_idxs = self.behavior_df.loc[self.behavior_df[f'{event_type}_id'] == event_id].index.values
        return interval_idxs[0], interval_idxs[-1]

    def export_results(self, out_dir_path: Path) -> None:
        dfs_to_export = {'immobility_bouts': self._export_immobility_related_bouts(df = self.behavior_df, event_type = 'immobility_bout'),
                         'freezing_bouts': self._export_immobility_related_bouts(df = self.behavior_df, event_type = 'freezing_bout'),
                         'gait_disruption_bouts': self._export_immobility_related_bouts(df = self.behavior_df, event_type = 'gait_disruption_bout'),
                         'gait_bouts': self._export_gait_related_bouts(df = self.behavior_df, event_type = 'gait_bout')}
        dfs_to_export['session_overview'] = self._create_session_overview_df(dfs_to_export_with_individual_bout_dfs = dfs_to_export)
        dfs_to_export['parameter_settings'] = self._create_parameter_settings_df()
        self.base_output_filepath = out_dir_path.joinpath(f'{self.metadata["animal"]}_{self.metadata["paradigm"]}_week-{self.week_id}')
        self._write_xlsx_file_to_disk(dfs_to_export = dfs_to_export)

    def _export_immobility_related_bouts(self, df: pd.DataFrame, event_type: str) -> pd.DataFrame:
        results_per_event = {'bout_id': [],
                            'duration': [],
                            'CenterOfGravity_x_at_bout_start': [],
                            'towards_open_at_bout_start': [],
                            'distance_covered_cm': [],
                            'start_time': [],
                            'end_time': []}
        results_per_event['bout_id'] = self._get_all_bout_ids(df = df, event_type = event_type)
        if len(results_per_event['bout_id']) >= 1:
            results_per_event['duration'] = self._get_bout_duration_per_bout_id(df = df, event_type = event_type, event_ids = results_per_event['bout_id'])
            x_positions_center_of_gravity_at_interval_borders = self._get_column_values_at_event_borders(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                column_name = 'CenterOfGravity_x')
            results_per_event['CenterOfGravity_x_at_bout_start'] = x_positions_center_of_gravity_at_interval_borders[:, 0]
            direction_towards_open_at_interval_borders = self._get_column_values_at_event_borders(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                column_name = 'facing_towards_open_end')
            results_per_event['towards_open_at_bout_start'] = direction_towards_open_at_interval_borders[:, 0]
            results_per_event['distance_covered_cm'] = self._get_distance_covered_per_event(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                marker_id = 'CenterOfGravity')
            bout_start_and_end_idxs = self._get_interval_start_and_end_idxs_per_event(df = df, event_type = event_type, event_ids = results_per_event['bout_id'])
            results_per_event['start_time'] = bout_start_and_end_idxs[:, 0]
            results_per_event['end_time'] = bout_start_and_end_idxs[:, 1]
        return pd.DataFrame(data = results_per_event)

    def _export_gait_related_bouts(self, df: pd.DataFrame, event_type: str) -> pd.DataFrame:
        results_per_event = {'bout_id': [],
                            'duration': [],
                            'CenterOfGravity_x_at_bout_end': [],
                            'towards_open_at_bout_end': [],
                            #'mean_rolling_speed_cm_per_s': [],
                            'distance_covered_cm': [],
                            'start_time': [],
                            'end_time': []}
        results_per_event['bout_id'] = self._get_all_bout_ids(df = df, event_type = event_type)
        if len(results_per_event['bout_id']) >= 1:
            results_per_event['duration'] = self._get_bout_duration_per_bout_id(df = df, event_type = event_type, event_ids = results_per_event['bout_id'])
            x_positions_center_of_gravity_at_interval_borders = self._get_column_values_at_event_borders(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                column_name = 'CenterOfGravity_x')
            results_per_event['CenterOfGravity_x_at_bout_end'] = x_positions_center_of_gravity_at_interval_borders[:, 1]
            direction_towards_open_at_interval_borders = self._get_column_values_at_event_borders(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                column_name = 'facing_towards_open_end')
            results_per_event['towards_open_at_bout_end'] = direction_towards_open_at_interval_borders[:, 1]
            """
            results_per_event['mean_rolling_speed_cm_per_s'] = self._get_mean_column_value_per_event(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                column_name = 'CenterOfGravity_rolling_speed_cm_per_s')
            """
            results_per_event['distance_covered_cm'] = self._get_distance_covered_per_event(df = df,
                                                                                event_type = event_type,
                                                                                event_ids = results_per_event['bout_id'],
                                                                                marker_id = 'CenterOfGravity')
            bout_start_and_end_idxs = self._get_interval_start_and_end_idxs_per_event(df = df, event_type = event_type, event_ids = results_per_event['bout_id'])
            results_per_event['start_time'] = bout_start_and_end_idxs[:, 0]
            results_per_event['end_time'] = bout_start_and_end_idxs[:, 1]
        return pd.DataFrame(data = results_per_event)

    def _get_distance_covered_per_event(self, df: pd.DataFrame, event_type: str, event_ids: List[float], marker_id: str) -> List[float]:
        distances_per_event = []
        for event_id in event_ids:
            df_tmp = df.loc[df[f'{event_type}_id'] == event_id].copy()
            distances_per_event.append(((df_tmp[f'{marker_id}_x'].diff()**2 + df_tmp[f'{marker_id}_y'].diff()**2)**0.5).cumsum().iloc[-1])
        return distances_per_event

    def _get_mean_column_value_per_event(self, df: pd.DataFrame, event_type: str, event_ids: List[float], column_name: str) -> List[float]:
        mean_values = []
        for event_id in event_ids:
            mean_values.append(df.loc[df[f'{event_type}_id'] == event_id, column_name].mean())
        return mean_values

    def _get_all_bout_ids(self, df: pd.DataFrame, event_type: str) -> np.ndarray:
        return df[f'{event_type}_id'].dropna().unique()

    def _get_bout_duration_per_bout_id(self, df: pd.DataFrame, event_type: str, event_ids: List[float]) -> List[float]:
        durations = []
        for event_id in event_ids:
            durations.append(df.loc[df[f'{event_type}_id'] == event_id, f'{event_type}_duration'].iloc[0])
        return durations

    def _get_column_values_at_event_borders(self, df: pd.DataFrame, event_type: str, event_ids: List[float], column_name: str) -> np.ndarray:
        values_at_interval_borders = []
        for event_id in event_ids:
            start_value = df.loc[df[f'{event_type}_id'] == event_id, column_name].iloc[0]
            end_value = df.loc[df[f'{event_type}_id'] == event_id, column_name].iloc[-1]
            values_at_interval_borders.append((start_value, end_value))
        return np.asarray(values_at_interval_borders)

    def _get_interval_start_and_end_idxs_per_event(self, df: pd.DataFrame, event_type: str, event_ids: List[float]) -> np.ndarray:
        interval_border_idxs = []
        for event_id in event_ids:
            start_time, end_time = df.loc[df[f'{event_type}_id'] == event_id].index.values[[0, -1]]*self.framerate
            interval_border_idxs.append((start_time, end_time))
        return np.asarray(interval_border_idxs)

    def _create_session_overview_df(self, dfs_to_export_with_individual_bout_dfs: Dict[str, pd.DataFrame]) -> pd.DataFrame:
        session_overview = {'bout_type': [],
                            'total_bouts_count': [],
                            'total_duration': [],
                            'total_distance_covered': [],
                            'mean_duration': [],
                            'mean_distance_covered': [],
                            'mean_CenterOfGravity_x': []}
        for tab_name, df in dfs_to_export_with_individual_bout_dfs.items():
            bout_ids_split_depending_on_direction = self._get_bout_id_splits_depending_on_direction(df = df)
            for split_id, relevant_bout_ids in bout_ids_split_depending_on_direction.items():
                session_overview = self._add_results_to_session_overview(session_overview = session_overview,
                                                                         df = df,
                                                                         event_type = tab_name,
                                                                         event_prefix = split_id,
                                                                         bout_ids = relevant_bout_ids)
        return pd.DataFrame(data = session_overview)

    def _get_bout_id_splits_depending_on_direction(self, df: pd.DataFrame) -> Dict[str, List[float]]:
        towards_open_column_name = self._get_column_name_from_substring(all_columns = list(df.columns), substring = 'towards_open')
        bout_ids_split_by_direction = {'all': list(df['bout_id'].unique()),
                                       'towards_open': list(df.loc[df[towards_open_column_name] == True, 'bout_id'].unique()),
                                       'towards_closed': list(df.loc[df[towards_open_column_name] != True, 'bout_id'].unique())}
        return bout_ids_split_by_direction

    def _get_column_name_from_substring(self, all_columns: List[str], substring: str) -> str:
        matching_column_names = [column_name for column_name in all_columns if substring in column_name]
        assert len(matching_column_names) == 1, \
                f'There should be exactly one match for {substring} - however, {len(matching_column_names)} were found: [{matching_column_names}].'
        return matching_column_names[0]

    def _add_results_to_session_overview(self, session_overview: Dict, df: pd.DataFrame, event_type: str, event_prefix: str, bout_ids: List[float]) -> Dict:
        session_overview['bout_type'].append(f'{event_prefix}_{event_type}')
        if len(bout_ids) > 0:
            session_overview['total_bouts_count'].append(len(bout_ids))
            session_overview['total_duration'].append(df.loc[df['bout_id'].isin(bout_ids), 'duration'].cumsum().iloc[-1])
            session_overview['total_distance_covered'].append(df.loc[df['bout_id'].isin(bout_ids), 'distance_covered_cm'].cumsum().iloc[-1])
            session_overview['mean_duration'].append(df.loc[df['bout_id'].isin(bout_ids), 'duration'].mean())
            session_overview['mean_distance_covered'].append(df.loc[df['bout_id'].isin(bout_ids), 'distance_covered_cm'].mean())
            center_of_gravity_x_column_name = self._get_column_name_from_substring(all_columns = list(df.columns), substring = 'CenterOfGravity_x')
            session_overview['mean_CenterOfGravity_x'].append(df.loc[df['bout_id'].isin(bout_ids), center_of_gravity_x_column_name].mean())
        else:
            session_overview['total_bouts_count'].append(0)
            session_overview['total_duration'].append(0)
            session_overview['total_distance_covered'].append(0)
            session_overview['mean_duration'].append(np.nan)
            session_overview['mean_distance_covered'].append(np.nan)
            session_overview['mean_CenterOfGravity_x'].append(np.nan)
        return session_overview

    def _create_parameter_settings_df(self) -> pd.DataFrame:
        logged_settings = {'parameter': [], 'specified_value': []}
        for parameter, value in self.logs.items():
            logged_settings['parameter'].append(parameter)
            logged_settings['specified_value'].append(value)
        return pd.DataFrame(data = logged_settings)

    def _write_xlsx_file_to_disk(self, dfs_to_export: Dict[str, pd.DataFrame]) -> None:
        writer = pd.ExcelWriter(f'{self.base_output_filepath}.xlsx', engine='xlsxwriter')
        for tab_name, df in dfs_to_export.items():
            df.to_excel(writer, sheet_name = tab_name)
        writer.save()

    def inspect_processing(self,
                           marker_ids_to_inspect: List[str]=['CenterOfGravity'],
                           verbose: bool=False,
                           show_plot: bool=False,
                           save_plot: bool=True,
                           show_legend: bool=False
                          ) -> None:
        if verbose:
            print(f'Inspection of file: {self.filepath.name}:')
            for marker_id in marker_ids_to_inspect:
                coverage = self._compute_coverage(df = self.bodyparts[marker_id].df, critical_marker_ids = [marker_id])
                print(f'... coverage of "{marker_id}" was at: {round(coverage*100, 2)} %')
        if show_plot or save_plot:
            self._plot_selected_marker_ids_on_normalized_maze(marker_ids = marker_ids_to_inspect,
                                                              show = show_plot,
                                                              save = save_plot,
                                                              legend = show_legend)

    def _plot_selected_marker_ids_on_normalized_maze(self, marker_ids: List[str], show: bool=True, save: bool=True, legend: bool=False) -> None:
        fig = plt.figure(figsize=(10, 5), facecolor='white')
        ax = fig.add_subplot(111)
        for corner_marker_id in ['MazeCornerClosedRight', 'MazeCornerClosedLeft', 'MazeCornerOpenRight', 'MazeCornerOpenLeft']:
            x, y = self.logs[f'normalized_{corner_marker_id}_coords']
            plt.scatter(x, y, label = corner_marker_id)
        for marker_id in marker_ids:
            plt.scatter(self.bodyparts[marker_id].df['x'], self.bodyparts[marker_id].df['y'], alpha = 0.1, label = marker_id)
        plt.plot([0, 0, 50, 50, 0], [0, 4, 4, 0, 0], c = 'black')
        ax.set_aspect('equal')
        if legend:
            plt.legend()
        if save:
            assert hasattr(self, 'base_output_filepath'), 'You must run ".export_results()" first if you´d like to save the plot. Alternatively, just opt to show the plot.'
            plt.savefig(f'{self.base_output_filepath}.png', dpi = 300)
        if show:
            plt.show()
        else:
            plt.close()

class Bodypart2D():
    """
    Class that contains information for one single Bodypart.

    Attributes:
        self.id(str): Deeplabcut label of the bodypart
    """

    @property
    def exclusion_criteria(self) -> Dict:
        return {'likelihood_threshold': 0.5,
                              'min_x': -5,
                              'max_x': 55,
                              'min_y': -3,
                              'max_y': 7}

    def __init__(self, bodypart_id: str, df: pd.DataFrame, fps: int)->None:
        """
        Constructor for class Bodypart.

        Since the points in df_raw represent coordinates in the distorted dataframe, we use df_undistort for calculations.

        Parameters:
            bodypart_id(str): unique id of marker
        """
        self.id = bodypart_id
        sliced_df = self._slice_df(df = df)
        self.df = self._apply_exclusion_criteria(df = sliced_df, exclusion_criteria = self.exclusion_criteria)
        self.fps = fps
        self.framerate = 1/fps

    def _slice_df(self, df: pd.DataFrame) -> pd.DataFrame:
        """
        Function, that extracts the coordinates of a single bodypart.

        Parameters:
            df(pandas.DataFrame): the full dataframe of the recording with all bodyparts
        """
        df_input = df.copy()
        data = {'x': df_input.loc[:, f'{self.id}_x'],
                'y': df_input.loc[:, f'{self.id}_y'],
                'likelihood': df_input.loc[:, f'{self.id}_likelihood']}
        return pd.DataFrame(data = data)

    def _apply_exclusion_criteria(self, df: pd.DataFrame, exclusion_criteria: Dict) -> None:
        df.loc[df['likelihood'] < exclusion_criteria['likelihood_threshold'], :] = np.nan
        for coord in ['x', 'y']:
            df.loc[df[coord] < exclusion_criteria[f'min_{coord}'], :] = np.nan
            df.loc[df[coord] > exclusion_criteria[f'max_{coord}'], :] = np.nan
        return df

    def calculate_speed_and_identify_immobility(self, sliding_window_size: int, immobility_threshold: float) -> None:
        self._add_speed_to_df()
        self._add_rolling_speed_to_df(sliding_window_size = sliding_window_size)
        self._add_immobility_to_df(immobility_threshold = immobility_threshold)

    def _add_speed_to_df(self)->None:
        self.df.loc[:, 'speed_cm_per_s'] = (self.df.loc[:, 'x'].diff()**2 + self.df.loc[:, 'y'].diff()**2)**0.5 / self.framerate

    def _add_rolling_speed_to_df(self, sliding_window_size: int) -> None:
        min_periods = int(sliding_window_size * 0.66)
        self.df.loc[:, 'rolling_speed_cm_per_s'] = self.df.loc[:, 'speed_cm_per_s'].rolling(sliding_window_size, min_periods = min_periods, center = True).mean()

    def _add_immobility_to_df(self, immobility_threshold: float) -> None:
        self.df.loc[:, 'immobility'] = False
        self.df.loc[self.df['rolling_speed_cm_per_s'] < immobility_threshold, 'immobility'] = True


from PIL import Image
import os
from tqdm import tqdm

'''
由于部分的face_dataset的图片的格式是32位位深，但是在我的网络网络卷积中只有三层无法进行4层的卷积。

'''

def convert_to_24bit_depth(input_dir, output_dir):
    # 确保输出目录存在
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    # 获取输入目录中的图片文件列表
    image_files = [f for f in os.listdir(input_dir) if f.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))]

    # 使用tqdm创建进度条
    for filename in tqdm(image_files, desc="Converting images", unit="image"):
        input_path = os.path.join(input_dir, filename)
        output_path = os.path.join(output_dir, filename)

        # 打开图像
        image = Image.open(input_path)

        # 将图像转换为24位位深
        image = image.convert('RGB')

        # 保存转换后的图像
        image.save(output_path)

if __name__ == "__main__":
    # 指定输入和输出目录
    input_directory = 'face_dataset'
    output_directory = 'output'

    # 调用函数进行转换
    convert_to_24bit_depth(input_directory, output_directory)


import numpy as np
import onnxruntime
from tqdm import tqdm
import os
from PIL import Image
import argparse
import psutil
import gpizero
import telnetlib as tel
import time

#argument parser
parser = argparse.ArgumentParser(description='EE379K project - Deployment code')
parser.add_argument('--model', type=str, default='mbvn1_e0_f5.onnx', help='Enter model name')

args = parser.parse_args()

# Create Inference session using ONNX runtime
sess = onnxruntime.InferenceSession(args.model)

# Get the input name for the ONNX model
input_name = sess.get_inputs()[0].name
print("Input name  :", input_name)

# Get the shape of the input
input_shape = sess.get_inputs()[0].shape
print("Input shape :", input_shape)

# Mean and standard deviation used for PyTorch models
mean = np.array((0.4914, 0.4822, 0.4465))
std = np.array((0.2023, 0.1994, 0.2010))

def getTelnetPower(SP2_tel, last_power):    #for both devices
    tel_dat = str(SP2_tel.read_very_eager())
    #print("telnet reading:", tel_dat)
    findex = tel_dat.rfind('\n')
    findex2 = tel_dat[:findex].rfind('\n')
    findex2 = findex2 if findex2 != -1 else 0
    ln = tel_dat[findex2: findex].strip().split(',')
    if len(ln) < 2:
        total_power = last_power
    else:
        total_power = float(ln[-2])
    return total_power

SP2_tel = tel.Telnet('192.168.4.1')
DELAY = 0.2

# Label names for CIFAR10 Dataset
label_names = ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
accuracy = 0
total_inference_time = 0
total_power = 0
max_power = -1
# The test_deployment folder contains all 10.000 images from the testing dataset of CIFAR10 in .png format
for filename in tqdm(os.listdir("HW3_files/test_deployment")):
    # Take each image, one by one, and make inference
    with Image.open(os.path.join("HW3_files/test_deployment", filename)).resize((32, 32)) as img:
        last_time  = time.time()
        #print("Image shape:", np.float32(img).shape)
        # For PyTorch models ONLY: normalize image
        input_image = (np.float32(img) / 255. - mean) / std
        # For PyTorch models ONLY: Add the Batch axis in the data Tensor (C, H, W)
        input_image = np.expand_dims(np.float32(input_image), axis=0)
        # For PyTorch models ONLY: change the order from (B, H, W, C) to (B, C, H, W)
        input_image = input_image.transpose([0, 3, 1, 2])

        # Run inference and get the prediction for the input image
        start_time = time.time()
        pred_onnx = sess.run(None, {input_name: input_image})[0]
        total_inference_time += time.time() - start_time()
        # Find the prediction with the highest probability
        top_prediction = np.argmax(pred_onnx[0])

        # Get the label of the predicted class
        pred_class = label_names[top_prediction]
        #true label
        true_class = ''.join([i for i in filename[:-4] if i.isalpha()])

        #accuracy calculation
        if pred_class == true_class:
            accuracy += 1

        #power calculation
        power = getTelnetPower(SP2_tel,power)
        if power > max_power:
            max_power = power
        total_power += power
        #sampling rate = 0.2s
        elapsed = time.time() - last_time
        time.sleep(max(0,DELAY-elapsed))
print("Accuracy: " + str(accuracy/10000))
print("Average Latency: " + str(total_inference_time/10000))
print("Maximum Power Cconsumption: " + str(max_power))
print("Average Energy Consumption: " +str(total_power*DELAY/10000))


# coding=utf-8

"""
@Author:lynn

example:
    python3 terminal_predict.py \
    --model_type bert \
    --labels ./labels.txt \
    --output_dir germeval \
    --max_seq_length  128 \

When console displays 'Input chinese sentence:', then you input some sentences and get pos result.
More detail see ./example/how_to_use.ipynb
"""

import datetime
import collections
import glob
import math
import os
import re

import numpy as np
import tensorflow as tf
from absl import app, flags, logging

from transformers import (
    TF2_WEIGHTS_NAME,
    TF_MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING,
    AutoConfig,
    AutoTokenizer,
    TFAutoModelForTokenClassification,
)
from utils_ner import convert_examples_to_features, get_labels, InputExample

MODEL_CONFIG_CLASSES = list(TF_MODEL_FOR_TOKEN_CLASSIFICATION_MAPPING.keys())
MODEL_TYPES = tuple(conf.model_type for conf in MODEL_CONFIG_CLASSES)

ALL_MODELS = sum((tuple(conf.pretrained_config_archive_map.keys()) for conf in MODEL_CONFIG_CLASSES), (),)

flags.DEFINE_string("model_type", None, "Model type selected in the list: " + ", ".join(MODEL_TYPES))

flags.DEFINE_string("output_dir", None, "The output directory where the model checkpoints will be written.")

flags.DEFINE_string(
    "labels", "", "Path to a file containing all labels. If not specified, CoNLL-2003 labels are used."
)

flags.DEFINE_integer(
    "max_seq_length",
    128,
    "The maximum total input sentence length after tokenization. "
    "Sequences longer than this will be truncated, sequences shorter "
    "will be padded.",
)

flags.DEFINE_string(
    "tpu",
    None,
    "The Cloud TPU to use for training. This should be either the name "
    "used when creating the Cloud TPU, or a grpc://ip.address.of.tpu:8470 "
    "url.",
)

flags.DEFINE_integer("num_tpu_cores", 8, "Total number of TPU cores to use.")

flags.DEFINE_boolean("do_lower_case", False, "Set this flag if you are using an uncased model.")

flags.DEFINE_boolean("no_cuda", False, "Avoid using CUDA when available")

# flags.DEFINE_boolean("overwrite_output_dir", False, "Overwrite the content of the output directory")

flags.DEFINE_boolean("overwrite_cache", False, "Overwrite the cached training and evaluation sets")

flags.DEFINE_boolean("fp16", False, "Whether to use 16-bit (mixed) precision instead of 32-bit")

flags.DEFINE_string(
    "gpus",
    "0",
    "Comma separated list of gpus devices. If only one, switch to single "
    "gpu strategy, if None takes all the gpus available.",
)

def read_examples_from_line(line):
    guid_index = 1
    examples = []
    words = []
    labels = []
    for word in line:
        words.append(word)
        labels.append("O")

    if words:
        examples.append(InputExample(
            guid="{}-{}".format('predict', guid_index), words=words, labels=labels)
        )

    return examples

def main(_):
    logging.set_verbosity(logging.INFO)
    args = flags.FLAGS.flag_values_dict()

    if args["fp16"]:
        tf.config.optimizer.set_experimental_options({"auto_mixed_precision": True})

    if args["tpu"]:
        resolver = tf.distribute.cluster_resolver.TPUClusterResolver(tpu=args["tpu"])
        tf.config.experimental_connect_to_cluster(resolver)
        tf.tpu.experimental.initialize_tpu_system(resolver)
        strategy = tf.distribute.experimental.TPUStrategy(resolver)
        args["n_device"] = args["num_tpu_cores"]
    elif len(args["gpus"].split(",")) > 1:
        args["n_device"] = len([f"/gpu:{gpu}" for gpu in args["gpus"].split(",")])
        strategy = tf.distribute.MirroredStrategy(devices=[f"/gpu:{gpu}" for gpu in args["gpus"].split(",")])
    elif args["no_cuda"]:
        args["n_device"] = 1
        strategy = tf.distribute.OneDeviceStrategy(device="/cpu:0")
    else:
        args["n_device"] = len(args["gpus"].split(","))
        strategy = tf.distribute.OneDeviceStrategy(device="/gpu:" + args["gpus"].split(",")[0])

    logging.warning(
        "n_device: %s, distributed training: %s, 16-bits training: %s",
        args["n_device"],
        bool(args["n_device"] > 1),
        args["fp16"],
    )

    labels = get_labels(args["labels"])
    pad_token_label_id = -1

    logging.info("predict parameters %s", args)
    tokenizer = AutoTokenizer.from_pretrained(args["output_dir"], do_lower_case=args["do_lower_case"])
    model = TFAutoModelForTokenClassification.from_pretrained(args["output_dir"])

    while True:
        print('Input chinese sentence:')
        line = str(input())
        if line == 'quit':
            break
        if len(line) < 1:
            print('Please input a chinese sentence or "quit" to break this loop:')
            continue

        examples = read_examples_from_line(line)
        features = convert_examples_to_features(
            examples,
            labels,
            args["max_seq_length"],
            tokenizer,
            cls_token_at_end=bool(args["model_type"] in ["xlnet"]),
            # xlnet has a cls token at the end
            cls_token=tokenizer.cls_token,
            cls_token_segment_id=2 if args["model_type"] in ["xlnet"] else 0,
            sep_token=tokenizer.sep_token,
            sep_token_extra=bool(args["model_type"] in ["roberta"]),
            # roberta uses an extra separator b/w pairs of sentences, cf. github.com/pytorch/fairseq/commit/1684e166e3da03f5b600dbb7855cb98ddfcd0805
            pad_on_left=bool(args["model_type"] in ["xlnet"]),
            # pad on the left for xlnet
            pad_token=tokenizer.pad_token_id,
            pad_token_segment_id=tokenizer.pad_token_type_id,
            pad_token_label_id=pad_token_label_id,
        )

        feature = features[0]
        X = collections.OrderedDict()

        X["input_ids"] = tf.train.Feature(int64_list=tf.train.Int64List(value=list(feature.input_ids)))
        X["input_mask"] = tf.train.Feature(int64_list=tf.train.Int64List(value=list(feature.input_mask)))
        X["segment_ids"] = tf.train.Feature(int64_list=tf.train.Int64List(value=list(feature.segment_ids)))
        X["label_ids"] = tf.train.Feature(int64_list=tf.train.Int64List(value=list(feature.label_ids)))
        tf_example = tf.train.Example(features=tf.train.Features(feature=X))
        tf_example = tf_example.SerializeToString()

        max_seq_length = args["max_seq_length"]
        name_to_features = {
            "input_ids": tf.io.FixedLenFeature([max_seq_length], tf.int64),
            "input_mask": tf.io.FixedLenFeature([max_seq_length], tf.int64),
            "segment_ids": tf.io.FixedLenFeature([max_seq_length], tf.int64),
            "label_ids": tf.io.FixedLenFeature([max_seq_length], tf.int64),
        }

        def _decode_record(record):
            example = tf.io.parse_single_example(record, name_to_features)
            features = {}
            features["input_ids"] = example["input_ids"]
            features["input_mask"] = example["input_mask"]
            features["segment_ids"] = example["segment_ids"]
            return features, example["label_ids"]

        dataset = []
        dataset.append(tf_example)

        dataset = tf.data.Dataset.from_tensor_slices(dataset)
        dataset = dataset.map(_decode_record)

        batch_size = 1
        dataset = dataset.batch(batch_size)

        eval_features, eval_labels = iter(dataset).next()

        inputs = {"attention_mask": eval_features["input_mask"], "training": False}

        if args["model_type"] != "distilbert":
            inputs["token_type_ids"] = (
                eval_features["segment_ids"] if args["model_type"] in ["bert", "xlnet"] else None
            )

        with strategy.scope():
            logits = model(eval_features["input_ids"], **inputs)[0]
            active_loss = tf.reshape(eval_labels, (-1,)) != pad_token_label_id

        preds = logits.numpy()
        label_ids = eval_labels.numpy()

        preds = np.argmax(preds, axis=2)
        y_pred = [[] for _ in range(label_ids.shape[0])]

        for i in range(label_ids.shape[0]):
            for j in range(label_ids.shape[1]):
                if label_ids[i, j] != pad_token_label_id:
                    y_pred[i].append(labels[preds[i, j]])

        tokens = tokenizer.tokenize(line)
        print('## tokens = %s' % tokens)
        print('## y_pred = %s' % y_pred)
        print('## %s = %s' % (len(tokens), len(y_pred[0])))
        word_group = []
        subword = {}

        def _add_word(subword):
            word_group.append(subword['token'] + '/' + subword['flag'])
            subword.clear()

        for i, token in enumerate(tokens):
            flag = y_pred[0][i]
            print('## %s = %s' % (token, flag))
            if flag.startswith('B'):
                if len(subword) > 0:
                    _add_word(subword)
                subword['token'] = token
                subword['flag'] = flag
            elif flag.startswith('I'):
                if (
                    len(subword) > 0
                    and (y_pred[0][i-1].startswith('I') or y_pred[0][i-1].startswith('B'))
                    and (y_pred[0][i-1][1:] == flag[1:])
                ):
                    subword['token'] = subword['token'] + token
                    continue
                elif len(subword) > 0:
                    _add_word(subword)
                subword['token'] = token
                subword['flag'] = flag
            else:
                if len(subword) > 0:
                    _add_word(subword)
                subword['token'] = token
                subword['flag'] = flag
                _add_word(subword)

        if len(subword) > 0:
            _add_word(subword)
        print('## word_group = %s' % word_group)

if __name__ == "__main__":
    flags.mark_flag_as_required("output_dir")
    flags.mark_flag_as_required("model_type")
    app.run(main)