Repository: neuromodulation/py_neuromodulation (language: python)

Issue:
Fix type hints project-wide
Since Pydantic will be used to validate analysis settings, and the contents of settings will be propagated to the different submodules of PyNeuro, type hints should be consistently provided, and correct, all accross the project. MyPy and PyRight are throwing a lot of static analysis errors at the time, which is unclear if they would affect Pydantic integration or not, but I think it's better to fix them, also in case users of the module are using static analysis tools themselves.

[mypy_report.txt](https://github.com/neuromodulation/py_neuromodulation/files/14995650/mypy_report.txt)
[pyright_report.txt](https://github.com/neuromodulation/py_neuromodulation/files/14995651/pyright_report.txt)

Add support for pandas DataFrames as input data
Currently only numpy arrays are accepted as input when creating nm_channel object. 
A name related error is thrown when running:

`features = stream.run(data)` 

Find Error reproduction in a jupyter notebook and sample data attached.
The Exception thrown is misleading. 

[reproduce_error.zip](https://github.com/neuromodulation/py_neuromodulation/files/14882602/reproduce_error.zip)



Discussion / hints:

Hello Samed. First of all thank you for reporting this issue and providing the code to reproduce it. I checked the error message and indeed there was a bug in there, in that it was showing the channels names of the input data twice, instead of comparing the input data channel names with the expected names, that are the ones set in nm_channels when initializing the stream.

After fixing that, now this is the content of the thrown Exception:
```
If data is passed as a DataFrame, thecolumns must match the channel names in `nm_channels`.
Data columns: [' EXG_Channel_0', ' EXG_Channel_1', ' EXG_Channel_2', ' EXG_Channel_3', ' EXG_Channel_4', ' EXG_Channel_5', ' EXG_Channel_6', ' EXG_Channel_7']
Expected: nm_channels.name: ['ch0', 'ch1', 'ch2', 'ch3', 'ch4', 'ch5', 'ch6', 'ch7'].
```

As you see, the problem is that the columns in the input dataframe have not been renamed to match the nm_channels object, or viceversa.  You have 2 lines that potentially fix this error:

```
data = data.rename(columns=dict(zip(data.columns[0:8], nm_channels.name)))
```
or
```
nm_channels = nm_define_nmchannels.set_channels(ch_names=data.keys()[0:8], ch_types=['SEEG']*8)
```
Notice that I have changed the slicing ranges from 1:9 in the original code to 0:8

After fixing that, that error wasn't thrown anymore but the resulting feature dataframe was empty. I noticed that there was a mismatch between the expected shape for numpy data (n_channels, n_timepoints) and the expected shape for a pandas.df (n_timepoints, n_channels). That should be fixed by PR  #301


Fix patch:
diff --git a/examples/plot_1_example_BIDS.py b/examples/plot_1_example_BIDS.py
index 62e038f0..2a27fc2c 100644
--- a/examples/plot_1_example_BIDS.py
+++ b/examples/plot_1_example_BIDS.py
@@ -175,9 +175,9 @@
 nm_plots.plot_corr_matrix(
     feature=feature_reader.feature_arr.filter(regex="ECOG_RIGHT_0"),
     ch_name="ECOG_RIGHT_0-avgref",
-    feature_names=feature_reader.feature_arr.filter(
+    feature_names=list(feature_reader.feature_arr.filter(
         regex="ECOG_RIGHT_0-avgref"
-    ).columns,
+    ).columns),
     feature_file=feature_reader.feature_file,
     show_plot=True,
     figsize=(15, 15),
diff --git a/examples/plot_2_example_add_feature.py b/examples/plot_2_example_add_feature.py
index 20834af7..fb229639 100644
--- a/examples/plot_2_example_add_feature.py
+++ b/examples/plot_2_example_add_feature.py
@@ -32,7 +32,7 @@ def __init__(
         self.s = settings
         self.ch_names = ch_names
 
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         for ch_idx, ch in enumerate(self.ch_names):
             features_compute[f"new_feature_{ch}"] = np.mean(data[ch_idx, :])
 
diff --git a/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py b/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py
index eef535ba..fa97e424 100644
--- a/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py
+++ b/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py
@@ -1,25 +1,20 @@
 import nibabel as nib
 import numpy as np
-import scipy.io as sio
-import os
 from matplotlib import pyplot as plt
 
 
 class NiiToMNI:
-
     def __init__(
         self,
         PATH_nii_file: str = r"C:\code\RMap_ROI_Estimation\Automated Anatomical Labeling 3 (Rolls 2020).nii",
     ) -> None:
-
-        self.img = nib.load(PATH_nii_file)
+        self.img = nib.Nifti1Image.from_filename(PATH_nii_file)
         self.data = self.img.get_fdata()
 
     def downsample_nii(
         self,
         resampling_factor: int = 150,
     ):
-
         # PATH_MNI_TO_ATLAS = r"C:\code\mni_to_atlas\src\mni_to_atlas\atlases\AAL.nii"
         # img_mni_to_atlas = nib.load(PATH_MNI_TO_ATLAS)
 
@@ -55,12 +50,9 @@ def downsample_nii(
 
     def select_non_zero_voxels(
         self,
-        mni_coordinates: np.array,
+        mni_coordinates: np.ndarray,
     ):
-
-        coords = np.hstack(
-            (mni_coordinates, np.ones((mni_coordinates.shape[0], 1)))
-        )
+        coords = np.hstack((mni_coordinates, np.ones((mni_coordinates.shape[0], 1))))
 
         # and transform back to get the voxel values
         voxels_downsampled = np.array(
@@ -74,15 +66,15 @@ def select_non_zero_voxels(
             coord_.append(mni_coordinates[i, :])
 
         # get only voxel values non-zero
-        ival = np.array(ival)
-        coord_ = np.array(coord_)
-        ival_non_zero = ival[ival != 0]
-        coord_non_zero = coord_[ival != 0]
+        ival_arr = np.array(ival)
+        coord_arr = np.array(coord_)
+        ival_non_zero = ival_arr[ival != 0]
+        coord_non_zero = coord_arr[ival != 0]
         print(coord_non_zero.shape)
 
         return coord_non_zero, ival_non_zero
 
-    def plot_3d_coordinates(self, coord_non_zero: np.array):
+    def plot_3d_coordinates(self, coord_non_zero: np.ndarray):
         fig = plt.figure()
         ax = fig.add_subplot(111, projection="3d")
         ax.scatter(
@@ -96,11 +88,8 @@ def plot_3d_coordinates(self, coord_non_zero: np.array):
 
 
 if __name__ == "__main__":
-
     nii_to_mni = NiiToMNI(
         PATH_nii_file=r"C:\code\py_neuromodulation\ConnectivityDecoding\Automated Anatomical Labeling 3 (Rolls 2020).nii"
     )
     mni_coordinates = nii_to_mni.downsample_nii(resampling_factor=150)
-    coord_non_zero, ival_non_zero = nii_to_mni.select_non_zero_voxels(
-        mni_coordinates
-    )
+    coord_non_zero, ival_non_zero = nii_to_mni.select_non_zero_voxels(mni_coordinates)
diff --git a/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py b/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py
index 5e284437..2238ff6b 100644
--- a/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py
+++ b/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py
@@ -2,7 +2,6 @@
 import numpy as np
 import scipy.io as sio
 import os
-from matplotlib import pyplot as plt
 
 
 def write_connectome_mat(
@@ -14,7 +13,6 @@ def write_connectome_mat(
     ),
     func_: bool = False,
 ):
-
     # connectome = sio.loadmat(PATH_CONNECTOME)  # check if read was successful
 
     # load all fingerprints and put them in .npy
@@ -23,16 +21,13 @@ def write_connectome_mat(
         files_fps = [f for f in os.listdir(PATH_Fingerprints) if ".nii" in f]
     else:
         files_fps = [
-            f
-            for f in os.listdir(PATH_Fingerprints)
-            if "func_seed_AvgR_Fz.nii" in f
+            f for f in os.listdir(PATH_Fingerprints) if "func_seed_AvgR_Fz.nii" in f
         ]
 
     # I except 1025 files, check which ones are missing
     missing_files = []
 
     for i in range(1, 1026):
-
         MISSING = False
 
         if func_ is False:
@@ -49,20 +44,14 @@ def write_connectome_mat(
                 r"D:\Connectome_RMAP_OUT\whole_brain\ROIs", f"ROI-{i}.nii"
             )
             # copy the ROI file to the following folder:
-            PATH_ROI_OUT = (
-                r"D:\Connectome_RMAP_OUT\whole_brain\ROI_missing_struc"
-            )
+            PATH_ROI_OUT = r"D:\Connectome_RMAP_OUT\whole_brain\ROI_missing_struc"
             import shutil
 
             shutil.copy(ROI_file, os.path.join(PATH_ROI_OUT, f"ROI-{i}.nii"))
 
     for idx, f in enumerate(files_fps):
         # load the .nii file and put it all in in a dictionary with the name of the file
-        fp = (
-            nib.load(os.path.join(PATH_Fingerprints, f))
-            .get_fdata()
-            .astype(np.float16)
-        )
+        fp = nib.load(os.path.join(PATH_Fingerprints, f)).get_fdata().astype(np.float16)
         if "struc" in f:
             dict_connectome[f[f.find("ROI-") + 4 : f.find("_struc")]] = fp
         else:
@@ -79,7 +68,6 @@ def write_connectome_mat(
 
 
 if __name__ == "__main__":
-
     write_connectome_mat(
         PATH_Fingerprints=r"D:\Connectome_RMAP_OUT\whole_brain\struc\HCP1000 6K",
         PATH_CONNECTOME=os.path.join(
diff --git a/py_neuromodulation/__init__.py b/py_neuromodulation/__init__.py
index baa54f0b..65aa9847 100644
--- a/py_neuromodulation/__init__.py
+++ b/py_neuromodulation/__init__.py
@@ -1,13 +1,15 @@
-from . import (
-    nm_analysis,
-    nm_stream_abc,
-    nm_cohortwrapper,
-    nm_across_patient_decoding,
-    nm_stream_offline,
-    nm_settings,
-    nm_define_nmchannels,
-)
+from .utils import _logging
+from pathlib import PurePath
+from importlib.metadata import version
+
+__version__ = version("py_neuromodulation")
+
+# Define constant for py_nm directory
+PYNM_DIR = PurePath(__file__).parent
+
+# logger initialization first to prevent circular import
+logger = _logging.logger
+
+# Bring Stream and DataProcessor classes to top namespace
 from .nm_stream_offline import Stream
 from .nm_run_analysis import DataProcessor
-
-__version__ = "0.1.0.dev1"
diff --git a/py_neuromodulation/_write_example_dataset_helper.py b/py_neuromodulation/_write_example_dataset_helper.py
index 739d75b2..93791b56 100644
--- a/py_neuromodulation/_write_example_dataset_helper.py
+++ b/py_neuromodulation/_write_example_dataset_helper.py
@@ -1,28 +1,28 @@
 import mne
 import mne_bids
 import pybv  # pip install pybv
-import os
+from pathlib import PurePath
 
 
 def set_chtypes(vhdr_raw):
     """
     define MNE RawArray channel types
     """
-    print('Setting new channel types...')
+    print("Setting new channel types...")
     remapping_dict = {}
-    for ch_name in vhdr_raw.info['ch_names']:
-        if ch_name.startswith('ECOG'):
-            remapping_dict[ch_name] = 'ecog'
-        elif ch_name.startswith(('LFP', 'STN')):
-            remapping_dict[ch_name] = 'dbs'
-        elif ch_name.startswith('EMG'):
-            remapping_dict[ch_name] = 'emg'
-        elif ch_name.startswith('EEG'):
-            remapping_dict[ch_name] = 'misc'
-        elif ch_name.startswith(('MOV', 'ANALOG', 'ROT', 'ACC', 'AUX', 'X', 'Y', 'Z')):
-            remapping_dict[ch_name] = 'misc'
+    for ch_name in vhdr_raw.info["ch_names"]:
+        if ch_name.startswith("ECOG"):
+            remapping_dict[ch_name] = "ecog"
+        elif ch_name.startswith(("LFP", "STN")):
+            remapping_dict[ch_name] = "dbs"
+        elif ch_name.startswith("EMG"):
+            remapping_dict[ch_name] = "emg"
+        elif ch_name.startswith("EEG"):
+            remapping_dict[ch_name] = "misc"
+        elif ch_name.startswith(("MOV", "ANALOG", "ROT", "ACC", "AUX", "X", "Y", "Z")):
+            remapping_dict[ch_name] = "misc"
         else:
-            remapping_dict[ch_name] = 'misc'
+            remapping_dict[ch_name] = "misc"
     vhdr_raw.set_channel_types(remapping_dict, verbose=False)
     return vhdr_raw
 
@@ -36,9 +36,15 @@ def write_bids_example():
 
     entities = mne_bids.get_entities_from_fname(PATH_RUN)
 
-    bids_path = mne_bids.BIDSPath(subject=entities["subject"], session=entities["session"], task=entities["task"],
-                                  run=entities["run"], acquisition=entities["acquisition"], datatype="ieeg",
-                                  root=PATH_BIDS)
+    bids_path = mne_bids.BIDSPath(
+        subject=entities["subject"],
+        session=entities["session"],
+        task=entities["task"],
+        run=entities["run"],
+        acquisition=entities["acquisition"],
+        datatype="ieeg",
+        root=PATH_BIDS,
+    )
 
     raw_arr = mne_bids.read_raw_bids(bids_path)
 
@@ -48,18 +54,30 @@ def write_bids_example():
     data = raw_arr.get_data()[:10, :]
     ch_names = raw_arr.ch_names[:10]
 
+    pybv.write_brainvision(
+        data=data,
+        sfreq=raw_arr.info["sfreq"],
+        ch_names=ch_names,
+        fname_base="example",
+        folder_out=PATH_OUT_TEMP,
+    )
 
-    pybv.write_brainvision(data=data, sfreq=raw_arr.info["sfreq"], ch_names=ch_names,
-                           fname_base="example", folder_out=PATH_OUT_TEMP)
-
-    data_to_write = mne.io.read_raw_brainvision(os.path.join(PATH_OUT_TEMP, "example.vhdr"))
+    data_to_write = mne.io.read_raw_brainvision(PurePath(PATH_OUT_TEMP, "example.vhdr"))
 
     # example.eeg / .vhdr need to be deleted afterwards
 
     data_to_write = set_chtypes(data_to_write)
     data_to_write.info["line_freq"] = 60
 
-    mne_bids.write_raw_bids(data_to_write, mne_bids.BIDSPath(subject="testsub", session="EphysMedOff",
-                            task="gripforce", datatype="ieeg", run="0",
-                            root=PATH_OUT),
-                            overwrite=True)
+    mne_bids.write_raw_bids(
+        data_to_write,
+        mne_bids.BIDSPath(
+            subject="testsub",
+            session="EphysMedOff",
+            task="gripforce",
+            datatype="ieeg",
+            run="0",
+            root=PATH_OUT,
+        ),
+        overwrite=True,
+    )
diff --git a/py_neuromodulation/nm_EpochStream.py b/py_neuromodulation/nm_EpochStream.py
deleted file mode 100644
index a62d658b..00000000
--- a/py_neuromodulation/nm_EpochStream.py
+++ /dev/null
@@ -1,92 +0,0 @@
-import os
-import pickle
-
-import numpy as np
-import pandas as pd
-
-import py_neuromodulation as nm
-from py_neuromodulation import nm_generator
-
-
-class EpochStream(nm.nm_stream.PNStream):
-    def __init__(self) -> None:
-        super().__init__()
-
-    def read_epoch_data(self, path_epoch) -> None:
-        """Read npy array of epochs. Shape is assumed to be (samples, channels, time)
-
-        Parameters
-        ----------
-        path_epoch : str
-        """
-        self.data = np.load(path_epoch)
-
-    def get_data(
-        self,
-    ) -> np.array:
-        """This data generator returns one epoch at a time.
-        Data will thus be analyzed in steps of the epoch size
-
-        Returns
-        -------
-        np.array
-            _description_
-
-        Yields
-        ------
-        Iterator[np.array]
-            _description_
-        """
-        for n_batch in range(self.data.shape[0]):
-            yield self.data[n_batch, :, :]
-
-    def run(
-        self,
-    ):
-        self._set_run()
-        # shape is n, channels=7, 800 Hz
-
-        self.feature_arr = pd.DataFrame()
-        self.feature_arr_list = []
-        epoch_gen = self.get_data()
-        idx_epoch = 0
-
-        while True:
-            data = next(
-                epoch_gen, None
-            )  # None will be returned if generator ran through
-            if data is None:
-                break
-            gen = nm_generator.raw_data_generator(
-                data, self.settings, self.sfreq
-            )
-
-            def get_data_within_epoch() -> np.array:
-                return next(gen, None)
-
-            idx_within_epoch = 0
-            while True:
-                data_within_epoch = get_data_within_epoch()
-                if data_within_epoch is None:
-                    break
-
-                feature_series = self.run_analysis.process_data(
-                    data_within_epoch
-                )
-                if idx_within_epoch == 0:
-                    self.feature_arr = pd.DataFrame([feature_series])
-                    idx_within_epoch += 1
-                else:
-                    self.feature_arr = self.feature_arr.append(
-                        feature_series, ignore_index=True
-                    )
-            self.feature_arr_list.append(self.feature_arr)
-
-    def _add_timestamp(
-        self, feature_series: pd.Series, idx: int = None
-    ) -> pd.Series:
-        # in case of epochs no timestamp is necessary
-        return feature_series
-
-    def _add_coordinates(self) -> None:
-        pass
diff --git a/py_neuromodulation/nm_IO.py b/py_neuromodulation/nm_IO.py
index 397ccf4e..c4d8d66b 100644
--- a/py_neuromodulation/nm_IO.py
+++ b/py_neuromodulation/nm_IO.py
@@ -1,24 +1,17 @@
 import json
-import os
-import sys
-from pathlib import Path
-import logging
-
-logger = logging.getLogger("PynmLogger")
-
+from pathlib import PurePath, Path
 
 import mne
 import mne_bids
 import numpy as np
 import pandas as pd
-from scipy import io
-
-import pyarrow
-from pyarrow import csv
+import scipy.io as sio
 
-import py_neuromodulation
+from pyarrow import Table as pyarrow_Table
+from pyarrow import csv as pyarrow_csv
 
-_PathLike = str | os.PathLike
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import logger, PYNM_DIR
 
 
 def load_nm_channels(
@@ -37,7 +30,7 @@ def load_nm_channels(
     if isinstance(nm_channels, pd.DataFrame):
         nm_ch_return = nm_channels
     elif nm_channels:
-        if not os.path.isfile(nm_channels):
+        if not Path(nm_channels).is_file():
             raise ValueError(
                 "PATH_NM_CHANNELS is not a valid file. Got: " f"{nm_channels}"
             )
@@ -51,7 +44,7 @@ def read_BIDS_data(
     BIDS_PATH: _PathLike | None = None,
     datatype: str = "ieeg",
     line_noise: int = 50,
-) -> tuple[mne.io.Raw, np.ndarray, int | float, int, list | None, list | None]:
+) -> tuple[mne.io.Raw, np.ndarray, float, int, list | None, list | None]:
     """Given a run path and bids data path, read the respective data
 
     Parameters
@@ -110,43 +103,30 @@ def get_coord_list(
 
 def read_grid(PATH_GRIDS: _PathLike | None, grid_str: str) -> pd.DataFrame:
     if PATH_GRIDS is None:
-        grid = pd.read_csv(
-            Path(__file__).parent / ("grid_" + grid_str.lower() + ".tsv"),
-            sep="\t",
-        )
+        grid = pd.read_csv(PYNM_DIR / ("grid_" + grid_str.lower() + ".tsv"), sep="\t")
     else:
         grid = pd.read_csv(
-            Path(PATH_GRIDS) / ("grid_" + grid_str.lower() + ".tsv"),
-            sep="\t",
+            PurePath(PATH_GRIDS, "grid_" + grid_str.lower() + ".tsv"), sep="\t"
         )
     return grid
 
 
-def get_annotations(
-    PATH_ANNOTATIONS: str, PATH_RUN: str, raw_arr: mne.io.RawArray
-):
+def get_annotations(PATH_ANNOTATIONS: str, PATH_RUN: str, raw_arr: mne.io.RawArray):
+    filepath = PurePath(PATH_ANNOTATIONS, PurePath(PATH_RUN).name[:-5] + ".txt")
     try:
-        annot = mne.read_annotations(
-            Path(PATH_ANNOTATIONS) / (os.path.basename(PATH_RUN)[:-5] + ".txt")
-        )
+        annot = mne.read_annotations(filepath)
         raw_arr.set_annotations(annot)
 
         # annotations starting with "BAD" are omitted with reject_by_annotations 'omit' param
         annot_data = raw_arr.get_data(reject_by_annotation="omit")
     except FileNotFoundError:
-        logger.critical(
-            "Annotations file could not be found"
-            + "expected location: "
-            + str(
-                Path(PATH_ANNOTATIONS)
-                / (os.path.basename(PATH_RUN)[:-5] + ".txt")
-            )
-        )
+        logger.critical(f"Annotations file could not be found: {filepath}")
+
     return annot, annot_data, raw_arr
 
 
 def read_plot_modules(
-    PATH_PLOT: _PathLike = Path(__file__).absolute().parent / "plots",
+    PATH_PLOT: _PathLike = PYNM_DIR / "plots",
 ):
     """Read required .mat files for plotting
 
@@ -156,10 +136,10 @@ def read_plot_modules(
         path to plotting files, by default
     """
 
-    faces = io.loadmat(os.path.join(PATH_PLOT, "faces.mat"))
-    vertices = io.loadmat(os.path.join(PATH_PLOT, "Vertices.mat"))
-    grid = io.loadmat(os.path.join(PATH_PLOT, "grid.mat"))["grid"]
-    stn_surf = io.loadmat(os.path.join(PATH_PLOT, "STN_surf.mat"))
+    faces = sio.loadmat(PurePath(PATH_PLOT, "faces.mat"))
+    vertices = sio.loadmat(PurePath(PATH_PLOT, "Vertices.mat"))
+    grid = sio.loadmat(PurePath(PATH_PLOT, "grid.mat"))["grid"]
+    stn_surf = sio.loadmat(PurePath(PATH_PLOT, "STN_surf.mat"))
     x_ver = stn_surf["vertices"][::2, 0]
     y_ver = stn_surf["vertices"][::2, 1]
     x_ecog = vertices["Vertices"][::1, 0]
@@ -211,9 +191,9 @@ def save_features_and_settings(
     """
 
     # create out folder if doesn't exist
-    if not os.path.exists(os.path.join(out_path, folder_name)):
-        logger.Info(f"Creating output folder: {folder_name}")
-        os.makedirs(os.path.join(out_path, folder_name))
+    if not Path(out_path, folder_name).exists():
+        logger.info(f"Creating output folder: {folder_name}")
+        Path(out_path, folder_name).mkdir(parents=True)
 
     dict_sidecar = {"fs": fs, "coords": coords, "line_noise": line_noise}
 
@@ -230,17 +210,12 @@ def write_csv(df, path_out):
     Difference with pandas.df.to_csv() is that it does not
     write an index column by default
     """
-    csv.write_csv(pyarrow.Table.from_pandas(df), path_out)
+    pyarrow_csv.write_csv(pyarrow_Table.from_pandas(df), path_out)
 
 
-def save_settings(
-    settings: dict, path_out: _PathLike, folder_name: str | None = None
-) -> None:
-    path_out = _pathlike_to_str(path_out)
-    if folder_name is not None:
-        path_out = os.path.join(
-            path_out, folder_name, folder_name + "_SETTINGS.json"
-        )
+def save_settings(settings: dict, path_out: _PathLike, folder_name: str = "") -> None:
+    if folder_name:
+        path_out = PurePath(path_out, folder_name, folder_name + "_SETTINGS.json")
 
     with open(path_out, "w") as f:
         json.dump(settings, f, indent=4)
@@ -250,13 +225,10 @@ def save_settings(
 def save_nm_channels(
     nmchannels: pd.DataFrame,
     path_out: _PathLike,
-    folder_name: str | None = None,
+    folder_name: str = "",
 ) -> None:
-    path_out = _pathlike_to_str(path_out)
-    if folder_name is not None:
-        path_out = os.path.join(
-            path_out, folder_name, folder_name + "_nm_channels.csv"
-        )
+    if folder_name:
+        path_out = PurePath(path_out, folder_name, folder_name + "_nm_channels.csv")
     write_csv(nmchannels, path_out)
     logger.info(f"nm_channels.csv saved to {path_out}")
 
@@ -264,32 +236,26 @@ def save_nm_channels(
 def save_features(
     df_features: pd.DataFrame,
     path_out: _PathLike,
-    folder_name: str | None = None,
+    folder_name: str = "",
 ) -> None:
-    path_out = _pathlike_to_str(path_out)
-    if folder_name is not None:
-        path_out = os.path.join(
-            path_out, folder_name, folder_name + "_FEATURES.csv"
-        )
+    if folder_name:
+        path_out = PurePath(path_out, folder_name, folder_name + "_FEATURES.csv")
     write_csv(df_features, path_out)
     logger.info(f"FEATURES.csv saved to {str(path_out)}")
 
 
-def save_sidecar(
-    sidecar: dict, path_out: _PathLike, folder_name: str | None = None
-) -> None:
-    path_out = _pathlike_to_str(path_out)
+def save_sidecar(sidecar: dict, path_out: _PathLike, folder_name: str = "") -> None:
     save_general_dict(sidecar, path_out, "_SIDECAR.json", folder_name)
 
 
 def save_general_dict(
     dict_: dict,
     path_out: _PathLike,
-    str_add: str,
-    folder_name: str | None = None,
+    str_add: str = "",
+    folder_name: str = "",
 ) -> None:
-    if folder_name is not None:
-        path_out = os.path.join(path_out, folder_name, folder_name + str_add)
+    if folder_name:
+        path_out = PurePath(path_out, folder_name, folder_name + str_add)
 
     with open(path_out, "w") as f:
         json.dump(
@@ -299,10 +265,10 @@ def save_general_dict(
             indent=4,
             separators=(",", ": "),
         )
-    logger.info(f"{str_add} saved to " + str(path_out))
+    logger.info(f"{str_add} saved to {path_out}")
 
 
-def default_json_convert(obj) -> list | int | float:
+def default_json_convert(obj) -> list | float:
     if isinstance(obj, np.ndarray):
         return obj.tolist()
     if isinstance(obj, pd.DataFrame):
@@ -314,30 +280,33 @@ def default_json_convert(obj) -> list | int | float:
     raise TypeError("Not serializable")
 
 
-def read_sidecar(PATH: str) -> dict:
-    with open(PATH + "_SIDECAR.json") as f:
+def read_sidecar(PATH: _PathLike) -> dict:
+    with open(PurePath(str(PATH) + "_SIDECAR.json")) as f:
         return json.load(f)
 
 
-def read_settings(PATH: str) -> dict:
-    with open(PATH if ".json" in PATH else PATH + "_SETTINGS.json") as f:
+def read_settings(PATH: _PathLike) -> dict:
+    with open(PATH if ".json" in str(PATH) else str(PATH) + "_SETTINGS.json") as f:
         return json.load(f)
 
 
-def read_features(PATH: str) -> pd.DataFrame:
-    return pd.read_csv(PATH + "_FEATURES.csv", engine="pyarrow")
+def read_features(PATH: _PathLike) -> pd.DataFrame:
+    return pd.read_csv(str(PATH) + "_FEATURES.csv", engine="pyarrow")
 
 
-def read_nm_channels(PATH: str) -> pd.DataFrame:
-    return pd.read_csv(PATH + "_nm_channels.csv")
+def read_nm_channels(PATH: _PathLike) -> pd.DataFrame:
+    return pd.read_csv(str(PATH) + "_nm_channels.csv")
 
 
-def get_run_list_indir(PATH: str) -> list:
+def get_run_list_indir(PATH: _PathLike) -> list:
+    from os import walk
+
     f_files = []
-    for dirpath, _, files in os.walk(PATH):
+    # for dirpath, _, files in Path(PATH).walk(): # Only works in python >=3.12
+    for dirpath, _, files in walk(PATH):
         for x in files:
             if "FEATURES" in x:
-                f_files.append(os.path.basename(dirpath))
+                f_files.append(PurePath(dirpath).name)
     return f_files
 
 
@@ -348,7 +317,7 @@ def loadmat(filename) -> dict:
     from mat files. It calls the function check keys to cure all entries
     which are still mat-objects
     """
-    data = io.loadmat(filename, struct_as_record=False, squeeze_me=True)
+    data = sio.loadmat(filename, struct_as_record=False, squeeze_me=True)
     return _check_keys(data)
 
 
@@ -358,8 +327,6 @@ def get_paths_example_data():
     dataset used in most examples.
     """
 
-    SCRIPT_DIR = Path(py_neuromodulation.__file__).parent.absolute()
-
     sub = "testsub"
     ses = "EphysMedOff"
     task = "gripforce"
@@ -369,16 +336,9 @@ def get_paths_example_data():
     # Define run name and access paths in the BIDS format.
     RUN_NAME = f"sub-{sub}_ses-{ses}_task-{task}_run-{run}"
 
-    PATH_BIDS = Path(SCRIPT_DIR) / "data"
+    PATH_BIDS = PYNM_DIR / "data"
 
-    PATH_RUN = (
-        Path(SCRIPT_DIR)
-        / "data"
-        / f"sub-{sub}"
-        / f"ses-{ses}"
-        / datatype
-        / RUN_NAME
-    )
+    PATH_RUN = PYNM_DIR / "data" / f"sub-{sub}" / f"ses-{ses}" / datatype / RUN_NAME
 
     # Provide a path for the output data.
     PATH_OUT = PATH_BIDS / "derivatives"
@@ -392,7 +352,7 @@ def _check_keys(dict):
     todict is called to change them to nested dictionaries
     """
     for key in dict:
-        if isinstance(dict[key], io.matlab.mio5_params.mat_struct):
+        if isinstance(dict[key], sio.matlab.mio5_params.mat_struct):
             dict[key] = _todict(dict[key])
     return dict
 
@@ -404,14 +364,8 @@ def _todict(matobj) -> dict:
     dict = {}
     for strg in matobj._fieldnames:
         elem = matobj.__dict__[strg]
-        if isinstance(elem, io.matlab.mio5_params.mat_struct):
+        if isinstance(elem, sio.matlab.mio5_params.mat_struct):
             dict[strg] = _todict(elem)
         else:
             dict[strg] = elem
     return dict
-
-
-def _pathlike_to_str(path: _PathLike) -> str:
-    if isinstance(path, str):
-        return path
-    return str(path)
diff --git a/py_neuromodulation/nm_RMAP.py b/py_neuromodulation/nm_RMAP.py
index b8ae881e..758f394b 100644
--- a/py_neuromodulation/nm_RMAP.py
+++ b/py_neuromodulation/nm_RMAP.py
@@ -1,19 +1,17 @@
 import numpy as np
-import os
-import wget
+from pathlib import PurePath, Path
+from urllib.request import urlretrieve
 
 
 # from numba import jit
-from scipy import stats
 import scipy.io as sio
 import pandas as pd
-from typing import Union, Tuple, List
 import nibabel as nib
 from matplotlib import pyplot as plt
 
-import py_neuromodulation
-
-from py_neuromodulation import nm_plots
+from py_neuromodulation.nm_plots import reg_plot
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import PYNM_DIR
 
 LIST_STRUC_UNCONNECTED_GRIDPOINTS_HULL = [256, 385, 417, 447, 819, 914]
 LIST_STRUC_UNCONNECTED_GRIDPOINTS_WHOLEBRAIN = [
@@ -79,7 +77,6 @@
 
 
 class ConnectivityChannelSelector:
-
     def __init__(
         self,
         whole_brain_connectome: bool = True,
@@ -105,18 +102,15 @@ def __init__(
         self.whole_brain_connectome = whole_brain_connectome
         self.func_connectivity = func_connectivity
 
-        self.PATH_CONN_DECODING = os.path.join(
-            py_neuromodulation.__path__[0],
-            "ConnectivityDecoding",
-        )
+        self.PATH_CONN_DECODING = PYNM_DIR / "ConnectivityDecoding"
 
         if whole_brain_connectome:
-            self.PATH_GRID = os.path.join(
+            self.PATH_GRID = PurePath(
                 self.PATH_CONN_DECODING,
                 "mni_coords_whole_brain.mat",
             )
             self.grid = sio.loadmat(self.PATH_GRID)["downsample_ctx"]
-            if func_connectivity is False:
+            if not func_connectivity:
                 # reduce the grid to only valid points that are not in LIST_STRUC_UNCONNECTED_GRIDPOINTS_WHOLEBRAIN
                 self.grid = np.delete(
                     self.grid,
@@ -124,12 +118,12 @@ def __init__(
                     axis=0,
                 )
         else:
-            self.PATH_GRID = os.path.join(
+            self.PATH_GRID = PurePath(
                 self.PATH_CONN_DECODING,
                 "mni_coords_cortical_surface.mat",
             )
             self.grid = sio.loadmat(self.PATH_GRID)["downsample_ctx"]
-            if func_connectivity is False:
+            if not func_connectivity:
                 # reduce the grid to only valid points that are not in LIST_STRUC_UNCONNECTED_GRIDPOINTS_HULL
                 self.grid = np.delete(
                     self.grid, LIST_STRUC_UNCONNECTED_GRIDPOINTS_HULL, axis=0
@@ -137,17 +131,14 @@ def __init__(
 
         if func_connectivity:
             self.RMAP_arr = nib.load(
-                os.path.join(self.PATH_CONN_DECODING, "RMAP_func_all.nii")
+                PurePath(self.PATH_CONN_DECODING, "RMAP_func_all.nii")
             ).get_fdata()
         else:
             self.RMAP_arr = nib.load(
-                os.path.join(self.PATH_CONN_DECODING, "RMAP_struc.nii")
+                PurePath(self.PATH_CONN_DECODING, "RMAP_struc.nii")
             ).get_fdata()
 
-    def _get_connectome_name(
-        self, whole_brain_connectome: str, func_connectivity: str
-    ):
-
+    def _get_connectome_name(self, whole_brain_connectome: str, func_connectivity: str):
         connectome_name = "connectome_"
         if whole_brain_connectome:
             connectome_name += "whole_brain_"
@@ -167,37 +158,28 @@ def get_available_connectomes(self) -> list:
         -------
         list_connectomes: list
         """
-        return os.listdir(
-            os.path.join(
-                self.PATH_CONN_DECODING,
-                "connectome_folder",
-            )
-        )
+        return list(Path(self.PATH_CONN_DECODING, "connectome_folder").iterdir())
 
     def plot_grid(self) -> None:
         """Plot the loaded template grid that passed coordinates are matched to."""
 
         fig = plt.figure()
         ax = fig.add_subplot(111, projection="3d")
-        ax.scatter(
-            self.grid[:, 0], self.grid[:, 1], self.grid[:, 2], s=50, alpha=0.2
-        )
+        ax.scatter(self.grid[:, 0], self.grid[:, 1], self.grid[:, 2], s=50, alpha=0.2)
         plt.show()
 
-    def get_closest_node(
-        self, coord: Union[List, np.array]
-    ) -> Tuple[List, List]:
+    def get_closest_node(self, coord: list | np.ndarray) -> tuple[list, list]:
         """Given a list or np.array of coordinates, return the closest nodes in the
         grid and their indices.
 
         Parameters
         ----------
-        coord : np.array
+        coord : np.ndarray
             MNI coordinates with shape (num_channels, 3)
 
         Returns
         -------
-        Tuple[List, List]
+        Tuple[list, list]
             Grid coordinates, grid indices
         """
 
@@ -209,15 +191,15 @@ def get_closest_node(
         return [self.grid[idx] for idx in idx_], idx_
 
     def get_rmap_correlations(
-        self, fps: Union[list, np.array], RMAP_use: np.array = None
-    ) -> List:
+        self, fps: list | np.ndarray, RMAP_use: np.ndarray | None = None
+    ) -> list:
         """Calculate correlations of passed fingerprints with the RMAP
 
         Parameters
         ----------
         fps : Union[list, np.array]
             List of fingerprints
-        RMAP_use : np.array, optional
+        RMAP_use : np.ndarray, optional
             Passed RMAP, by default None
 
         Returns
@@ -235,8 +217,8 @@ def get_rmap_correlations(
 
     def load_connectome(
         self,
-        whole_brain_connectome: bool = None,
-        func_connectivity: bool = None,
+        whole_brain_connectome: bool | None = None,
+        func_connectivity: bool | None = None,
     ) -> None:
         """Load connectome, if not available download connectome from
         Zenodo.
@@ -259,13 +241,13 @@ def load_connectome(
             self.whole_brain_connectome, self.func_connectivity
         )
 
-        PATH_CONNECTOME = os.path.join(
+        PATH_CONNECTOME = Path(
             self.PATH_CONN_DECODING,
             "connectome_folder",
             self.connectome_name + ".mat",
         )
 
-        if os.path.exists(PATH_CONNECTOME) is False:
+        if not PATH_CONNECTOME.exists():
             user_input = input(
                 "Do you want to download the connectome? (yes/no): "
             ).lower()
@@ -276,7 +258,7 @@ def load_connectome(
 
         self.connectome = sio.loadmat(PATH_CONNECTOME)
 
-    def get_grid_fingerprints(self, grid_idx: Union[list, np.array]) -> list:
+    def get_grid_fingerprints(self, grid_idx: list | np.ndarray) -> list:
         return [self.connectome[str(grid_idx)] for grid_idx in grid_idx]
 
     def download_connectome(
@@ -288,18 +270,16 @@ def download_connectome(
         record_id = "10804702"
         file_name = self.connectome_name
 
-        wget.download(
+        filepath = Path(self.PATH_CONN_DECODING, "connectome_folder")
+        filepath.mkdir(parents=True, exist_ok=True)
+
+        urlretrieve(
             f"https://zenodo.org/api/records/{record_id}/files/{file_name}/content",
-            out=os.path.join(
-                self.PATH_CONN_DECODING,
-                "connectome_folder",
-                f"{self.connectome_name}.mat",
-            ),
+            filepath / f"{self.connectome_name}.mat",
         )
 
 
 class RMAPCross_Val_ChannelSelector:
-
     def __init__(self) -> None:
         pass
 
@@ -310,53 +290,48 @@ def load_fingerprint(self, path_nii) -> None:
         fp = epi_img.get_fdata()
         return fp
 
-    def load_all_fingerprints(
-        self, path_dir: str, cond_str: str = "_AvgR_Fz.nii"
-    ):
-
+    def load_all_fingerprints(self, path_dir: str, cond_str: str = "_AvgR_Fz.nii"):
         if cond_str is not None:
-            l_fps = list(filter(lambda k: cond_str in k, os.listdir(path_dir)))
+            l_fps = list(filter(lambda k: cond_str in str(k), Path(path_dir).iterdir()))
         else:
-            l_fps = os.listdir(path_dir)
+            l_fps = list(Path(path_dir).iterdir())
 
-        return l_fps, [
-            self.load_fingerprint(os.path.join(path_dir, f)) for f in l_fps
-        ]
+        return l_fps, [self.load_fingerprint(PurePath(path_dir, f)) for f in l_fps]
 
     def get_fingerprints_from_path_with_cond(
         self,
-        path_dir: str,
-        str_to_omit: str = None,
-        str_to_keep: str = None,
+        path_dir: _PathLike,
+        str_to_omit: str = "",
+        str_to_keep: str = "",
         keep: bool = True,
-    ):
+    ) -> tuple[list, list]:
+        l_fps = []
 
-        if keep:
+        if keep and str_to_keep:
             l_fps = list(
                 filter(
-                    lambda k: "_AvgR_Fz.nii" in k and str_to_keep in k,
-                    os.listdir(path_dir),
+                    lambda k: "_AvgR_Fz.nii" in str(k) and str_to_keep in str(k),
+                    Path(path_dir).iterdir(),
                 )
             )
-        else:
+
+        elif not keep and str_to_omit:
             l_fps = list(
                 filter(
-                    lambda k: "_AvgR_Fz.nii" in k and str_to_omit not in k,
-                    os.listdir(path_dir),
+                    lambda k: "_AvgR_Fz.nii" in str(k) and str_to_omit not in str(k),
+                    Path(path_dir).iterdir(),
                 )
             )
-        return l_fps, [
-            self.load_fingerprint(os.path.join(path_dir, f)) for f in l_fps
-        ]
+
+        return l_fps, [self.load_fingerprint(PurePath(path_dir, f)) for f in l_fps]
 
     @staticmethod
     def save_Nii(
-        fp: np.array,
-        affine: np.array,
+        fp: np.ndarray,
+        affine: np.ndarray,
         name: str = "img.nii",
         reshape: bool = True,
     ):
-
         if reshape:
             fp = np.reshape(fp, (91, 109, 91), order="F")
 
@@ -364,13 +339,12 @@ def save_Nii(
 
         nib.save(img, name)
 
-    def get_RMAP(self, X: np.array, y: np.array):
+    def get_RMAP(self, X: np.ndarray, y: np.ndarray):
         # faster than calculate_RMap_numba
         # https://stackoverflow.com/questions/71252740/correlating-an-array-row-wise-with-a-vector/71253141#71253141
 
         r = (
-            len(y) * np.sum(X * y[None, :], axis=-1)
-            - (np.sum(X, axis=-1) * np.sum(y))
+            len(y) * np.sum(X * y[None, :], axis=-1) - (np.sum(X, axis=-1) * np.sum(y))
         ) / (
             np.sqrt(
                 (len(y) * np.sum(X**2, axis=-1) - np.sum(X, axis=-1) ** 2)
@@ -407,9 +381,7 @@ def get_corr_numba(fp, fp_test):
         val = np.corrcoef(fp_test, fp)[0][1]
         return val
 
-    def leave_one_ch_out_cv(
-        self, l_fps_names: list, l_fps_dat: list, l_per: list
-    ):
+    def leave_one_ch_out_cv(self, l_fps_names: list, l_fps_dat: list, l_per: list):
         # l_fps_dat is not flattened
 
         per_left_out = []
@@ -432,9 +404,7 @@ def leave_one_ch_out_cv(
 
             per_predict.append(
                 np.nan_to_num(
-                    self.get_corr_numba(
-                        rmap_cv, l_fps_dat[idx_left_out].flatten()
-                    )
+                    self.get_corr_numba(rmap_cv, l_fps_dat[idx_left_out].flatten())
                 )
             )
             per_left_out.append(l_per[idx_left_out])
@@ -451,13 +421,9 @@ def leave_one_sub_out_cv(
 
         for subject_test in sub_list:
             # print(subject_test)
-            idx_test = [
-                idx for idx, f in enumerate(l_fps_names) if subject_test in f
-            ]
+            idx_test = [idx for idx, f in enumerate(l_fps_names) if subject_test in f]
             idx_train = [
-                idx
-                for idx, f in enumerate(l_fps_names)
-                if subject_test not in f
+                idx for idx, f in enumerate(l_fps_names) if subject_test not in f
             ]
             l_cv = list(np.array(l_fps_dat)[idx_train])
             per_cv = list(np.array(l_per)[idx_train])
@@ -485,7 +451,6 @@ def get_highest_corr_sub_ch(
         cohorts_train: dict,
         path_dir: str = r"C:\Users\ICN_admin\OneDrive - Charité - Universitätsmedizin Berlin\Connectomics\DecodingToolbox_BerlinPittsburgh_Beijing\functional_connectivity",
     ):
-
         fp_test = self.get_fingerprints_from_path_with_cond(
             path_dir=path_dir,
             str_to_keep=f"{cohort_test}_{sub_test}_ROI_{ch_test}",
@@ -505,9 +470,7 @@ def get_highest_corr_sub_ch(
                 )
 
                 for fp, fp_name in zip(fps, fps_name):
-                    ch = fp_name[
-                        fp_name.find("ROI") + 4 : fp_name.find("func") - 1
-                    ]
+                    ch = fp_name[fp_name.find("ROI") + 4 : fp_name.find("func") - 1]
                     corr_val = self.get_corr_numba(fp_test, fp)
                     fp_pairs.append([cohort, sub, ch, corr_val])
 
@@ -515,7 +478,7 @@ def get_highest_corr_sub_ch(
         return fp_pairs[idx_max][0:3]
 
     def plot_performance_prediction_correlation(
-        per_left_out, per_predict, out_path_save: str = None
+        per_left_out, per_predict, out_path_save: str | None = None
     ):
         df_plt_corr = pd.DataFrame()
         df_plt_corr["test_performance"] = per_left_out
@@ -523,7 +486,7 @@ def plot_performance_prediction_correlation(
             per_predict  # change "struct" with "funct" for functional connectivity
         )
 
-        nm_plots.reg_plot(
+        reg_plot(
             x_col="test_performance",
             y_col="struct_conn_predict",
             data=df_plt_corr,
diff --git a/py_neuromodulation/nm_across_patient_decoding.py b/py_neuromodulation/nm_across_patient_decoding.py
deleted file mode 100644
index 14ce9788..00000000
--- a/py_neuromodulation/nm_across_patient_decoding.py
+++ /dev/null
@@ -1,927 +0,0 @@
-import numpy as np
-import os
-import pandas as pd
-from sklearn import metrics, linear_model, model_selection
-
-import py_neuromodulation
-from py_neuromodulation import nm_decode, nm_RMAP
-
-
-class AcrossPatientRunner:
-    def __init__(
-        self,
-        outpath: str,
-        model=linear_model.LogisticRegression(class_weight="balanced"),
-        TRAIN_VAL_SPLIT=False,
-        eval_method=metrics.balanced_accuracy_score,
-        cv_method=model_selection.KFold(n_splits=3, shuffle=False),
-        VERBOSE=False,
-        use_nested_cv=True,
-        RUN_BAY_OPT=False,
-        ML_model_name="LM",
-        cohorts: list = None,
-        load_channel_all: bool = False,
-        load_grid_point_all: bool = False,
-    ) -> None:
-
-        self.outpath = outpath
-        self.model = model
-        self.TRAIN_VAL_SPLIT = TRAIN_VAL_SPLIT
-        self.use_nested_cv = use_nested_cv
-        self.RUN_BAY_OPT = RUN_BAY_OPT
-        self.VERBOSE = VERBOSE
-        self.eval_method = eval_method
-        self.ML_model_name = ML_model_name
-        self.cv_method = cv_method
-        self.cohorts = cohorts
-
-        self.grid_cortex = pd.read_csv(
-            os.path.join(py_neuromodulation.__path__[0], "grid_cortex.tsv"),
-            sep="\t",
-        ).to_numpy()
-
-        self.RMAPSelector = nm_RMAP.RMAPChannelSelector()
-
-        if load_channel_all is True:
-            self.ch_all = np.load(
-                os.path.join(self.outpath, "channel_all.npy"),
-                allow_pickle="TRUE",
-            ).item()
-        if load_grid_point_all is True:
-            self.grid_point_all = np.load(
-                os.path.join(self.outpath, "grid_point_all.npy"),
-                allow_pickle="TRUE",
-            ).item()
-
-    def init_decoder(self) -> nm_decode.Decoder:
-
-        return nm_decode.Decoder(
-            model=self.model,
-            TRAIN_VAL_SPLIT=self.TRAIN_VAL_SPLIT,
-            get_movement_detection_rate=True,
-            eval_method=self.eval_method,
-            VERBOSE=self.VERBOSE,
-            cv_method=self.cv_method,
-            use_nested_cv=self.use_nested_cv,
-            RUN_BAY_OPT=self.RUN_BAY_OPT,
-        )
-
-    def eval_model(self, X_train, y_train, X_test, y_test):
-
-        return self.decoder.wrapper_model_train(
-            X_train,
-            y_train,
-            X_test,
-            y_test,
-            cv_res=nm_decode.CV_res(get_movement_detection_rate=True),
-        )
-
-    @staticmethod
-    def get_data_sub_ch(channel_all, cohort, sub, ch):
-
-        X_train = []
-        y_train = []
-
-        for f in channel_all[cohort][sub][ch].keys():
-            X_train.append(channel_all[cohort][sub][ch][f]["data"])
-            y_train.append(channel_all[cohort][sub][ch][f]["label"])
-        if len(X_train) > 1:
-            X_train = np.concatenate(X_train, axis=0)
-            y_train = np.concatenate(y_train, axis=0)
-        else:
-            X_train = X_train[0]
-            y_train = y_train[0]
-
-        return X_train, y_train
-
-    def get_patients_train_dict(self, sub_test, cohort_test, val_approach: str, data_select: dict):
-        cohorts_train = {}
-        for cohort in self.cohorts:
-            if val_approach == "leave_1_cohort_out" and cohort == cohort_test:
-                continue
-            if (
-                val_approach == "leave_1_sub_out_within_coh"
-                and cohort != cohort_test
-            ):
-                continue
-            cohorts_train[cohort] = []
-            for sub in data_select[cohort]:
-                if (
-                    val_approach == "leave_1_sub_out_within_coh"
-                    and sub == sub_test
-                    and cohort == cohort_test
-                ):
-                    continue
-                if (
-                    val_approach == "leave_1_sub_out_across_coh"
-                    and sub == sub_test
-                ):
-                    continue
-                cohorts_train[cohort].append(sub)
-        return cohorts_train
-
-    def get_data_grid_point(
-        self, sub_test: str, cohort_test: str, best_gp_list: list
-    ):
-        for gp in best_gp_list:
-            if gp in self.grid_point_all[cohort_test][sub_test]:
-                X_test, y_test = self.get_data_sub_ch(
-                    self.grid_point_all, cohort_test, sub_test, gp
-                )
-                break
-            else:
-                continue
-        return X_test, y_test
-
-    def get_data_channels(self, sub_test: str, cohort_test: str, df_rmap: list):
-        ch_test = df_rmap.query("cohort == @cohort_test and sub == @sub_test")[
-            "ch"
-        ].iloc[0]
-        X_test, y_test = self.get_data_sub_ch(
-            self.ch_all, cohort_test, sub_test, ch_test
-        )
-        return X_test, y_test
-
-    def cross_val_approach_RMAP(
-        self,
-        val_approach: str = "leave_1_cohort_out",
-        df_select: pd.DataFrame = None,
-        select_best_gp: bool = False,
-        add_UPDRS: bool = False,
-        df_updrs: pd.DataFrame = None,
-    ):
-
-        if select_best_gp is True:
-            best_gp_list = list(
-                df_select.sort_values("performance_test", ascending=False)["ch"]
-            )
-            data_select = self.grid_point_all
-        else:
-            data_select = self.ch_all
-
-        p_ = {}
-        for cohort_test in self.cohorts:
-            print(cohort_test)
-            if cohort_test not in p_:
-                p_[cohort_test] = {}
-            for sub_test in data_select[cohort_test].keys():
-                print(sub_test)
-                if sub_test not in p_[cohort_test]:
-                    p_[cohort_test][sub_test] = {}
-                if select_best_gp is True:
-                    X_test, y_test = self.get_data_grid_point(
-                        sub_test, cohort_test, best_gp_list
-                    )
-                else:
-                    X_test, y_test = self.get_data_channels(
-                        sub_test, cohort_test, df_rmap=df_select
-                    )
-
-                if add_UPDRS is True:
-                    updrs = df_updrs.query(
-                        "sub == @sub_test and cohort == @cohort_test"
-                    ).iloc[0]["UPDRS_total"]
-                    if np.isnan(updrs):
-                        continue
-                    X_test = np.concatenate(
-                        (
-                            X_test,
-                            np.expand_dims(
-                                np.repeat(updrs, repeats=X_test.shape[0]),
-                                axis=1,
-                            ),
-                        ),
-                        axis=1,
-                    )
-
-                cohorts_train = self.get_patients_train_dict(
-                    sub_test, cohort_test, val_approach=val_approach, data_select=data_select
-                )
-
-                X_train_comb = []
-                y_train_comb = []
-                for cohort_train in list(cohorts_train.keys()):
-                    for sub_train in cohorts_train[cohort_train]:
-                        if select_best_gp is True:
-                            X_train, y_train = self.get_data_grid_point(
-                                sub_train, cohort_train, best_gp_list
-                            )
-                        else:
-                            X_train, y_train = self.get_data_channels(
-                                sub_train, cohort_train, df_rmap=df_select
-                            )
-                        if add_UPDRS is True:
-                            updrs = df_updrs.query(
-                                "sub == @sub_train and cohort == @cohort_train"
-                            ).iloc[0]["UPDRS_total"]
-                            if np.isnan(
-                                updrs
-                            ):  # the returned True is here not boolean but np boolean
-                                continue
-                            X_train = np.concatenate(
-                                (
-                                    X_train,
-                                    np.expand_dims(
-                                        np.repeat(
-                                            updrs, repeats=X_train.shape[0]
-                                        ),
-                                        axis=1,
-                                    ),
-                                ),
-                                axis=1,
-                            )
-
-                        X_train_comb.append(X_train)
-                        y_train_comb.append(y_train)
-                if len(X_train_comb) > 1:
-                    X_train = np.concatenate(X_train_comb, axis=0)
-                    y_train = np.concatenate(y_train_comb, axis=0)
-                else:
-                    X_train = X_train_comb[0]
-                    y_train = X_train_comb[0]
-
-                self.decoder = self.init_decoder()
-
-                # X_train, y_train, X_test, y_test = self.decoder.append_samples_val(X_train, y_train, X_test, y_test, 5)
-
-                model = self.decoder.wrapper_model_train(
-                    X_train=X_train,
-                    y_train=y_train,
-                    return_fitted_model_only=True,
-                )
-                cv_res = self.decoder.eval_model(
-                    model,
-                    X_train,
-                    X_test,
-                    y_train,
-                    y_test,
-                    cv_res=nm_decode.CV_res(get_movement_detection_rate=True),
-                    save_data=False,
-                )
-                p_[cohort_test][sub_test] = cv_res
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name + f"_performance_{val_approach}_RMAP.npy",
-            ),
-            p_,
-        )
-
-    def cross_val_p2p_RMAP(
-        self,
-        df_select: pd.DataFrame = None,
-        select_best_gp: bool = False,
-    ):
-        if select_best_gp is True:
-            best_gp_list = list(
-                df_select.sort_values("performance_test", ascending=False)["ch"]
-            )
-            data_select = self.grid_point_all
-        else:
-            data_select = self.ch_all
-
-        p_ = {}
-        for cohort_test in self.cohorts:
-            print(cohort_test)
-            if cohort_test not in p_:
-                p_[cohort_test] = {}
-            for sub_test in data_select[cohort_test].keys():
-                print(sub_test)
-                if sub_test not in p_[cohort_test]:
-                    p_[cohort_test][sub_test] = {}
-
-                if select_best_gp is True:
-                    X_test, y_test = self.get_data_grid_point(
-                        sub_test, cohort_test, best_gp_list
-                    )
-                else:
-                    X_test, y_test = self.get_data_channels(
-                        sub_test, cohort_test, df_rmap=df_select
-                    )
-                for cohort_train in self.cohorts:
-                    if cohort_train not in p_[cohort_test][sub_test]:
-                        p_[cohort_test][sub_test][cohort_train] = {}
-                    for sub_train in list(data_select[cohort_train].keys()):
-                        if (
-                            sub_train
-                            not in p_[cohort_test][sub_test][cohort_train]
-                        ):
-                            p_[cohort_test][sub_test][cohort_train][
-                                sub_train
-                            ] = {}
-
-                        if select_best_gp is True:
-                            X_train, y_train = self.get_data_grid_point(
-                                sub_train, cohort_train, best_gp_list
-                            )
-                        else:
-                            X_train, y_train = self.get_data_channels(
-                                sub_train, cohort_train, df_rmap=df_select
-                            )
-                        self.decoder = self.init_decoder()
-
-                        # X_train, y_train, X_test, y_test = self.decoder.append_samples_val(X_train, y_train, X_test, y_test, 5)
-
-                        model = self.decoder.wrapper_model_train(
-                            X_train=X_train,
-                            y_train=y_train,
-                            return_fitted_model_only=True,
-                        )
-                        cv_res = self.decoder.eval_model(
-                            model,
-                            X_train,
-                            X_test,
-                            y_train,
-                            y_test,
-                            cv_res=nm_decode.CV_res(
-                                get_movement_detection_rate=True
-                            ),
-                            save_data=False,
-                        )
-                        p_[cohort_test][sub_test][cohort_train][
-                            sub_train
-                        ] = cv_res
-
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name + f"_performance_p2p_RMAP.npy",
-            ),
-            p_,
-        )
-
-    def leave_one_patient_out_RMAP(
-        self, val_approach: str = "leave_1_cohort_out"
-    ):
-        p_ = {}
-        for cohort_test in self.cohorts:
-            if cohort_test not in p_:
-                p_[cohort_test] = {}
-            for sub_test in self.ch_all[cohort_test].keys():
-                if sub_test not in p_:
-                    p_[cohort_test][sub_test] = {}
-                for ch_test in self.ch_all[cohort_test][sub_test].keys():
-                    if ch_test not in p_[cohort_test][sub_test]:
-                        p_[cohort_test][sub_test][ch_test] = {}
-
-                    cohorts_train = self.get_patients_train_dict(
-                        sub_test, cohort_test, val_approach=val_approach
-                    )
-
-                    (
-                        cohort_train,
-                        sub_train,
-                        ch_train,
-                    ) = self.RMAPSelector.get_highest_corr_sub_ch(
-                        cohort_test,
-                        sub_test,
-                        ch_test,
-                        cohorts_train,
-                        path_dir=r"C:\Users\ICN_admin\OneDrive - Charité - Universitätsmedizin Berlin\Connectomics\DecodingToolbox_BerlinPittsburgh_Beijing\functional_connectivity",
-                    )
-
-                    X_train, y_train = self.get_data_sub_ch(
-                        self.ch_all, cohort_train, sub_train, ch_train
-                    )
-                    X_test, y_test = self.get_data_sub_ch(
-                        self.ch_all, cohort_test, sub_test, ch_test
-                    )
-
-                    self.decoder = self.init_decoder()
-
-                    model = self.decoder.wrapper_model_train(
-                        X_train=X_train,
-                        y_train=y_train,
-                        return_fitted_model_only=True,
-                    )
-                    cv_res = self.decoder.eval_model(
-                        model,
-                        X_train,
-                        X_test,
-                        y_train,
-                        y_test,
-                        cv_res=nm_decode.CV_res(
-                            get_movement_detection_rate=True
-                        ),
-                        save_data=False,
-                        append_samples=True,
-                    )
-                    p_[cohort_test][sub_test][ch_test] = cv_res
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name
-                + "_performance_leave_one_cohort_out_RMAP.npy",
-            ),
-            p_,
-        )
-
-    def run_cohort_leave_one_patient_out_CV_within_cohort(self):
-
-        grid_point_all = np.load(
-            os.path.join(self.outpath, "grid_point_all.npy"),
-            allow_pickle="TRUE",
-        ).item()
-        performance_leave_one_patient_out = {}
-
-        for cohort in self.cohorts:
-            print("cohort: " + str(cohort))
-            performance_leave_one_patient_out[cohort] = {}
-
-            for grid_point in list(grid_point_all.keys()):
-                print("grid point: " + str(grid_point))
-                if cohort not in grid_point_all[grid_point]:
-                    continue
-                if len(list(grid_point_all[grid_point][cohort].keys())) <= 1:
-                    continue  # cannot do leave one out prediction with a single subject
-                performance_leave_one_patient_out[cohort][grid_point] = {}
-
-                for subject_test in list(
-                    grid_point_all[grid_point][cohort].keys()
-                ):
-                    X_test = []
-                    y_test = []
-                    for run in list(
-                        grid_point_all[grid_point][cohort][subject_test].keys()
-                    ):
-                        if (
-                            grid_point_all[grid_point][cohort][subject_test][
-                                run
-                            ]["lat"]
-                            != "CON"
-                        ):
-                            continue
-                        X_test.append(
-                            grid_point_all[grid_point][cohort][subject_test][
-                                run
-                            ]["data"]
-                        )
-                        y_test.append(
-                            grid_point_all[grid_point][cohort][subject_test][
-                                run
-                            ]["label"]
-                        )
-                    if len(X_test) > 1:
-                        X_test = np.concatenate(X_test, axis=0)
-                        y_test = np.concatenate(y_test, axis=0)
-                    else:
-                        X_test = X_test[0]
-                        y_test = y_test[0]
-                    X_train = []
-                    y_train = []
-                    for subject_train in list(
-                        grid_point_all[grid_point][cohort].keys()
-                    ):
-                        if subject_test == subject_train:
-                            continue
-                        for run in list(
-                            grid_point_all[grid_point][cohort][
-                                subject_train
-                            ].keys()
-                        ):
-                            if (
-                                grid_point_all[grid_point][cohort][
-                                    subject_train
-                                ][run]["lat"]
-                                != "CON"
-                            ):
-                                continue
-                            X_train.append(
-                                grid_point_all[grid_point][cohort][
-                                    subject_train
-                                ][run]["data"]
-                            )
-                            y_train.append(
-                                grid_point_all[grid_point][cohort][
-                                    subject_train
-                                ][run]["label"]
-                            )
-                    if len(X_train) > 1:
-                        X_train = np.concatenate(X_train, axis=0)
-                        y_train = np.concatenate(y_train, axis=0)
-                    else:
-                        X_train = X_train[0]
-                        y_train = y_train[0]
-
-                    # run here ML estimation
-                    self.decoder = self.init_decoder()
-                    model = self.decoder.wrapper_model_train(
-                        X_train=X_train,
-                        y_train=y_train,
-                        return_fitted_model_only=True,
-                    )
-                    # use initialized decoder
-                    try:
-                        cv_res = self.eval_model(
-                            X_train, y_train, X_test, y_test
-                        )
-                    except nm_decode.Decoder.ClassMissingException:
-                        continue
-
-                    performance_leave_one_patient_out[cohort][grid_point][
-                        subject_test
-                    ] = cv_res
-
-        performance_leave_one_patient_out["grid_cortex"] = self.grid_cortex
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name
-                + "_performance_leave_one_patient_out_within_cohort.npy",
-            ),
-            performance_leave_one_patient_out,
-        )
-        return performance_leave_one_patient_out
-
-    def run_cohort_leave_one_cohort_out_CV(self):
-        grid_point_all = np.load(
-            os.path.join(self.outpath, "grid_point_all.npy"),
-            allow_pickle="TRUE",
-        ).item()
-        performance_leave_one_cohort_out = {}
-
-        for cohort_test in self.cohorts:
-            print("cohort: " + str(cohort_test))
-            if cohort_test not in performance_leave_one_cohort_out:
-                performance_leave_one_cohort_out[cohort_test] = {}
-
-            for grid_point in list(grid_point_all.keys()):
-                print("grid point: " + str(grid_point))
-                if cohort_test not in grid_point_all[grid_point]:
-                    continue
-                if len(list(grid_point_all[grid_point].keys())) == 1:
-                    continue  # cannot do leave one cohort prediction with a single cohort
-
-                X_train = []
-                y_train = []
-                for cohort_train in self.cohorts:
-                    if cohort_test == cohort_train:
-                        continue
-                    if cohort_train not in grid_point_all[grid_point]:
-                        continue
-                    for subject_test in list(
-                        grid_point_all[grid_point][cohort_train].keys()
-                    ):
-                        for run in list(
-                            grid_point_all[grid_point][cohort_train][
-                                subject_test
-                            ].keys()
-                        ):
-                            if (
-                                grid_point_all[grid_point][cohort_train][
-                                    subject_test
-                                ][run]["lat"]
-                                != "CON"
-                            ):
-                                continue
-                            X_train.append(
-                                grid_point_all[grid_point][cohort_train][
-                                    subject_test
-                                ][run]["data"]
-                            )
-                            y_train.append(
-                                grid_point_all[grid_point][cohort_train][
-                                    subject_test
-                                ][run]["label"]
-                            )
-                if len(X_train) > 1:
-                    X_train = np.concatenate(X_train, axis=0)
-                    y_train = np.concatenate(y_train, axis=0)
-                else:
-                    X_train = X_train[0]
-                    y_train = y_train[0]
-
-                # run here ML estimation
-                self.decoder = self.init_decoder()
-                model = self.decoder.wrapper_model_train(
-                    X_train=X_train,
-                    y_train=y_train,
-                    return_fitted_model_only=True,
-                )
-
-                performance_leave_one_cohort_out[cohort_test][grid_point] = {}
-                for subject_test in list(
-                    grid_point_all[grid_point][cohort_test].keys()
-                ):
-                    X_test = []
-                    y_test = []
-                    for run in list(
-                        grid_point_all[grid_point][cohort_test][
-                            subject_test
-                        ].keys()
-                    ):
-                        if (
-                            grid_point_all[grid_point][cohort_test][
-                                subject_test
-                            ][run]["lat"]
-                            != "CON"
-                        ):
-                            continue
-                        X_test.append(
-                            grid_point_all[grid_point][cohort_test][
-                                subject_test
-                            ][run]["data"]
-                        )
-                        y_test.append(
-                            grid_point_all[grid_point][cohort_test][
-                                subject_test
-                            ][run]["label"]
-                        )
-                    if len(X_test) > 1:
-                        X_test = np.concatenate(X_test, axis=0)
-                        y_test = np.concatenate(y_test, axis=0)
-                    else:
-                        X_test = X_test[0]
-                        y_test = y_test[0]
-
-                    cv_res = self.decoder.eval_model(
-                        model,
-                        X_train,
-                        X_test,
-                        y_train,
-                        y_test,
-                        cv_res=nm_decode.CV_res(),
-                    )
-
-                    performance_leave_one_cohort_out[cohort_test][grid_point][
-                        subject_test
-                    ] = cv_res
-
-        performance_leave_one_cohort_out["grid_cortex"] = self.grid_cortex
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name + "_performance_leave_one_cohort_out.npy",
-            ),
-            performance_leave_one_cohort_out,
-        )
-
-    def run_leave_one_patient_out_across_cohorts(self):
-
-        grid_point_all = np.load(
-            os.path.join(self.outpath, "grid_point_all.npy"),
-            allow_pickle="TRUE",
-        ).item()
-        performance_leave_one_patient_out = {}
-
-        for grid_point in list(grid_point_all.keys()):
-            print("grid point: " + str(grid_point))
-            for cohort in self.cohorts:
-                print("cohort: " + str(cohort))
-                if cohort not in performance_leave_one_patient_out:
-                    performance_leave_one_patient_out[cohort] = {}
-
-                if cohort not in grid_point_all[grid_point]:
-                    continue
-                if len(list(grid_point_all[grid_point][cohort].keys())) <= 1:
-                    continue  # cannot do leave one out prediction with a single subject
-
-                if grid_point not in performance_leave_one_patient_out[cohort]:
-                    performance_leave_one_patient_out[cohort][grid_point] = {}
-
-                for subject_test in list(
-                    grid_point_all[grid_point][cohort].keys()
-                ):
-                    X_test = []
-                    y_test = []
-                    for run in list(
-                        grid_point_all[grid_point][cohort][subject_test].keys()
-                    ):
-                        if (
-                            grid_point_all[grid_point][cohort][subject_test][
-                                run
-                            ]["lat"]
-                            != "CON"
-                        ):
-                            continue
-                        X_test.append(
-                            grid_point_all[grid_point][cohort][subject_test][
-                                run
-                            ]["data"]
-                        )
-                        y_test.append(
-                            grid_point_all[grid_point][cohort][subject_test][
-                                run
-                            ]["label"]
-                        )
-                    if len(X_test) > 1:
-                        X_test = np.concatenate(X_test, axis=0)
-                        y_test = np.concatenate(y_test, axis=0)
-                    else:
-                        X_test = X_test[0]
-                        y_test = y_test[0]
-                    X_train = []
-                    y_train = []
-                    for cohort_inner in list(
-                        grid_point_all[grid_point].keys()
-                    ):  # available cohorts for that grid point
-                        for subject_train in list(
-                            grid_point_all[grid_point][cohort_inner].keys()
-                        ):
-                            if (subject_test == subject_train) and (
-                                cohort_inner == cohort
-                            ):
-                                continue
-                            for run in list(
-                                grid_point_all[grid_point][cohort_inner][
-                                    subject_train
-                                ].keys()
-                            ):
-                                if (
-                                    grid_point_all[grid_point][cohort_inner][
-                                        subject_train
-                                    ][run]["lat"]
-                                    != "CON"
-                                ):
-                                    continue
-                                X_train.append(
-                                    grid_point_all[grid_point][cohort_inner][
-                                        subject_train
-                                    ][run]["data"]
-                                )
-                                y_train.append(
-                                    grid_point_all[grid_point][cohort_inner][
-                                        subject_train
-                                    ][run]["label"]
-                                )
-                    if len(X_train) > 1:
-                        X_train = np.concatenate(X_train, axis=0)
-                        y_train = np.concatenate(y_train, axis=0)
-                    else:
-                        X_train = X_train[0]
-                        y_train = y_train[0]
-
-                    self.decoder = self.init_decoder()
-                    try:
-                        cv_res = self.eval_model(
-                            X_train, y_train, X_test, y_test
-                        )
-                    except nm_decode.Decoder.ClassMissingException:
-                        continue
-
-                    performance_leave_one_patient_out[cohort][grid_point][
-                        subject_test
-                    ] = cv_res
-
-        performance_leave_one_patient_out["grid_cortex"] = self.grid_cortex
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name
-                + "_performance_leave_one_patient_out_across_cohorts.npy",
-            ),
-            performance_leave_one_patient_out,
-        )
-
-    def run_leave_nminus1_patient_out_across_cohorts(self):
-
-        grid_point_all = np.load(
-            os.path.join(self.outpath, "grid_point_all_re.npy"),
-            allow_pickle="TRUE",
-        ).item()
-        performance_leave_one_patient_out = {}
-
-        for grid_point in list(grid_point_all.keys()):
-            print("grid point: " + str(grid_point))
-            for cohort_train in self.cohorts:
-                print("cohort: " + str(cohort_train))
-                if cohort_train not in performance_leave_one_patient_out:
-                    performance_leave_one_patient_out[cohort_train] = {}
-
-                if cohort_train not in grid_point_all[grid_point]:
-                    continue
-                if (
-                    len(list(grid_point_all[grid_point][cohort_train].keys()))
-                    <= 1
-                ):
-                    continue  # cannot do leave one out prediction with a single subject
-                if (
-                    grid_point
-                    not in performance_leave_one_patient_out[cohort_train]
-                ):
-                    performance_leave_one_patient_out[cohort_train][
-                        grid_point
-                    ] = {}
-
-                for subject_train in list(
-                    grid_point_all[grid_point][cohort_train].keys()
-                ):
-                    X_train = []
-                    y_train = []
-                    for run in list(
-                        grid_point_all[grid_point][cohort_train][
-                            subject_train
-                        ].keys()
-                    ):
-                        if (
-                            grid_point_all[grid_point][cohort_train][
-                                subject_train
-                            ][run]["lat"]
-                            != "CON"
-                        ):
-                            continue
-                        X_train.append(
-                            grid_point_all[grid_point][cohort_train][
-                                subject_train
-                            ][run]["data"]
-                        )
-                        y_train.append(
-                            grid_point_all[grid_point][cohort_train][
-                                subject_train
-                            ][run]["label"]
-                        )
-                    if len(X_train) > 1:
-                        X_train = np.concatenate(X_train, axis=0)
-                        y_train = np.concatenate(y_train, axis=0)
-                    else:
-                        X_train = X_train[0]
-                        y_train = y_train[0]
-
-                    for cohort_test in list(grid_point_all[grid_point].keys()):
-                        for subject_test in list(
-                            grid_point_all[grid_point][cohort_test].keys()
-                        ):
-                            if (subject_test == subject_train) and (
-                                cohort_test == cohort_train
-                            ):
-                                continue
-                            X_test = []
-                            y_test = []
-                            for run in list(
-                                grid_point_all[grid_point][cohort_test][
-                                    subject_test
-                                ].keys()
-                            ):
-                                if (
-                                    grid_point_all[grid_point][cohort_test][
-                                        subject_test
-                                    ][run]["lat"]
-                                    != "CON"
-                                ):
-                                    continue
-                                X_test.append(
-                                    grid_point_all[grid_point][cohort_test][
-                                        subject_test
-                                    ][run]["data"]
-                                )
-                                y_test.append(
-                                    grid_point_all[grid_point][cohort_test][
-                                        subject_test
-                                    ][run]["label"]
-                                )
-                            if len(X_test) > 1:
-                                X_test = np.concatenate(X_test, axis=0)
-                                y_test = np.concatenate(y_test, axis=0)
-                            else:
-                                X_test = X_test[0]
-                                y_test = y_test[0]
-
-                            self.decoder = self.init_decoder()
-                            try:
-                                cv_res = self.eval_model(
-                                    X_train, y_train, X_test, y_test
-                                )
-                            except nm_decode.Decoder.ClassMissingException:
-                                continue
-
-                            if (
-                                subject_train
-                                not in performance_leave_one_patient_out[
-                                    cohort_train
-                                ][grid_point]
-                            ):
-                                performance_leave_one_patient_out[cohort_train][
-                                    grid_point
-                                ][subject_train] = {}
-                            if (
-                                cohort_test
-                                not in performance_leave_one_patient_out[
-                                    cohort_train
-                                ][grid_point][subject_train]
-                            ):
-                                performance_leave_one_patient_out[cohort_train][
-                                    grid_point
-                                ][subject_train][cohort_test] = {}
-                            if (
-                                subject_test
-                                not in performance_leave_one_patient_out[
-                                    cohort_train
-                                ][grid_point][subject_train][cohort_test]
-                            ):
-                                performance_leave_one_patient_out[cohort_train][
-                                    grid_point
-                                ][subject_train][cohort_test][subject_test] = {}
-
-                            performance_leave_one_patient_out[cohort_train][
-                                grid_point
-                            ][subject_train][cohort_test][subject_test] = cv_res
-
-        performance_leave_one_patient_out["grid_cortex"] = self.grid_cortex
-        np.save(
-            os.path.join(
-                self.outpath,
-                self.ML_model_name
-                + "_performance_leave_nminus1_patient_out_across_cohorts.npy",
-            ),
-            performance_leave_one_patient_out,
-        )
diff --git a/py_neuromodulation/nm_analysis.py b/py_neuromodulation/nm_analysis.py
index 816df7b6..de39deca 100644
--- a/py_neuromodulation/nm_analysis.py
+++ b/py_neuromodulation/nm_analysis.py
@@ -1,16 +1,16 @@
-import os
-from pathlib import Path
-from re import VERBOSE
-import re
-from typing import Optional
+from pathlib import PurePath
 
-import _pickle as cPickle
+import pickle
 import numpy as np
 import pandas as pd
-from sklearn import base, linear_model, metrics, model_selection
-from scipy import stats
+from sklearn.linear_model import LogisticRegression
+from sklearn.metrics import r2_score
+from sklearn.model_selection import KFold
 
-from py_neuromodulation import nm_decode, nm_IO, nm_plots
+from scipy.stats import zscore as scipy_zscore
+
+from py_neuromodulation import nm_IO, nm_plots
+from py_neuromodulation.nm_decode import Decoder
 
 target_filter_str = {
     "CLEAN",
@@ -23,21 +23,8 @@
 
 
 class Feature_Reader:
-
-    feature_dir: str
-    feature_list: list[str]
-    settings: dict
-    sidecar: dict
-    sfreq: int
-    line_noise: int
-    nm_channels: pd.DataFrame
-    feature_arr: pd.DataFrame
-    ch_names: list[str]
-    ch_names_ECOG: list[str]
-    decoder: nm_decode.Decoder = None
-
     def __init__(
-        self, feature_dir: str, feature_file: str, binarize_label: bool = True
+        self, feature_dir: str, feature_file: str = "", binarize_label: bool = True
     ) -> None:
         """Feature_Reader enables analysis methods on top of NM_reader and NM_Decoder
 
@@ -49,19 +36,18 @@ def __init__(
             specific feature run, if None it is set to the first feature folder in feature_dir
         binarize_label : bool
             binarize label, by default True
-            
+
         """
-        self.feature_dir = feature_dir
-        self.feature_list = nm_IO.get_run_list_indir(self.feature_dir)
-        if feature_file is None:
+        self.feature_dir: str = feature_dir
+        self.feature_list: list[str] = nm_IO.get_run_list_indir(self.feature_dir)
+        self.feature_file: str
+        if not feature_file:
             self.feature_file = self.feature_list[0]
         else:
             self.feature_file = feature_file
 
-        FILE_BASENAME = Path(self.feature_file).stem
-        PATH_READ_FILE = str(
-            Path(self.feature_dir, FILE_BASENAME, FILE_BASENAME)
-        )
+        FILE_BASENAME = PurePath(self.feature_file).stem
+        PATH_READ_FILE = str(PurePath(self.feature_dir, FILE_BASENAME, FILE_BASENAME))
 
         self.settings = nm_IO.read_settings(PATH_READ_FILE)
         self.sidecar = nm_IO.read_sidecar(PATH_READ_FILE)
@@ -78,8 +64,7 @@ def __init__(
         self.ch_names = self.nm_channels.new_name
         self.used_chs = list(
             self.nm_channels[
-                (self.nm_channels["target"] == 0)
-                & (self.nm_channels["used"] == 1)
+                (self.nm_channels["target"] == 0) & (self.nm_channels["used"] == 1)
             ]["new_name"]
         )
         self.ch_names_ECOG = self.nm_channels.query(
@@ -98,9 +83,7 @@ def __init__(
             )
 
     def _get_target_ch(self) -> str:
-        target_names = list(
-            self.nm_channels[self.nm_channels["target"] == 1]["name"]
-        )
+        target_names = list(self.nm_channels[self.nm_channels["target"] == 1]["name"])
         target_clean = [
             target_name
             for target_name in target_names
@@ -134,7 +117,7 @@ def read_target_ch(
         label_name: str,
         binarize: bool = True,
         binarize_th: float = 0.3,
-    ) -> None:
+    ) -> np.ndarray:
         """_summary_
 
         Parameters
@@ -162,8 +145,8 @@ def read_target_ch(
     @staticmethod
     def filter_features(
         feature_columns: list,
-        ch_name: str = None,
-        list_feature_keywords: list[str] = None,
+        ch_name: str | None = None,
+        list_feature_keywords: list[str] | None = None,
     ) -> list:
         """filters read features by ch_name and/or modality
 
@@ -189,9 +172,7 @@ def filter_features(
 
         if list_feature_keywords is not None:
             feature_select = [
-                f
-                for f in feature_select
-                if any(x in f for x in list_feature_keywords)
+                f for f in feature_select if any(x in f for x in list_feature_keywords)
             ]
 
             if (
@@ -210,7 +191,7 @@ def filter_features(
         return feature_select
 
     def set_target_ch(self, ch_name: str) -> None:
-        self.label = ch_name
+        self.label_name = ch_name
 
     def normalize_features(
         self,
@@ -221,22 +202,17 @@ def normalize_features(
             pd.DataFrame: z-scored feature_arr
         """
         cols_norm = [c for c in self.feature_arr.columns if "time" not in c]
-        feature_arr_norm = stats.zscore(self.feature_arr[cols_norm], nan_policy="omit")
+        feature_arr_norm = scipy_zscore(self.feature_arr[cols_norm], nan_policy="omit")
         feature_arr_norm["time"] = self.feature_arr["time"]
         return feature_arr_norm
 
     def plot_cort_projection(self) -> None:
-        """_summary_
-        """
+        """_summary_"""
 
         if self.sidecar["sess_right"]:
-            ecog_strip = np.array(
-                self.sidecar["coords"]["cortex_right"]["positions"]
-            )
+            ecog_strip = np.array(self.sidecar["coords"]["cortex_right"]["positions"])
         else:
-            ecog_strip = np.array(
-                self.sidecar["coords"]["cortex_left"]["positions"]
-            )
+            ecog_strip = np.array(self.sidecar["coords"]["cortex_left"]["positions"])
         self.nmplotter.plot_cortex(
             grid_cortex=np.array(self.sidecar["grid_cortex"])
             if "grid_cortex" in self.sidecar
@@ -282,9 +258,9 @@ def plot_target_avg_all_channels(
 
     def plot_target_averaged_channel(
         self,
-        ch: str = None,
-        list_feature_keywords: Optional[list[str]] = None,
-        features_to_plt: list = None,
+        ch: str = "",
+        list_feature_keywords: list[str] | None = None,
+        features_to_plt: list | None = None,
         epoch_len: int = 4,
         threshold: float = 0.1,
         normalize_data: bool = True,
@@ -325,7 +301,6 @@ def plot_target_averaged_channel(
         # TODO: This does not work properly when we have bipolar rereferencing
 
         if features_to_plt is None:
-
             filtered_df = self.feature_arr[
                 self.filter_features(
                     self.feature_arr.columns, ch, list_feature_keywords
@@ -354,7 +329,7 @@ def plot_target_averaged_channel(
             if list_feature_keywords is not None
             else "all",
             cut_ch_name_cols=True,
-            ch_name=ch if ch is not None else None,
+            ch_name=ch,
             label_name=self.label_name,
             normalize_data=normalize_data,
             show_plot=show_plot,
@@ -364,20 +339,20 @@ def plot_target_averaged_channel(
             str_title=title,
             ytick_labelsize=ytick_labelsize,
             figsize_x=figsize_x,
-            figsize_y=figsize_y
+            figsize_y=figsize_y,
         )
 
     def plot_all_features(
         self,
-        ch_used: str = None,
-        time_limit_low_s: float = None,
-        time_limit_high_s: float = None,
+        ch_used: str | None = None,
+        time_limit_low_s: float | None = None,
+        time_limit_high_s: float | None = None,
         normalize: bool = True,
         save: bool = False,
         title="all_feature_plt.pdf",
         ytick_labelsize: int = 10,
-        clim_low: float = None,
-        clim_high: float = None,
+        clim_low: float | None = None,
+        clim_high: float | None = None,
     ):
         """_summary_
 
@@ -407,10 +382,7 @@ def plot_all_features(
             col_used = [
                 c
                 for c in self.feature_arr.columns
-                if c.startswith(ch_used)
-                or c == "time"
-                or "LABEL" in c
-                or "MOV" in c
+                if c.startswith(ch_used) or c == "time" or "LABEL" in c or "MOV" in c
             ]
             df = self.feature_arr[col_used[::-1]]
         else:
@@ -457,9 +429,7 @@ def get_performace_sub_strip(performance_sub: dict, plt_grid: bool = False):
         for ch in channels_:
             if "grid" not in ch and "combined" not in ch:
                 ecog_coords_strip.append(performance_sub[ch]["coord"])
-                ecog_strip_performance.append(
-                    performance_sub[ch]["performance_test"]
-                )
+                ecog_strip_performance.append(performance_sub[ch]["performance_test"])
             elif plt_grid is True and "gridcortex_" in ch:
                 cortex_grid.append(performance_sub[ch]["coord"])
                 grid_performance.append(performance_sub[ch]["performance_test"])
@@ -489,9 +459,7 @@ def plot_across_subject_grd_ch_performance(
                 ecog_coords_strip_sub,
                 _,
                 grid_performance_sub,
-            ) = self.get_performace_sub_strip(
-                performance_dict[sub], plt_grid=plt_grid
-            )
+            ) = self.get_performace_sub_strip(performance_dict[sub], plt_grid=plt_grid)
             ecog_strip_performance.extend(ecog_strip_performance_sub)
             ecog_coords_strip.extend(ecog_coords_strip_sub)
             grid_performance.append(grid_performance_sub)
@@ -560,9 +528,7 @@ def plot_subject_grid_ch_performance(
             grid_cortex=np.array(self.sidecar["grid_cortex"])
             if "grid_cortex" in self.sidecar
             else None,
-            ecog_strip=ecog_coords_strip
-            if len(ecog_coords_strip) > 0
-            else None,
+            ecog_strip=ecog_coords_strip if len(ecog_coords_strip) > 0 else None,
             grid_color=grid_performance if len(grid_performance) > 0 else None,
             strip_color=ecog_strip_performance
             if len(ecog_strip_performance) > 0
@@ -588,7 +554,9 @@ def plot_corr_matrix(
         )
 
     @staticmethod
-    def get_epochs(data, y_, epoch_len, sfreq, threshold=0) -> (np.ndarray, np.ndarray):
+    def get_epochs(
+        data, y_, epoch_len, sfreq, threshold=0
+    ) -> tuple[np.ndarray, np.ndarray]:
         """Return epoched data.
 
         Parameters
@@ -624,17 +592,12 @@ def get_epochs(data, y_, epoch_len, sfreq, threshold=0) -> (np.ndarray, np.ndarr
 
         ind_mov = ind_mov[low_limit & up_limit]
 
-        epoch_ = np.zeros(
-            [ind_mov.shape[0], epoch_lim, data.shape[1], data.shape[2]]
-        )
+        epoch_ = np.zeros([ind_mov.shape[0], epoch_lim, data.shape[1], data.shape[2]])
 
         y_arr = np.zeros([ind_mov.shape[0], int(epoch_lim)])
 
         for idx, i in enumerate(ind_mov):
-
-            epoch_[idx, :, :, :] = data[
-                i - epoch_lim // 2 : i + epoch_lim // 2, :, :
-            ]
+            epoch_[idx, :, :, :] = data[i - epoch_lim // 2 : i + epoch_lim // 2, :, :]
 
             y_arr[idx, :] = y_[i - epoch_lim // 2 : i + epoch_lim // 2]
 
@@ -642,15 +605,13 @@ def get_epochs(data, y_, epoch_len, sfreq, threshold=0) -> (np.ndarray, np.ndarr
 
     def set_decoder(
         self,
-        decoder: nm_decode.Decoder = None,
+        decoder: Decoder | None = None,
         TRAIN_VAL_SPLIT=False,
         RUN_BAY_OPT=False,
         save_coef=False,
-        model: base.BaseEstimator = linear_model.LogisticRegression,
-        eval_method=metrics.r2_score,
-        cv_method: model_selection.BaseCrossValidator = model_selection.KFold(
-            n_splits=3, shuffle=False
-        ),
+        model=LogisticRegression,
+        eval_method=r2_score,
+        cv_method=KFold(n_splits=3, shuffle=False),
         get_movement_detection_rate: bool = False,
         mov_detection_threshold=0.5,
         min_consequent_count=3,
@@ -669,8 +630,7 @@ def set_decoder(
         if decoder is not None:
             self.decoder = decoder
         else:
-
-            self.decoder = nm_decode.Decoder(
+            self.decoder = Decoder(
                 features=self.feature_arr,
                 label=self.label,
                 label_name=self.label_name,
@@ -700,7 +660,7 @@ def set_decoder(
 
     def run_ML_model(
         self,
-        feature_file: str = None,
+        feature_file: str | None = None,
         estimate_gridpoints: bool = False,
         estimate_channels: bool = True,
         estimate_all_channels_combined: bool = False,
@@ -766,7 +726,7 @@ def run_ML_model(
     def read_results(
         self,
         performance_dict: dict = {},
-        subject_name: str = None,
+        subject_name: str | None = None,
         DEFAULT_PERFORMANCE: float = 0.5,
         read_grid_points: bool = True,
         read_channels: bool = True,
@@ -777,9 +737,9 @@ def read_results(
         read_mrmr: bool = False,
         model_save: bool = False,
         save_results: bool = False,
-        PATH_OUT: str = None,
-        folder_name: str = None,
-        str_add: str = None,
+        PATH_OUT: str = "",  # Removed None default, save_general_dict does not handle None anyway
+        folder_name: str = "",
+        str_add: str = "",
     ):
         """Save performances of a given patient into performance_dict from saved nm_decoder
 
@@ -808,11 +768,11 @@ def read_results(
         PATH_OUT : string, by default None
         folder_name : string, by default None
         str_add : string, by default None
-        
+
         Returns
         -------
         performance_dict : dictionary
-            
+
         """
 
         if ".vhdr" in self.feature_file:
@@ -825,7 +785,7 @@ def read_results(
                 feature_file.find("sub-") : feature_file.find("_ses")
             ][4:]
 
-        PATH_ML_ = os.path.join(
+        PATH_ML_ = PurePath(
             self.feature_dir,
             feature_file,
             feature_file + "_" + ML_model_name + "_ML_RES.p",
@@ -833,7 +793,7 @@ def read_results(
 
         # read ML results
         with open(PATH_ML_, "rb") as input:
-            ML_res = cPickle.load(input)
+            ML_res = pickle.load(input)
             if self.decoder is None:
                 self.decoder = ML_res
 
@@ -858,15 +818,15 @@ def read_ML_performances(
                 obj_read, obj_write, set_inner_CV_res: bool = False
             ):
                 def set_score(
-                    key_set: str,
-                    key_get: str,
+                    key_set: str = "",
+                    key_get: str = "",
                     take_mean: bool = True,
                     val=None,
                 ):
-                    if set_inner_CV_res is True:
+                    if set_inner_CV_res:
                         key_set = "InnerCV_" + key_set
                         key_get = "InnerCV_" + key_get
-                    if take_mean is True:
+                    if take_mean:
                         val = np.mean(obj_read[key_get])
                     obj_write[key_set] = val
 
@@ -921,56 +881,45 @@ def set_score(
                         take_mean=True,
                     )
 
-                if read_bay_opt_params is True:
+                if read_bay_opt_params:
                     # transform dict into keys for json saving
                     dict_to_save = transform_list_of_dicts_into_dict_of_lists(
                         obj_read["best_bay_opt_params"]
                     )
                     set_score(
                         key_set="bay_opt_best_params",
-                        key_get=None,
                         take_mean=False,
                         val=dict_to_save,
                     )
 
-                if read_mrmr is True:
+                if read_mrmr:
                     # transform dict into keys for json saving
 
                     set_score(
                         key_set="mrmr_select",
-                        key_get=None,
                         take_mean=False,
                         val=obj_read["mrmr_select"],
                     )
-                if model_save is True:
+                if model_save:
                     set_score(
                         key_set="model_save",
-                        key_get=None,
                         take_mean=False,
                         val=obj_read["model_save"],
                     )
 
             read_ML_performances(obj_read, obj_write)
 
-            if (
-                len([key_ for key_ in obj_read.keys() if "InnerCV_" in key_])
-                > 0
-            ):
+            if len([key_ for key_ in obj_read.keys() if "InnerCV_" in key_]) > 0:
                 read_ML_performances(obj_read, obj_write, set_inner_CV_res=True)
 
         if read_channels:
-
             ch_to_use = self.ch_names_ECOG
             ch_to_use = self.decoder.used_chs
             for ch in ch_to_use:
-
                 performance_dict[subject_name][ch] = {}
 
                 if "coords" in self.sidecar:
-                    if (
-                        len(self.sidecar["coords"]) > 0
-                    ):  # check if coords are empty
-
+                    if len(self.sidecar["coords"]) > 0:  # check if coords are empty
                         coords_exist = False
                         for cortex_loc in self.sidecar["coords"].keys():
                             for ch_name_coord_idx, ch_name_coord in enumerate(
@@ -980,8 +929,10 @@ def set_score(
                                     coords = self.sidecar["coords"][cortex_loc][
                                         "positions"
                                     ][ch_name_coord_idx]
-                                    coords_exist = True  # optimally break out of the two loops...
-                        if coords_exist is False:
+                                    coords_exist = (
+                                        True  # optimally break out of the two loops...
+                                    )
+                        if not coords_exist:
                             coords = None
                         performance_dict[subject_name][ch]["coord"] = coords
                 write_CV_res_in_performance_dict(
@@ -1001,30 +952,27 @@ def set_score(
             )
 
         if read_grid_points:
-            performance_dict[subject_name][
-                "active_gridpoints"
-            ] = ML_res.active_gridpoints
+            performance_dict[subject_name]["active_gridpoints"] = (
+                ML_res.active_gridpoints
+            )
 
             for project_settings, grid_type in zip(
                 ["project_cortex", "project_subcortex"],
                 ["gridcortex_", "gridsubcortex_"],
             ):
-                if self.settings["postprocessing"][project_settings] is False:
+                if not self.settings["postprocessing"][project_settings]:
                     continue
 
                 # the sidecar keys are grid_cortex and subcortex_grid
                 for grid_point in range(
                     len(self.sidecar["grid_" + project_settings.split("_")[1]])
                 ):
-
                     gp_str = grid_type + str(grid_point)
 
                     performance_dict[subject_name][gp_str] = {}
-                    performance_dict[subject_name][gp_str][
-                        "coord"
-                    ] = self.sidecar["grid_" + project_settings.split("_")[1]][
-                        grid_point
-                    ]
+                    performance_dict[subject_name][gp_str]["coord"] = self.sidecar[
+                        "grid_" + project_settings.split("_")[1]
+                    ][grid_point]
 
                     if gp_str in ML_res.active_gridpoints:
                         write_CV_res_in_performance_dict(
@@ -1035,12 +983,12 @@ def set_score(
                         )
                     else:
                         # set non interpolated grid point to default performance
-                        performance_dict[subject_name][gp_str][
-                            "performance_test"
-                        ] = DEFAULT_PERFORMANCE
-                        performance_dict[subject_name][gp_str][
-                            "performance_train"
-                        ] = DEFAULT_PERFORMANCE
+                        performance_dict[subject_name][gp_str]["performance_test"] = (
+                            DEFAULT_PERFORMANCE
+                        )
+                        performance_dict[subject_name][gp_str]["performance_train"] = (
+                            DEFAULT_PERFORMANCE
+                        )
 
         if save_results:
             nm_IO.save_general_dict(
diff --git a/py_neuromodulation/nm_bispectra.py b/py_neuromodulation/nm_bispectra.py
index 70386b64..917052ce 100644
--- a/py_neuromodulation/nm_bispectra.py
+++ b/py_neuromodulation/nm_bispectra.py
@@ -1,14 +1,12 @@
-from typing import Iterable
+from collections.abc import Iterable, Callable
 import numpy as np
-from pybispectra import compute_fft, get_example_data_paths, WaveShape
+from pybispectra import compute_fft, WaveShape
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
 
 
-class Bispectra(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: int | float
-    ) -> None:
+class Bispectra(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         super().__init__(settings, ch_names, sfreq)
         self.sfreq = sfreq
         self.ch_names = ch_names
@@ -20,7 +18,7 @@ def __init__(
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         s = settings
 
@@ -66,12 +64,13 @@ def test_range(f_name, filter_range):
 
     def compute_bs_features(
         self,
-        spectrum_ch: np.array,
+        spectrum_ch: np.ndarray,
         features_compute: dict,
         ch_name: str,
         component: str,
-        f_band: str,
+        f_band: str | None,
     ) -> dict:
+        func: Callable
         for bispectrum_feature in self.s["bispectrum"]["bispectrum_features"]:
             if bispectrum_feature == "mean":
                 func = np.nanmean
@@ -105,7 +104,7 @@ def compute_bs_features(
 
         return features_compute
 
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         for ch_idx, ch_name in enumerate(self.ch_names):
             fft_coeffs, freqs = compute_fft(
                 data=np.expand_dims(data[ch_idx, :], axis=(0, 1)),
@@ -148,9 +147,7 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
                 for fb in self.s["bispectrum"]["frequency_bands"]:
                     range_ = (
                         f_spectrum_range >= self.s["frequency_ranges_hz"][fb][0]
-                    ) & (
-                        f_spectrum_range <= self.s["frequency_ranges_hz"][fb][1]
-                    )
+                    ) & (f_spectrum_range <= self.s["frequency_ranges_hz"][fb][1])
                     # waveshape.results.plot()
                     data_bs = spectrum_ch[range_, range_]
 
@@ -158,9 +155,7 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
                         data_bs, features_compute, ch_name, component, fb
                     )
 
-                if self.s["bispectrum"][
-                    "compute_features_for_whole_fband_range"
-                ]:
+                if self.s["bispectrum"]["compute_features_for_whole_fband_range"]:
                     features_compute = self.compute_bs_features(
                         spectrum_ch, features_compute, ch_name, component, None
                     )
diff --git a/py_neuromodulation/nm_bursts.py b/py_neuromodulation/nm_bursts.py
index a7a4e090..7735aa13 100644
--- a/py_neuromodulation/nm_bursts.py
+++ b/py_neuromodulation/nm_bursts.py
@@ -1,15 +1,13 @@
-import enum
 import numpy as np
-from typing import Iterable
-from scipy import signal
+from collections.abc import Iterable
+from scipy.signal import hilbert as scipy_hilbert
 
-from py_neuromodulation import nm_features_abc, nm_filter
+from py_neuromodulation.nm_features_abc import Feature
+from py_neuromodulation.nm_filter import MNEFilter
 
 
-class Burst(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class Burst(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.s = settings
         self.sfreq = sfreq
         self.ch_names = ch_names
@@ -23,27 +21,22 @@ def __init__(
 
         self.fband_names = self.s["burst_settings"]["frequency_bands"]
         self.f_ranges = [
-            self.s["frequency_ranges_hz"][fband_name]
-            for fband_name in self.fband_names
+            self.s["frequency_ranges_hz"][fband_name] for fband_name in self.fband_names
         ]
         self.seglengths = np.floor(
             self.sfreq
             / 1000
             * np.array(
                 [
-                    self.s["bandpass_filter_settings"]["segment_lengths_ms"][
-                        fband
-                    ]
+                    self.s["bandpass_filter_settings"]["segment_lengths_ms"][fband]
                     for fband in self.fband_names
                 ]
             )
         ).astype(int)
 
-        self.num_max_samples_ring_buffer = int(
-            self.sfreq * self.time_duration_s
-        )
+        self.num_max_samples_ring_buffer = int(self.sfreq * self.time_duration_s)
 
-        self.bandpass_filter = nm_filter.MNEFilter(
+        self.bandpass_filter = MNEFilter(
             f_ranges=self.f_ranges,
             sfreq=self.sfreq,
             filter_length=self.sfreq - 1,
@@ -53,21 +46,21 @@ def __init__(
         # create dict with fband, channel specific data store
         # for previous time_duration_s
         def init_ch_fband_dict() -> dict:
-            d = {}
+            d: dict = {}
             for ch in self.ch_names:
-                if ch not in d:
-                    d[ch] = {}
+                d.setdefault(ch, {})
                 for fb in self.fband_names:
-                    if fb not in d[ch]:
-                        d[ch][fb] = None
+                    d[ch].setdefault(fb, None)
+
             return d
 
         self.data_buffer = init_ch_fband_dict()
 
+    @staticmethod
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         assert isinstance(
             settings["burst_settings"]["threshold"], (float, int)
@@ -83,13 +76,11 @@ def test_settings(
         ), f"burst setting time_duration_s needs to be greater than 0, got: {settings['burst_settings']['time_duration_s']}"
 
         for fband_burst in settings["burst_settings"]["frequency_bands"]:
-            assert fband_burst in list(
-                settings["frequency_ranges_hz"].keys()
+            assert (
+                fband_burst in list(settings["frequency_ranges_hz"].keys())
             ), f"bursting {fband_burst} needs to be defined in settings['frequency_ranges_hz']"
 
-        for burst_feature in settings["burst_settings"][
-            "burst_features"
-        ].keys():
+        for burst_feature in settings["burst_settings"]["burst_features"].keys():
             assert isinstance(
                 settings["burst_settings"]["burst_features"][burst_feature],
                 bool,
@@ -98,10 +89,10 @@ def test_settings(
                 f"got: {settings['burst_settings']['burst_features'][burst_feature]}"
             )
 
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         # filter_data returns (n_channels, n_fbands, n_samples)
         filtered_data = np.abs(
-            signal.hilbert(self.bandpass_filter.filter_data(data), axis=2)
+            scipy_hilbert(self.bandpass_filter.filter_data(data), axis=2)
         )
         for ch_idx, ch_name in enumerate(self.ch_names):
             for fband_idx, fband_name in enumerate(self.fband_names):
@@ -118,7 +109,7 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
                     )[-self.num_max_samples_ring_buffer :]
 
                 # calc features
-                burst_thr = np.percentile(
+                burst_thr: float = np.percentile(
                     self.data_buffer[ch_name][fband_name], q=self.threshold
                 )
 
@@ -126,31 +117,25 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
                     new_dat, burst_thr, self.sfreq
                 )
 
-                features_compute[
-                    f"{ch_name}_bursts_{fband_name}_duration_mean"
-                ] = (np.mean(burst_length) if len(burst_length) != 0 else 0)
-                features_compute[
-                    f"{ch_name}_bursts_{fband_name}_amplitude_mean"
-                ] = (
+                features_compute[f"{ch_name}_bursts_{fband_name}_duration_mean"] = (
+                    np.mean(burst_length) if len(burst_length) != 0 else 0
+                )
+                features_compute[f"{ch_name}_bursts_{fband_name}_amplitude_mean"] = (
                     np.mean([np.mean(a) for a in burst_amplitude])
                     if len(burst_length) != 0
                     else 0
                 )
 
-                features_compute[
-                    f"{ch_name}_bursts_{fband_name}_duration_max"
-                ] = (np.max(burst_length) if len(burst_length) != 0 else 0)
-                features_compute[
-                    f"{ch_name}_bursts_{fband_name}_amplitude_max"
-                ] = (
+                features_compute[f"{ch_name}_bursts_{fband_name}_duration_max"] = (
+                    np.max(burst_length) if len(burst_length) != 0 else 0
+                )
+                features_compute[f"{ch_name}_bursts_{fband_name}_amplitude_max"] = (
                     np.max([np.max(a) for a in burst_amplitude])
                     if len(burst_amplitude) != 0
                     else 0
                 )
 
-                features_compute[
-                    f"{ch_name}_bursts_{fband_name}_burst_rate_per_s"
-                ] = (
+                features_compute[f"{ch_name}_bursts_{fband_name}_burst_rate_per_s"] = (
                     np.mean(burst_length)
                     / (self.s["segment_length_features_ms"] / 1000)
                     if len(burst_length) != 0
@@ -161,15 +146,11 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
                 if self.data_buffer[ch_name][fband_name][-1] > burst_thr:
                     in_burst = True
 
-                features_compute[f"{ch_name}_bursts_{fband_name}_in_burst"] = (
-                    in_burst
-                )
+                features_compute[f"{ch_name}_bursts_{fband_name}_in_burst"] = in_burst
         return features_compute
 
     @staticmethod
-    def get_burst_amplitude_length(
-        beta_averp_norm, burst_thr: float, sfreq: float
-    ):
+    def get_burst_amplitude_length(beta_averp_norm, burst_thr: float, sfreq: float):
         """
         Analysing the duration of beta burst
         """
@@ -179,20 +160,14 @@ def get_burst_amplitude_length(
         burst_length = []
         burst_amplitude = []
 
-        burst_time_points = np.where(deriv == True)[0]
+        burst_time_points = np.where(deriv)[0]
 
         for i in range(burst_time_points.size // 2):
-            burst_length.append(
-                burst_time_points[2 * i + 1] - burst_time_points[2 * i]
-            )
+            burst_length.append(burst_time_points[2 * i + 1] - burst_time_points[2 * i])
             burst_amplitude.append(
-                beta_averp_norm[
-                    burst_time_points[2 * i] : burst_time_points[2 * i + 1]
-                ]
+                beta_averp_norm[burst_time_points[2 * i] : burst_time_points[2 * i + 1]]
             )
 
         # the last burst length (in case isburst == True) is omitted,
         # since the true burst length cannot be estimated
-        burst_length = np.array(burst_length) / sfreq
-
-        return burst_amplitude, burst_length
+        return burst_amplitude, np.array(burst_length) / sfreq
diff --git a/py_neuromodulation/nm_coherence.py b/py_neuromodulation/nm_coherence.py
index b713de33..9e2d7088 100644
--- a/py_neuromodulation/nm_coherence.py
+++ b/py_neuromodulation/nm_coherence.py
@@ -1,9 +1,9 @@
 from scipy import signal
 import numpy as np
-from typing import Iterable
-import warnings
+from collections.abc import Iterable
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
+from py_neuromodulation import logger
 
 
 class CoherenceObject:
@@ -44,13 +44,13 @@ def get_coh(self, features_compute, x, y):
         self.Pyy = signal.welch(y, self.sfreq, self.window, nperseg=128)[1]
         self.Pxy = signal.csd(x, y, self.sfreq, self.window, nperseg=128)[1]
 
-        if self.coh is True:
+        if self.coh:
             self.coh_val = np.abs(self.Pxy**2) / (self.Pxx * self.Pyy)
-        if self.icoh is True:
+        if self.icoh:
             self.icoh_val = np.array(self.Pxy / (self.Pxx * self.Pyy)).imag
 
         for coh_idx, coh_type in enumerate([self.coh, self.icoh]):
-            if coh_type is True:
+            if coh_type:
                 if coh_idx == 0:
                     coh_val = self.coh_val
                     coh_name = "coh"
@@ -59,11 +59,9 @@ def get_coh(self, features_compute, x, y):
                     coh_name = "icoh"
 
             for idx, fband in enumerate(self.fbands):
-                if self.features_coh["mean_fband"] is True:
+                if self.features_coh["mean_fband"]:
                     feature_calc = np.mean(
-                        coh_val[
-                            np.bitwise_and(self.f > fband[0], self.f < fband[1])
-                        ]
+                        coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])]
                     )
                     feature_name = "_".join(
                         [
@@ -76,11 +74,9 @@ def get_coh(self, features_compute, x, y):
                         ]
                     )
                     features_compute[feature_name] = feature_calc
-                if self.features_coh["max_fband"] is True:
+                if self.features_coh["max_fband"]:
                     feature_calc = np.max(
-                        coh_val[
-                            np.bitwise_and(self.f > fband[0], self.f < fband[1])
-                        ]
+                        coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])]
                     )
                     feature_name = "_".join(
                         [
@@ -93,7 +89,7 @@ def get_coh(self, features_compute, x, y):
                         ]
                     )
                     features_compute[feature_name] = feature_calc
-            if self.features_coh["max_allfbands"] is True:
+            if self.features_coh["max_allfbands"]:
                 feature_calc = self.f[np.argmax(coh_val)]
                 feature_name = "_".join(
                     [
@@ -109,13 +105,8 @@ def get_coh(self, features_compute, x, y):
         return features_compute
 
 
-class NM_Coherence(nm_features_abc.Feature):
-
-    
-
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class NM_Coherence(Feature):
+    def __init__(self, settings: dict, ch_names: list[str], sfreq: float) -> None:
         self.s = settings
         self.sfreq = sfreq
         self.ch_names = ch_names
@@ -128,15 +119,15 @@ def __init__(
                 fband_specs.append(self.s["frequency_ranges_hz"][band_name])
 
             ch_1_name = self.s["coherence"]["channels"][idx_coh][0]
-            ch_1_name_reref = [
-                ch for ch in self.ch_names if ch.startswith(ch_1_name)
-            ][0]
+            ch_1_name_reref = [ch for ch in self.ch_names if ch.startswith(ch_1_name)][
+                0
+            ]
             ch_1_idx = self.ch_names.index(ch_1_name_reref)
 
             ch_2_name = self.s["coherence"]["channels"][idx_coh][1]
-            ch_2_name_reref = [
-                ch for ch in self.ch_names if ch.startswith(ch_2_name)
-            ][0]
+            ch_2_name_reref = [ch for ch in self.ch_names if ch.startswith(ch_2_name)][
+                0
+            ]
             ch_2_idx = self.ch_names.index(ch_2_name_reref)
 
             self.coherence_objects.append(
@@ -159,9 +150,8 @@ def __init__(
     def test_settings(
         s: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
-
         assert (
             len(s["coherence"]["frequency_bands"]) > 0
         ), "coherence frequency_bands list needs to specify at least one frequency band"
@@ -190,11 +180,11 @@ def test_settings(
         )
 
         if sum(list(s["coherence"]["method"].values())) == 0:
-            warnings.warn(
+            logger.warn(
                 "feature coherence enabled, but no coherence['method'] selected"
             )
 
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         for coh_obj in self.coherence_objects:
             features_compute = coh_obj.get_coh(
                 features_compute,
diff --git a/py_neuromodulation/nm_cohortwrapper.py b/py_neuromodulation/nm_cohortwrapper.py
deleted file mode 100644
index 78de8295..00000000
--- a/py_neuromodulation/nm_cohortwrapper.py
+++ /dev/null
@@ -1,435 +0,0 @@
-from re import VERBOSE
-import sys
-import os
-import numpy as np
-from pathlib import Path
-from scipy import stats
-import pandas as pd
-from multiprocessing import Pool
-from sklearn import linear_model, discriminant_analysis, ensemble, svm
-from sklearn import metrics
-from sklearn.base import clone
-from sklearn import model_selection
-from sklearn.utils import class_weight
-from scipy.ndimage import binary_dilation, binary_erosion, label
-import _pickle as cPickle
-from scipy import io
-from matplotlib import pyplot as plt
-import matplotlib
-import bids
-from bids import BIDSLayout
-from itertools import product
-import nibabel as nib
-
-import py_neuromodulation
-from py_neuromodulation import nm_decode, nm_analysis, nm_IO
-from py_neuromodulation import nm_stream_offline
-
-
-class CohortRunner:
-    def __init__(
-        self,
-        cohorts: dict = None,
-        ML_model_name="LM",
-        model=linear_model.LogisticRegression(class_weight="balanced"),
-        eval_method=metrics.balanced_accuracy_score,
-        estimate_gridpoints=False,
-        estimate_channels=True,
-        estimate_all_channels_combined=False,
-        save_coef=False,
-        TRAIN_VAL_SPLIT=False,
-        plot_features=False,
-        plot_grid_performances=False,
-        run_ML_model=True,
-        run_bids=True,
-        binarize_label=True,
-        used_types=("ecog", "dbs", "seeg"),
-        target_keywords=("mov", "squared", "label"),
-        get_movement_detection_rate=False,
-        run_pool=True,
-        VERBOSE=False,
-        LIMIT_DATA=False,
-        RUN_BAY_OPT=False,
-        STACK_FEATURES_N_SAMPLES=True,
-        cv_method=model_selection.KFold(n_splits=3, shuffle=False),
-        use_nested_cv=True,
-        outpath=r"C:\Users\ICN_admin\Documents\Decoding_Toolbox\write_out\0209_SharpWaveLimFeaturesSTFT_with_Grid",
-        PATH_SETTINGS=r"C:\Users\ICN_admin\Documents\py_neuromodulation\pyneuromodulation\nm_settings.json",
-    ) -> None:
-
-        self.ML_model_name = ML_model_name
-        self.model = model
-        self.outpath = outpath
-        self.PATH_SETTINGS = PATH_SETTINGS
-        self.estimate_gridpoints = estimate_gridpoints
-        self.estimate_channels = estimate_channels
-        self.estimate_all_channels_combined = estimate_all_channels_combined
-        self.save_coef = save_coef
-        self.plot_features = plot_features
-        self.plot_grid_performances = plot_grid_performances
-        self.run_ML_model = run_ML_model
-        self.run_bids = run_bids
-        self.run_pool = run_pool
-        self.TRAIN_VAL_SPLIT = TRAIN_VAL_SPLIT
-        self.cohorts = cohorts
-        self.VERBOSE = VERBOSE
-        self.LIMIT_DATA = LIMIT_DATA
-        self.eval_method = eval_method
-        self.cv_method = cv_method
-        self.use_nested_cv = use_nested_cv
-        self.RUN_BAY_OPT = RUN_BAY_OPT
-        self.TRAIN_VAL_SPLIT = TRAIN_VAL_SPLIT
-        self.STACK_FEATURES_N_SAMPLES = STACK_FEATURES_N_SAMPLES
-        self.model = model
-        self.binarize_label = binarize_label
-        self.used_types = used_types
-        self.target_keywords = target_keywords
-        self.get_movement_detection_rate = get_movement_detection_rate
-        self.grid_cortex = pd.read_csv(
-            os.path.join(py_neuromodulation.__path__[0], "grid_cortex.tsv"),
-            sep="\t",
-        ).to_numpy()
-
-    def init_decoder(self) -> nm_decode.Decoder:
-        return nm_decode.Decoder(
-            model=self.model,
-            TRAIN_VAL_SPLIT=self.TRAIN_VAL_SPLIT,
-            STACK_FEATURES_N_SAMPLES=self.STACK_FEATURES_N_SAMPLES,
-            get_movement_detection_rate=self.get_movement_detection_rate,
-            eval_method=self.eval_method,
-            VERBOSE=self.VERBOSE,
-            cv_method=self.cv_method,
-            use_nested_cv=self.use_nested_cv,
-            RUN_BAY_OPT=self.RUN_BAY_OPT,
-        )
-
-    def multiprocess_pipeline_run_wrapper(self, PATH_RUN):
-
-        if type(PATH_RUN) is bids.layout.models.BIDSFile:
-            PATH_RUN = PATH_RUN.path
-
-        # set BIDS PATH and out path
-        # better option: feed the output and bids path as well as a param through the pool
-
-        for cohort, PATH_COHORT in self.cohorts.items():
-            if cohort in PATH_RUN:
-                PATH_BIDS = PATH_COHORT
-                PATH_OUT = os.path.join(self.outpath, cohort)
-                break
-
-        if self.run_bids:
-            stream = nm_stream_offline.Stream(
-                PATH_RUN=PATH_RUN,
-                PATH_BIDS=PATH_BIDS,
-                PATH_OUT=PATH_OUT,
-                LIMIT_DATA=self.LIMIT_DATA,
-                LIMIT_HIGH=200000,
-                LIMIT_LOW=0,
-                target_keywords=self.target_keywords,
-                used_types=self.used_types,
-                PATH_SETTINGS=self.PATH_SETTINGS,
-                VERBOSE=self.VERBOSE,
-            )
-        stream.run()
-
-        feature_file = os.path.basename(PATH_RUN)[:-5]  # cut off ".vhdr"
-
-        feature_reader = nm_analysis.Feature_Reader(
-            feature_dir=PATH_OUT,
-            feature_file=feature_file,
-            binarize_label=self.binarize_label,
-        )
-
-        if self.plot_grid_performances:
-            feature_reader.plot_cort_projection()
-
-        if self.plot_features:
-
-            ch_to_plot = feature_reader.nm_channels.query(
-                '(type=="ecog") and (used == 1)'
-            )["name"]
-
-            feature_used = "stft"
-
-            for ch_used in ch_to_plot:
-                feature_reader.plot_target_averaged_channel(
-                    ch=ch_used,
-                    list_feature_keywords=[feature_used],
-                    epoch_len=4,
-                    threshold=0.5,
-                )
-
-        # model = discriminant_analysis.LinearDiscriminantAnalysis()
-        # model = xgboost.XGBClassifier(scale_pos_weight=10)  # balanced class weights
-        # model = ensemble.RandomForestClassifier(n_estimators=6, max_depth=6, class_weight='balanced')
-        # model = svm.SVC(class_weight="balanced")
-
-        if self.run_ML_model:
-            # set decoder for this specific run (using the feature_reader features)
-            feature_reader.decoder = self.init_decoder()
-
-            feature_reader.decoder.set_data(
-                features=feature_reader.feature_arr,
-                label=feature_reader.label,
-                label_name=feature_reader.label_name,
-                used_chs=feature_reader.used_chs,
-            )
-
-            performances = feature_reader.run_ML_model(
-                estimate_channels=self.estimate_channels,
-                estimate_gridpoints=self.estimate_gridpoints,
-                estimate_all_channels_combined=self.estimate_all_channels_combined,
-                save_results=True,
-                output_name="XGBNOMP",
-            )
-
-        if self.plot_grid_performances:
-            feature_reader.plot_subject_grid_ch_performance(
-                performance_dict=performances, plt_grid=True
-            )
-
-    def run_cohorts(self):
-
-        run_files_all = []
-        for _, PATH_COHORT in self.cohorts.items():
-            layout = BIDSLayout(PATH_COHORT)
-            run_files_all.append(layout.get(extension=".vhdr"))
-
-        run_files_all = list(np.concatenate(run_files_all))
-
-        if self.run_pool:
-            pool = Pool(processes=50)
-            pool.map(self.multiprocess_pipeline_run_wrapper, run_files_all)
-        else:
-            # self.multiprocess_pipeline_run_wrapper(run_files_all[11])
-            for run_file in run_files_all[:12]:
-                self.multiprocess_pipeline_run_wrapper(run_file)
-
-    def read_cohort_results(self, feature_path, cohort):
-        """Read for a given path (of potentially multiple estimated runs) performance results
-        Parameters
-        ----------
-        feature_path : string
-            path where estimated runs are saved
-        cohort : string
-            used for saving output npy dictionary
-        ML_model_name : string
-            model name, by default "LM"
-        """
-
-        # Here the runs are overwritten!
-        folders_path = [x[0] for x in os.walk(feature_path)]
-        feature_paths = [os.path.basename(x) for x in folders_path[1:]]
-        performance_out = {}
-
-        for feature_file in feature_paths:
-            feature_reader = nm_analysis.Feature_Reader(
-                feature_dir=feature_path, feature_file=feature_file
-            )
-
-            performance_run = feature_reader.read_results(
-                read_grid_points=self.estimate_gridpoints,
-                read_channels=self.estimate_channels,
-                read_all_combined=self.estimate_all_channels_combined,
-                read_mov_detection_rates=True,
-            )
-
-            sub = feature_file[
-                feature_file.find("sub-") : feature_file.find("_ses")
-            ][4:]
-            if sub not in performance_out:
-                performance_out[sub] = {}
-            performance_out[sub][feature_file] = performance_run[
-                sub
-            ]  # get saved in performance_run
-
-        np.save(
-            os.path.join(
-                self.outpath, self.ML_model_name + "_cohort_" + cohort + ".npy"
-            ),
-            performance_out,
-        )
-
-    def cohort_wrapper_read_cohort(self):
-        """Read results for multiple cohorts"""
-
-        for cohort in self.cohorts.keys():
-            self.read_cohort_results(
-                os.path.join(self.outpath, cohort), cohort
-            )
-
-    def read_all_channels(
-        self,
-        channel_all,
-        feature_path,
-        feature_file,
-        cohort,
-        read_channels: bool = True,
-    ):
-        """Save for a given feature path all used grid point data. Necessary to run across patient and cohort analysis.
-        Parameters
-        ----------
-        channel_all : dictionary
-            dictionary with data, label, label_name and feature_names for each channel
-        feature_path : string
-            path to feature files
-        feature_file : string
-            feature file
-        cohort : string
-            used for indecing of grid_point_all
-        read_ch : bool
-            if True read channels, else read grid_points
-        Returns
-        -------
-        dictionary
-            ch_all
-        """
-        feature_reader = nm_analysis.Feature_Reader(
-            feature_dir=feature_path, feature_file=feature_file
-        )
-        if "Washington" in feature_path:
-            mov_starts = np.where(np.diff(feature_reader.feature_arr["mov"])>0)[0]
-            seg_cut = []
-            for mov_start in mov_starts:
-                for i in range(5):
-                    seg_cut.append(mov_start+i)
-
-            ind_cut = np.concatenate((np.where(feature_reader.feature_arr["mov"] == 11)[0], seg_cut))
-            idx_select = set(np.arange(feature_reader.feature_arr["mov"].shape[0])) - set(ind_cut)
-            feature_reader.feature_arr = feature_reader.feature_arr.iloc[list(idx_select), :].reset_index(drop=True)
-            #analyzer.feature_arr["mov"] = analyzer.feature_arr["mov"] > 0
-            feature_reader.label = np.array(feature_reader.feature_arr["mov"] > 0, dtype=int)
-            subject_name = feature_file[:2]
-            task_name = "hand_movement"
-            run_number = 1
-        else:
-            subject_name = feature_file[
-                feature_file.find("sub-") + 4 : feature_file.find("_ses")
-            ]
-            sess_name = feature_file[
-                feature_file.find("ses-") + 4 : feature_file.find("_task")
-            ]
-            task_name = feature_file[
-                feature_file.find("task-") + 5 : feature_file.find("_run")
-            ]
-            run_number = feature_file[
-                feature_file.find("run-") + 4 : feature_file.find("_ieeg")
-            ]
-        print(feature_reader.label_name)
-        decoder = nm_decode.Decoder(
-            features=feature_reader.feature_arr,
-            label=feature_reader.label,
-            label_name=feature_reader.label_name,
-            used_chs=feature_reader.used_chs,
-        )
-
-        if read_channels is True:
-            decoder.set_data_ind_channels()
-            data_to_read = decoder.ch_ind_data
-        else:
-            decoder.set_data_grid_points()
-            data_to_read = decoder.grid_point_ind_data
-
-        for ch in list(data_to_read.keys()):
-            if cohort not in channel_all:
-                channel_all[cohort] = {}
-            if subject_name not in channel_all[cohort]:
-                channel_all[cohort][subject_name] = {}
-            if ch not in channel_all[cohort][subject_name]:
-                channel_all[cohort][subject_name][ch] = {}
-            channel_all[cohort][subject_name][ch][feature_file] = {}
-
-            channel_all[cohort][subject_name][ch][feature_file][
-                "data"
-            ] = data_to_read[ch]
-            channel_all[cohort][subject_name][ch][feature_file][
-                "feature_names"
-            ] = [
-                ch_[len(ch) + 1 :]
-                for ch_ in decoder.features.columns
-                if ch in ch_
-            ]
-            channel_all[cohort][subject_name][ch][feature_file][
-                "label"
-            ] = decoder.label
-            channel_all[cohort][subject_name][ch][feature_file][
-                "label_name"
-            ] = decoder.label_name
-
-            # check laterality
-            lat = "CON"  # Beijing is always contralateral
-            # Pittsburgh Subjects
-            if (
-                "LEFT" in decoder.label_name
-                and "LEFT" in decoder.features.columns[1]
-            ) or (
-                "RIGHT" in decoder.label_name
-                and "RIGHT" in decoder.features.columns[1]
-            ):
-                lat = "IPS"
-
-            # Berlin subjects
-            if (
-                "_L_" in decoder.features.columns[1]
-                and task_name == "SelfpacedRotationL"
-            ) or (
-                "_R_" in decoder.features.columns[1]
-                and task_name == "SelfpacedRotationR"
-            ):
-                lat = "IPS"
-            channel_all[cohort][subject_name][ch][feature_file]["lat"] = lat
-        return channel_all
-
-    def cohort_wrapper_read_all_grid_points(self, read_channels=True):
-        cohorts = self.cohorts.keys()
-        grid_point_all = {}
-        for cohort in cohorts:
-            print("COHORT: " + cohort)
-            feature_path = os.path.join(self.outpath, cohort)
-            feature_list = nm_IO.get_run_list_indir(feature_path)
-            for feature_file in feature_list:
-                print(feature_file)
-                grid_point_all = self.read_all_channels(
-                    grid_point_all,
-                    feature_path,
-                    feature_file,
-                    cohort,
-                    read_channels=read_channels,
-                )
-
-        if read_channels is True:
-            np.save(
-                os.path.join(self.outpath, "channel_all.npy"), grid_point_all
-            )
-        else:
-            np.save(
-                os.path.join(self.outpath, "grid_point_all.npy"),
-                grid_point_all,
-            )
-
-    @staticmethod
-    def rewrite_grid_point_all(d, outpath):
-
-        # from channel_all[cohort][subject_name][ch][feature_file]
-
-        # to grid_point_all[grid_point][cohort][subject_test][run]
-
-        p = {}
-        for cohort in d.keys():
-            for sub in d[cohort].keys():
-                for gp in d[cohort][sub].keys():
-                    for f in d[cohort][sub][gp].keys():
-                        if gp not in p.keys():
-                            p[gp] = {}
-                        if cohort not in p[gp].keys():
-                            p[gp][cohort] = {}
-                        if sub not in p[gp][cohort].keys():
-                            p[gp][cohort][sub] = {}
-                        if f not in p[gp][cohort][sub].keys():
-                            p[gp][cohort][sub][f] = {}
-                        for key_ in d[cohort][sub][gp][f].keys():
-                            p[gp][cohort][sub][f][key_] = d[cohort][sub][gp][
-                                f
-                            ][key_]
-
-        np.save(os.path.join(outpath, "grid_point_all_re.npy"), p)
diff --git a/py_neuromodulation/nm_decode.py b/py_neuromodulation/nm_decode.py
index 8bda7a00..9bc712e3 100644
--- a/py_neuromodulation/nm_decode.py
+++ b/py_neuromodulation/nm_decode.py
@@ -2,32 +2,22 @@
     model_selection,
     metrics,
     linear_model,
-    discriminant_analysis,
-    base,
     decomposition,
     cross_decomposition,
 )
-from skopt.space import Real, Integer, Categorical
-from skopt.utils import use_named_args
-from skopt import gp_minimize, Optimizer
-from sklearn.linear_model import ElasticNet
+from skopt import Optimizer
 from sklearn.base import clone
-from sklearn.utils import class_weight
 from scipy.ndimage import binary_dilation, binary_erosion
 from scipy.ndimage import label as label_ndimage
 from imblearn.over_sampling import RandomOverSampler
 from imblearn.under_sampling import RandomUnderSampler
+from mrmr import mrmr_classif
+
 import pandas as pd
-import os
-import json
 import numpy as np
-#from numba import jit
-#import xgboost
 from copy import deepcopy
-
-from mrmr import mrmr_classif
-from typing import Type
-import _pickle as cPickle
+from pathlib import PurePath
+import pickle
 
 
 class CV_res:
@@ -38,58 +28,32 @@ def __init__(
         mrmr_select: bool = False,
         model_save: bool = False,
     ) -> None:
-
-        self.score_train = []
-        self.score_test = []
-        self.y_test = []
-        self.y_train = []
-        self.y_test_pr = []
-        self.y_train_pr = []
-        self.X_test = []
-        self.X_train = []
-        self.coef = []
-        if get_movement_detection_rate is True:
-            self.mov_detection_rates_test = []
-            self.tprate_test = []
-            self.fprate_test = []
-            self.mov_detection_rates_train = []
-            self.tprate_train = []
-            self.fprate_train = []
-        if RUN_BAY_OPT is True:
-            self.best_bay_opt_params = []
-        if mrmr_select is True:
-            self.mrmr_select = []
-        if model_save is True:
-            self.model_save = []
+        self.score_train: list = []
+        self.score_test: list = []
+        self.y_test: list = []
+        self.y_train: list = []
+        self.y_test_pr: list = []
+        self.y_train_pr: list = []
+        self.X_test: list = []
+        self.X_train: list = []
+        self.coef: list = []
+
+        if get_movement_detection_rate:
+            self.mov_detection_rates_test: list = []
+            self.tprate_test: list = []
+            self.fprate_test: list = []
+            self.mov_detection_rates_train: list = []
+            self.tprate_train: list = []
+            self.fprate_train: list = []
+        if RUN_BAY_OPT:
+            self.best_bay_opt_params: list = []
+        if mrmr_select:
+            self.mrmr_select: list = []
+        if model_save:
+            self.model_save: list = []
 
 
 class Decoder:
-
-    features: pd.DataFrame
-    label: np.ndarray
-    model: base.BaseEstimator
-    cv_method: model_selection.BaseCrossValidator
-    use_nested_cv: bool
-    threshold_score: bool
-    mov_detection_threshold: float
-    TRAIN_VAL_SPLIT: bool
-    RUN_BAY_OPT: bool
-    save_coef: bool
-    get_movement_detection_rate: bool
-    min_consequent_count: int
-    STACK_FEATURES_N_SAMPLES: bool
-    time_stack_n_samples: int
-    ros: RandomOverSampler = None
-    rus: RandomUnderSampler = None
-    VERBOSE: bool = False
-    ch_ind_data: dict = {}
-    grid_point_ind_data: dict = {}
-    active_gridpoints: list = []
-    feature_names: list = []
-    ch_ind_results: dict = {}
-    gridpoint_ind_results: dict = {}
-    all_ch_results: dict = {}
-
     class ClassMissingException(Exception):
         def __init__(
             self,
@@ -99,14 +63,14 @@ def __init__(
             super().__init__(self.message)
 
         def __str__(self):
-            print(self.message)
+            return self.message
 
     def __init__(
         self,
-        features: pd.DataFrame = None,
-        label: np.ndarray = None,
-        label_name: str = None,
-        used_chs: list[str] = None,
+        features: pd.DataFrame | None = None,
+        label: np.ndarray | None = None,
+        label_name: str | None = None,
+        used_chs: list[str] | None = None,
         model=linear_model.LinearRegression(),
         eval_method=metrics.r2_score,
         cv_method=model_selection.KFold(n_splits=3, shuffle=False),
@@ -122,7 +86,7 @@ def __init__(
         min_consequent_count: int = 3,
         bay_opt_param_space: list = [],
         VERBOSE: bool = False,
-        sfreq: int = None,
+        sfreq: int | None = None,
         undersampling: bool = False,
         oversampling: bool = False,
         mrmr_select: bool = False,
@@ -201,7 +165,6 @@ def __init__(
             self.ros = RandomOverSampler(random_state=0)
 
     def set_data(self, features):
-
         if features is not None:
             self.features = features
             self.feature_names = [
@@ -209,9 +172,7 @@ def set_data(self, features):
                 for col in self.features.columns
                 if not (("time" in col) or (self.label_name in col))
             ]
-            self.data = np.nan_to_num(
-                np.array(self.features[self.feature_names])
-            )
+            self.data = np.nan_to_num(np.array(self.features[self.feature_names]))
 
     def set_data_ind_channels(self):
         """specified channel individual data"""
@@ -220,11 +181,7 @@ def set_data_ind_channels(self):
             self.ch_ind_data[ch] = np.nan_to_num(
                 np.array(
                     self.features[
-                        [
-                            col
-                            for col in self.features.columns
-                            if col.startswith(ch)
-                        ]
+                        [col for col in self.features.columns if col.startswith(ch)]
                     ]
                 )
             )
@@ -247,13 +204,13 @@ def set_CV_results(self, attr_name, contact_point=None):
         else:
             obj_set = getattr(self, attr_name)
 
-        def set_scores(cv_res: Type[CV_res], set_inner_CV_res: bool = False):
+        def set_scores(cv_res: CV_res, set_inner_CV_res: bool = False):
             """
             This function renames the CV_res keys for InnerCV
             """
 
             def set_score(key_: str, val):
-                if set_inner_CV_res is True:
+                if set_inner_CV_res:
                     key_ = "InnerCV_" + key_
                 obj_set[key_] = val
 
@@ -269,9 +226,7 @@ def set_score(key_: str, val):
             if self.save_coef:
                 set_score("coef", cv_res.coef)
             if self.get_movement_detection_rate:
-                set_score(
-                    "mov_detection_rates_test", cv_res.mov_detection_rates_test
-                )
+                set_score("mov_detection_rates_test", cv_res.mov_detection_rates_test)
                 set_score(
                     "mov_detection_rates_train",
                     cv_res.mov_detection_rates_train,
@@ -281,18 +236,18 @@ def set_score(key_: str, val):
                 set_score("tprate_test", cv_res.tprate_test)
                 set_score("tprate_train", cv_res.tprate_train)
 
-            if self.RUN_BAY_OPT is True:
+            if self.RUN_BAY_OPT:
                 set_score("best_bay_opt_params", cv_res.best_bay_opt_params)
 
-            if self.mrmr_select is True:
+            if self.mrmr_select:
                 set_score("mrmr_select", cv_res.mrmr_select)
-            if self.model_save is True:
+            if self.model_save:
                 set_score("model_save", cv_res.model_save)
             return obj_set
 
         obj_set = set_scores(self.cv_res)
 
-        if self.use_nested_cv is True:
+        if self.use_nested_cv:
             obj_set = set_scores(self.cv_res_inner, set_inner_CV_res=True)
 
     def run_CV_caller(self, feature_contacts: str = "ind_channels"):
@@ -309,16 +264,12 @@ def run_CV_caller(self, feature_contacts: str = "ind_channels"):
             "grid_points",
         ]
         if feature_contacts not in valid_feature_contacts:
-            raise ValueError(
-                f"{feature_contacts} not in {valid_feature_contacts}"
-            )
+            raise ValueError(f"{feature_contacts} not in {valid_feature_contacts}")
 
         if feature_contacts == "grid_points":
             for grid_point in self.active_gridpoints:
                 self.run_CV(self.grid_point_ind_data[grid_point], self.label)
-                self.set_CV_results(
-                    "gridpoint_ind_results", contact_point=grid_point
-                )
+                self.set_CV_results("gridpoint_ind_results", contact_point=grid_point)
             return self.gridpoint_ind_results
 
         if feature_contacts == "ind_channels":
@@ -355,9 +306,7 @@ def set_data_grid_points(self, cortex_only=False, subcortex_only=False):
 
         if subcortex_only:
             self.active_gridpoints = [
-                i
-                for i in self.active_gridpoints
-                if i.startswith("gridsubcortex")
+                i for i in self.active_gridpoints if i.startswith("gridsubcortex")
             ]
 
         self.feature_names = [
@@ -384,7 +333,7 @@ def get_movement_grouped_array(
 
         Parameters
         ----------
-        prediction : np.array
+        prediction : np.ndarray
             numpy array of either predictions or labels, that is going to be grouped
         threshold : float, optional
             threshold to be applied to 'prediction', by default 0.5
@@ -393,15 +342,13 @@ def get_movement_grouped_array(
 
         Returns
         -------
-        labeled_array : np.array
+        labeled_array : np.ndarray
             grouped vector with incrementing number for movement blocks
         labels_count : int
             count of individual movement blocks
         """
         mask = prediction > threshold
-        structure = [
-            True
-        ] * min_consequent_count  # used for erosion and dilation
+        structure = [True] * min_consequent_count  # used for erosion and dilation
         eroded = binary_erosion(mask, structure)
         dilated = binary_dilation(eroded, structure)
         labeled_array, labels_count = label_ndimage(dilated)
@@ -428,10 +375,10 @@ def calc_movement_detection_rate(
         -------
         mov_detection_rate : float
             movement detection rate, where at least 'min_consequent_count' samples where high in prediction
-        fpr : np.array
-            sklearn.metrics false positive rate np.array
-        tpr : np.array
-            sklearn.metrics true positive rate np.array
+        fpr : np.ndarray
+            sklearn.metrics false positive rate np.ndarray
+        tpr : np.ndarray
+            sklearn.metrics true positive rate np.ndarray
         """
 
         pred_grouped, _ = self.get_movement_grouped_array(
@@ -444,17 +391,13 @@ def calc_movement_detection_rate(
         hit_rate = np.zeros(labels_count)
         pred_group_bin = np.array(pred_grouped > 0)
 
-        for label_number in range(
-            1, labels_count + 1
-        ):  # labeling starts from 1
+        for label_number in range(1, labels_count + 1):  # labeling starts from 1
             hit_rate[label_number - 1] = np.sum(
                 pred_group_bin[np.where(y_grouped == label_number)[0]]
             )
 
         try:
-            mov_detection_rate = (
-                np.where(hit_rate > 0)[0].shape[0] / labels_count
-            )
+            mov_detection_rate = np.where(hit_rate > 0)[0].shape[0] / labels_count
         except ZeroDivisionError:
             print("no movements in label")
             return 0, 0, 0
@@ -498,16 +441,12 @@ def append_previous_n_samples(X: np.ndarray, y: np.ndarray, n: int = 5):
 
     @staticmethod
     def append_samples_val(X_train, y_train, X_val, y_val, n):
-
-        X_train, y_train = Decoder.append_previous_n_samples(
-            X_train, y_train, n=n
-        )
+        X_train, y_train = Decoder.append_previous_n_samples(X_train, y_train, n=n)
         X_val, y_val = Decoder.append_previous_n_samples(X_val, y_val, n=n)
         return X_train, y_train, X_val, y_val
 
     def fit_model(self, model, X_train, y_train):
-
-        if self.TRAIN_VAL_SPLIT is True:
+        if self.TRAIN_VAL_SPLIT:
             X_train, X_val, y_train, y_val = model_selection.train_test_split(
                 X_train, y_train, train_size=0.7, shuffle=False
             )
@@ -530,20 +469,20 @@ def fit_model(self, model, X_train, y_train):
             #     )
             # elif type(model) is xgboost.sklearn.XGBRegressor:
             #     # might be necessary to adapt for other classifiers
-            # 
+            #
             #     def evalerror(preds, dtrain):
             #         labels = dtrain.get_label()
             #         # return a pair metric_name, result. The metric name must not contain a
             #         # colon (:) or a space since preds are margin(before logistic
             #         # transformation, cutoff at 0)
-            # 
+            #
             #         r2 = metrics.r2_score(labels, preds)
-            # 
+            #
             #         if r2 < 0:
             #             r2 = 0
-            # 
+            #
             #         return "r2", -r2
-            # 
+            #
             #     model.set_params(eval_metric=evalerror)
             #     model.fit(
             #         X_train,
@@ -555,17 +494,16 @@ def fit_model(self, model, X_train, y_train):
             # else:
             #     model.fit(X_train, y_train, eval_set=[(X_val, y_val)])
         else:
-
             # check for LDA; and apply rebalancing
             if self.oversampling:
                 X_train, y_train = self.ros.fit_resample(X_train, y_train)
             if self.undersampling:
                 X_train, y_train = self.rus.fit_resample(X_train, y_train)
 
-            #if type(model) is xgboost.sklearn.XGBClassifier:
+            # if type(model) is xgboost.sklearn.XGBClassifier:
             #    model.set_params(eval_metric="logloss")
             #    model.fit(X_train, y_train)
-            #else:
+            # else:
             model.fit(X_train, y_train)
 
         return model
@@ -577,11 +515,10 @@ def eval_model(
         X_test,
         y_train,
         y_test,
-        cv_res: Type[CV_res],
+        cv_res: CV_res,
         save_data=True,
         save_probabilities=False,
-    ) -> Type[CV_res]:
-
+    ) -> CV_res:
         if self.save_coef:
             cv_res.coef.append(model_train.coef_)
 
@@ -591,30 +528,28 @@ def eval_model(
         sc_te = self.eval_method(y_test, y_test_pr)
         sc_tr = self.eval_method(y_train, y_train_pr)
 
-        if self.threshold_score is True:
+        if self.threshold_score:
             if sc_tr < 0:
                 sc_tr = 0
             if sc_te < 0:
                 sc_te = 0
 
-        if self.get_movement_detection_rate is True:
+        if self.get_movement_detection_rate:
             self._set_movement_detection_rates(
                 y_test, y_test_pr, y_train, y_train_pr, cv_res
             )
 
         cv_res.score_train.append(sc_tr)
         cv_res.score_test.append(sc_te)
-        if save_data is True:
+        if save_data:
             cv_res.X_train.append(X_train)
             cv_res.X_test.append(X_test)
-        if self.model_save is True:
-            cv_res.model_save.append(
-                deepcopy(model_train)
-            )  # clone won't copy params
+        if self.model_save:
+            cv_res.model_save.append(deepcopy(model_train))  # clone won't copy params
         cv_res.y_train.append(y_train)
         cv_res.y_test.append(y_test)
 
-        if save_probabilities is False:
+        if not save_probabilities:
             cv_res.y_train_pr.append(y_train_pr)
             cv_res.y_test_pr.append(y_test_pr)
         else:
@@ -628,9 +563,8 @@ def _set_movement_detection_rates(
         y_test_pr: np.ndarray,
         y_train: np.ndarray,
         y_train_pr: np.ndarray,
-        cv_res: Type[CV_res],
-    ) -> Type[CV_res]:
-
+        cv_res: CV_res,
+    ) -> CV_res:
         mov_detection_rate, fpr, tpr = self.calc_movement_detection_rate(
             y_test,
             y_test_pr,
@@ -661,11 +595,10 @@ def wrapper_model_train(
         y_train,
         X_test=None,
         y_test=None,
-        cv_res: Type[CV_res] = None,
+        cv_res: CV_res | None = None,
         return_fitted_model_only: bool = False,
         save_data=True,
     ):
-
         if cv_res is None:
             cv_res = CV_res(
                 get_movement_detection_rate=self.get_movement_detection_rate,
@@ -675,7 +608,7 @@ def wrapper_model_train(
             )
 
         model_train = clone(self.model)
-        if self.STACK_FEATURES_N_SAMPLES is True:
+        if self.STACK_FEATURES_N_SAMPLES:
             if X_test is not None:
                 X_train, y_train, X_test, y_test = Decoder.append_samples_val(
                     X_train,
@@ -694,10 +627,10 @@ def wrapper_model_train(
         ):  # only one class present
             raise Decoder.ClassMissingException
 
-        if self.RUN_BAY_OPT is True:
+        if self.RUN_BAY_OPT:
             model_train = self.bay_opt_wrapper(model_train, X_train, y_train)
 
-        if self.mrmr_select is True:
+        if self.mrmr_select:
             if len(self.feature_names) > X_train.shape[1]:
                 # analyze induvidual ch
                 columns_names = [
@@ -707,8 +640,7 @@ def wrapper_model_train(
                 ]
                 if self.columns_names_single_ch is None:
                     self.columns_names_single_ch = [
-                        f[len(self.ch_name_tested) + 1 :]
-                        for f in columns_names
+                        f[len(self.ch_name_tested) + 1 :] for f in columns_names
                     ]
             else:
                 # analyze all_ch_combined
@@ -717,28 +649,25 @@ def wrapper_model_train(
             X_test = pd.DataFrame(X_test, columns=columns_names)
 
             y_train = pd.Series(y_train)
-            selected_features = mrmr_classif(
-                X=X_train, y=y_train, K=20, n_jobs=60
-            )
+            selected_features = mrmr_classif(X=X_train, y=y_train, K=20, n_jobs=60)
 
             X_train = X_train[selected_features]
             X_test = X_test[selected_features]
 
-        if self.pca is True:
+        if self.pca:
             pca = decomposition.PCA(n_components=10)
             pca.fit(X_train)
             X_train = pca.transform(X_train)
             X_test = pca.transform(X_test)
 
-        if self.cca is True:
+        if self.cca:
             cca = cross_decomposition.CCA(n_components=10)
             cca.fit(X_train, y_train)
             X_train = cca.transform(X_train)
             X_test = cca.transform(X_test)
 
-        if self.STACK_FEATURES_N_SAMPLES is True:
-
-            if return_fitted_model_only is True:
+        if self.STACK_FEATURES_N_SAMPLES:
+            if return_fitted_model_only:
                 X_train, y_train = self.append_previous_n_samples(
                     X_train, y_train, self.time_stack_n_samples
                 )
@@ -750,14 +679,14 @@ def wrapper_model_train(
         # fit model
         model_train = self.fit_model(model_train, X_train, y_train)
 
-        if return_fitted_model_only is True:
+        if return_fitted_model_only:
             return model_train
 
         cv_res = self.eval_model(
             model_train, X_train, X_test, y_train, y_test, cv_res, save_data
         )
 
-        if self.mrmr_select is True:
+        if self.mrmr_select:
             cv_res.mrmr_select.append(selected_features)
 
         return cv_res
@@ -776,13 +705,10 @@ def run_CV(self, data, label):
 
         def split_data(data):
             if self.cv_method == "NonShuffledTrainTestSplit":
-
                 # set outer 10s set to train index
                 # test index is thus in the middle starting at random number
                 N_samples = data.shape[0]
-                test_area_points = (N_samples - self.sfreq * 10) - (
-                    self.sfreq * 10
-                )
+                test_area_points = (N_samples - self.sfreq * 10) - (self.sfreq * 10)
                 test_points = int(N_samples * 0.3)
 
                 if test_area_points > test_points:
@@ -790,9 +716,7 @@ def split_data(data):
                         int(self.sfreq * 10),
                         N_samples - self.sfreq * 10 - test_points,
                     )
-                    test_index = np.arange(
-                        start_index, start_index + test_points
-                    )
+                    test_index = np.arange(start_index, start_index + test_points)
                     train_index = np.concatenate(
                         (
                             np.arange(0, start_index),
@@ -822,7 +746,7 @@ def split_data(data):
 
         cv_res = self.init_cv_res()
 
-        if self.use_nested_cv is True:
+        if self.use_nested_cv:
             cv_res_inner = self.init_cv_res()
 
         for train_index, test_index in split_data(data):
@@ -835,12 +759,10 @@ def split_data(data):
             except Decoder.ClassMissingException:
                 continue
 
-            if self.use_nested_cv is True:
+            if self.use_nested_cv:
                 data_inner = data[train_index]
                 label_inner = label[train_index]
-                for train_index_inner, test_index_inner in split_data(
-                    data_inner
-                ):
+                for train_index_inner, test_index_inner in split_data(data_inner):
                     X_train_inner = data_inner[train_index_inner, :]
                     y_train_inner = label_inner[train_index_inner]
                     X_test_inner = data_inner[test_index_inner]
@@ -857,7 +779,7 @@ def split_data(data):
                         continue
 
         self.cv_res = cv_res
-        if self.use_nested_cv is True:
+        if self.use_nested_cv:
             self.cv_res_inner = cv_res_inner
 
     def bay_opt_wrapper(self, model_train, X_train, y_train):
@@ -885,9 +807,7 @@ def bay_opt_wrapper(self, model_train, X_train, y_train):
         # set bay. opt. obtained best params to model
         params_bo_dict = {}
         for i in range(len(params_bo)):
-            setattr(
-                model_train, self.bay_opt_param_space[i].name, params_bo[i]
-            )
+            setattr(model_train, self.bay_opt_param_space[i].name, params_bo[i])
             params_bo_dict[self.bay_opt_param_space[i].name] = params_bo[i]
 
         self.best_bay_opt_params.append(params_bo_dict)
@@ -939,7 +859,6 @@ def run_Bay_Opt(
         """
 
         def get_f_val(model_bo):
-
             try:
                 model_bo = self.fit_model(model_bo, X_train, y_train)
             except Decoder.ClassMissingException:
@@ -969,24 +888,20 @@ def get_f_val(model_bo):
         # res is here automatically appended by skopt
         return res.x
 
-    def save(
-        self, feature_path: str, feature_file: str, str_save_add=None
-    ) -> None:
+    def save(self, feature_path: str, feature_file: str, str_save_add=None) -> None:
         """Save decoder object to pickle"""
 
         # why is the decoder not saved to a .json?
 
         if str_save_add is None:
-            PATH_OUT = os.path.join(
-                feature_path, feature_file, feature_file + "_ML_RES.p"
-            )
+            PATH_OUT = PurePath(feature_path, feature_file, feature_file + "_ML_RES.p")
         else:
-            PATH_OUT = os.path.join(
+            PATH_OUT = PurePath(
                 feature_path,
                 feature_file,
                 feature_file + "_" + str_save_add + "_ML_RES.p",
             )
 
-        print("model being saved to: " + str(PATH_OUT))
+        print(f"model being saved to: {PATH_OUT}")
         with open(PATH_OUT, "wb") as output:  # Overwrites any existing file.
-            cPickle.dump(self, output)
+            pickle.dump(self, output)
diff --git a/py_neuromodulation/nm_define_nmchannels.py b/py_neuromodulation/nm_define_nmchannels.py
index 6f1649c8..0907048e 100644
--- a/py_neuromodulation/nm_define_nmchannels.py
+++ b/py_neuromodulation/nm_define_nmchannels.py
@@ -1,6 +1,6 @@
 """Module for handling nm_channels."""
-from typing import Iterable, Optional, Union
 
+from collections.abc import Iterable
 import pandas as pd
 import numpy as np
 
@@ -11,59 +11,59 @@
 def set_channels(
     ch_names: list[str],
     ch_types: list[str],
-    reference: Union[list, str] = "default",
-    bads: Optional[list[str]] = None,
-    new_names: Union[str, list[str]] = "default",
+    reference: list | str = "default",
+    bads: list[str] | None = None,
+    new_names: str | list[str] = "default",
     ecog_only: bool = False,
-    used_types: Optional[Iterable[str]] = ("ecog", "dbs", "seeg"),
-    target_keywords: Optional[Iterable[str]] = ("mov", "squared", "label"),
+    used_types: Iterable[str] | None = ("ecog", "dbs", "seeg"),
+    target_keywords: Iterable[str] | None = ("mov", "squared", "label"),
 ) -> pd.DataFrame:
     """Return dataframe with channel-specific settings in nm_channels format.
 
-        Return an nm_channels dataframe with the columns: "name", "rereference",
-        "used", "target", "type", "status", "new_name"]. "name" is set to ch_names,
-        "rereference" can be specified individually. "used" is set to 1 for all
-        channel types specified in `used_types`, else to 0. "target" is set to 1
-        for all channels containing any of the `target_keywords`, else to 0.
+    Return an nm_channels dataframe with the columns: "name", "rereference",
+    "used", "target", "type", "status", "new_name"]. "name" is set to ch_names,
+    "rereference" can be specified individually. "used" is set to 1 for all
+    channel types specified in `used_types`, else to 0. "target" is set to 1
+    for all channels containing any of the `target_keywords`, else to 0.
 
-        Possible channel types:
-        https://github.com/mne-tools/mne-python/blob/6ae3b22033c745cce5cd5de9b92da54c13c36484/doc/_includes/channel_types.rst
+    Possible channel types:
+    https://github.com/mne-tools/mne-python/blob/6ae3b22033c745cce5cd5de9b92da54c13c36484/doc/_includes/channel_types.rst
 
-        Arguments
-        ---------
-            ch_names : list
-                list of channel names.
-            ch_types : list
-                list of channel types. Should optimally be of the types: "ECOG",
-                "DBS" or "SEEG".
-            reference : str | list of str | None, default: 'default'
-                re-referencing scheme. Default is "default". This sets ECOG channel
-                references to "average" and creates a bipolar referencing scheme
-                for LFP/DBS/SEEG channels, where each channel is referenced to
-                the adjacent lower channel, split by left and right hemisphere.
-                For this, the channel names must contain the substring "_L_" and/or
-                "_R_" (lower or upper case). CAVE: Adjacent channels will be
-                determined using the sort() function.
-            bads :  str | list of str, default: None
-                channels that should be marked as bad and not be used for
-                average re-referencing etc.
-            new_names : list of str | None, default: 'default'
-                new channel names that should be used when writing out the
-                features and results. Useful when applying re-referencing. Set to
-                'None' if no renaming should be performed. 'default' will infer
-                channel renaming from re-referencing information. If a list is
-                given, it should be in the same order as 'ch_names'.
-            ECOG_ONLY : boolean, default: False
-                if True, set only 'ecog' channel type to used
-            used_types : iterable of str | None, default : ("ecog", "dbs", "seeg")
-                data channel types to be used. Set to `None` to use no channel
-                types.
-            target_keywords : iterable of str | None, default : ("ecog", "dbs", "seeg")
-                keywords for target channels
+    Arguments
+    ---------
+        ch_names : list
+            list of channel names.
+        ch_types : list
+            list of channel types. Should optimally be of the types: "ECOG",
+            "DBS" or "SEEG".
+        reference : str | list of str | None, default: 'default'
+            re-referencing scheme. Default is "default". This sets ECOG channel
+            references to "average" and creates a bipolar referencing scheme
+            for LFP/DBS/SEEG channels, where each channel is referenced to
+            the adjacent lower channel, split by left and right hemisphere.
+            For this, the channel names must contain the substring "_L_" and/or
+            "_R_" (lower or upper case). CAVE: Adjacent channels will be
+            determined using the sort() function.
+        bads :  str | list of str, default: None
+            channels that should be marked as bad and not be used for
+            average re-referencing etc.
+        new_names : list of str | None, default: 'default'
+            new channel names that should be used when writing out the
+            features and results. Useful when applying re-referencing. Set to
+            'None' if no renaming should be performed. 'default' will infer
+            channel renaming from re-referencing information. If a list is
+            given, it should be in the same order as 'ch_names'.
+        ECOG_ONLY : boolean, default: False
+            if True, set only 'ecog' channel type to used
+        used_types : iterable of str | None, default : ("ecog", "dbs", "seeg")
+            data channel types to be used. Set to `None` to use no channel
+            types.
+        target_keywords : iterable of str | None, default : ("ecog", "dbs", "seeg")
+            keywords for target channels
 
-        Returns
-        -------
-            df: DataFrame in nm_channels format
+    Returns
+    -------
+        df: DataFrame in nm_channels format
     """
     if not (len(ch_names) == len(ch_types)):
         raise ValueError(
@@ -86,13 +86,13 @@ def set_channels(
     df["name"] = ch_names
 
     if used_types:
-        if type(used_types) is str:
-            used_types = [used_types]          # Even if the user passes only ("ecog"), the if statement bellow will work
+        if isinstance(used_types, str):
+            used_types = [
+                used_types
+            ]  # Even if the user passes only ("ecog"), the if statement bellow will work
         used_list = []
         for ch_type in ch_types:
-            if any(
-                use_type.lower() == ch_type.lower() for use_type in used_types
-            ):
+            if any(use_type.lower() == ch_type.lower() for use_type in used_types):
                 used_list.append(1)
             else:
                 used_list.append(0)
@@ -101,7 +101,7 @@ def set_channels(
         df["used"] = 0
 
     if target_keywords:
-        if type(target_keywords) is str:
+        if isinstance(target_keywords, str):
             target_keywords = [target_keywords]
         targets = []
         for ch_name in ch_names:
@@ -122,9 +122,7 @@ def set_channels(
 
     if isinstance(reference, str):
         if reference.lower() == "default":
-            df = _get_default_references(
-                df=df, ch_names=ch_names, ch_types=ch_types
-            )
+            df = _get_default_references(df=df, ch_names=ch_names, ch_types=ch_types)
         else:
             raise ValueError(
                 "`reference` must be either `default`, `None` or "
@@ -153,9 +151,9 @@ def set_channels(
         if isinstance(bads, str):
             bads = [bads]
         df["status"] = ["bad" if ch in bads else "good" for ch in ch_names]
-        df.loc[
-            df["status"] == "bad", "used"
-        ] = 0  # setting bad channels to not being used
+        df.loc[df["status"] == "bad", "used"] = (
+            0  # setting bad channels to not being used
+        )
     else:
         df["status"] = "good"
 
@@ -172,7 +170,7 @@ def set_channels(
         for name, ref in zip(df["name"], df["rereference"]):
             if ref == "None":
                 new_names.append(name)
-            elif type(ref) == float:
+            elif isinstance(ref, float):
                 if np.isnan(ref):
                     new_names.append(name)
             elif ref == "average":
@@ -226,17 +224,15 @@ def _get_default_references(
         if ("_r_" in lfp_ch.lower()) or ("_right_" in lfp_ch.lower())
     ]
     lfp_r.sort()
-    lfp_l_refs = [
-        lfp_l[i - 1] if i > 0 else lfp_l[-1] for i, _ in enumerate(lfp_l)
-    ]
-    lfp_r_refs = [
-        lfp_r[i - 1] if i > 0 else lfp_r[-1] for i, _ in enumerate(lfp_r)
-    ]
+    lfp_l_refs = [lfp_l[i - 1] if i > 0 else lfp_l[-1] for i, _ in enumerate(lfp_l)]
+    lfp_r_refs = [lfp_r[i - 1] if i > 0 else lfp_r[-1] for i, _ in enumerate(lfp_r)]
     ref_idx = list(df.columns).index("rereference")
     if len(ecog_chs) > 1:
         for ecog_ch in ecog_chs:
             df.iloc[df[df["name"] == ecog_ch].index[0], ref_idx] = "average"
-    if len(lfp_l) > 1:  # if there is only a single channel, the channel would be subtracted from itself
+    if (
+        len(lfp_l) > 1
+    ):  # if there is only a single channel, the channel would be subtracted from itself
         for i, lfp in enumerate(lfp_l):
             df.iloc[df[df["name"] == lfp].index[0], ref_idx] = lfp_l_refs[i]
     if len(lfp_r) > 1:
@@ -244,20 +240,22 @@ def _get_default_references(
             df.iloc[df[df["name"] == lfp].index[0], ref_idx] = lfp_r_refs[i]
     for other_ch in other_chs:
         df.iloc[df[df["name"] == other_ch].index[0], ref_idx] = "None"
-    
+
     df = df.replace(np.nan, "None")
 
     return df
 
+
 def get_default_channels_from_data(
-        data: np.array, car_rereferencing:bool=True,
+    data: np.ndarray,
+    car_rereferencing: bool = True,
 ):
     """Return default nm_channels dataframe with
-    ecog datatype, no bad channels, no targets, common average rereferencing 
+    ecog datatype, no bad channels, no targets, common average rereferencing
 
     Parameters
     ----------
-    data : np.array
+    data : np.ndarray
         Data array in shape (n_channels, n_time)
     car_rereferencing : bool, optional
         use common average rereferencing, by default True
@@ -265,7 +263,7 @@ def get_default_channels_from_data(
     Returns
     -------
     pd.DataFrame
-        nm_channel dataframe containing columns: 
+        nm_channel dataframe containing columns:
          - name
          - rereference
          - used
@@ -279,7 +277,7 @@ def get_default_channels_from_data(
     status = ["good" for _ in range(data.shape[0])]
     type_nm = ["ecog" for _ in range(data.shape[0])]
 
-    if car_rereferencing is True:
+    if car_rereferencing:
         rereference = ["average" for _ in range(data.shape[0])]
         new_name = [f"{ch}-avgref" for ch in ch_name]
     else:
diff --git a/py_neuromodulation/nm_eval_timing.py b/py_neuromodulation/nm_eval_timing.py
index 4a383e9d..56a399c7 100644
--- a/py_neuromodulation/nm_eval_timing.py
+++ b/py_neuromodulation/nm_eval_timing.py
@@ -1,12 +1,15 @@
 import timeit
 from . import nm_oscillatory
 import numpy as np
-from py_neuromodulation import (
-    nm_normalization,
-    nm_stft,
-    nm_bandpower,
-    nm_filter,
-)
+from py_neuromodulation.nm_normalization import normalize_features, normalize_raw
+from py_neuromodulation.nm_filter import apply_filter
+from py_neuromodulation.nm_oscillatory import STFT, BandPower
+
+# from py_neuromodulation import (
+# The following 2 files do not exist in the project, maybe there were deleted?
+# nm_stft,
+# nm_bandpower,
+# )
 
 
 class NM_Timer:
@@ -16,7 +19,6 @@ def __init__(self, analyzer) -> None:
         self.get_timings()
 
     def get_timings(self, number_repeat=1000):
-
         features_ = {}
         ch_idx = 0
         fs = self.analyzer.fs
@@ -50,9 +52,7 @@ def get_timings(self, number_repeat=1000):
             data = np.random.random(
                 [
                     N_CH_AFTER_REREF,
-                    self.analyzer.settings["raw_resampling_settings"][
-                        "resample_freq"
-                    ],
+                    self.analyzer.settings["raw_resampling_settings"]["resample_freq"],
                 ]
             )
 
@@ -68,22 +68,22 @@ def get_timings(self, number_repeat=1000):
         if self.analyzer.settings["methods"]["raw_normalization"]:
             dict_timings["time_norm_raw"] = (
                 timeit.timeit(
-                    lambda: nm_normalization.normalize_raw(
+                    lambda: normalize_raw(
                         current=data,
                         previous=data.T,
                         normalize_samples=int(
-                            self.analyzer.settings[
-                                "raw_normalization_settings"
-                            ]["normalization_time"]
+                            self.analyzer.settings["raw_normalization_settings"][
+                                "normalization_time"
+                            ]
                             * self.analyzer.fs
                         ),
                         sample_add=int(self.analyzer.fs / self.analyzer.fs_new),
-                        method=self.analyzer.settings[
-                            "raw_normalization_settings"
-                        ]["normalization_method"],
-                        clip=self.analyzer.settings[
-                            "raw_normalization_settings"
-                        ]["clip"],
+                        method=self.analyzer.settings["raw_normalization_settings"][
+                            "normalization_method"
+                        ],
+                        clip=self.analyzer.settings["raw_normalization_settings"][
+                            "clip"
+                        ],
                     ),
                     number=number_repeat,
                 )
@@ -98,16 +98,16 @@ def get_timings(self, number_repeat=1000):
         if self.analyzer.settings["methods"]["feature_normalization"]:
             dict_timings["time_feature_norm"] = (
                 timeit.timeit(
-                    lambda: nm_normalization.normalize_features(
+                    lambda: normalize_features(
                         current=features_current.to_numpy(),
                         previous=features_previous,
                         normalize_samples=self.analyzer.feat_normalize_samples,
-                        method=self.analyzer.settings[
-                            "feature_normalization_settings"
-                        ]["normalization_method"],
-                        clip=self.analyzer.settings[
-                            "feature_normalization_settings"
-                        ]["clip"],
+                        method=self.analyzer.settings["feature_normalization_settings"][
+                            "normalization_method"
+                        ],
+                        clip=self.analyzer.settings["feature_normalization_settings"][
+                            "clip"
+                        ],
                     ),
                     number=number_repeat,
                 )
@@ -117,9 +117,7 @@ def get_timings(self, number_repeat=1000):
         if self.analyzer.settings["methods"]["project_cortex"]:
             dict_timings["time_projection"] = (
                 timeit.timeit(
-                    lambda: self.analyzer.projection.project_features(
-                        features_current
-                    ),
+                    lambda: self.analyzer.projection.project_features(features_current),
                     number=number_repeat,
                 )
                 / number_repeat
@@ -197,7 +195,7 @@ def get_timings(self, number_repeat=1000):
                 )
             ).astype(int)
 
-            dat_filtered = nm_filter.apply_filter(
+            dat_filtered = apply_filter(
                 data, self.analyzer.features.filter_fun
             )  # shape (bands, time)
             dict_timings["time_bandpass_filter"] = (
diff --git a/py_neuromodulation/nm_features.py b/py_neuromodulation/nm_features.py
index fd4f1ba0..b33fb477 100644
--- a/py_neuromodulation/nm_features.py
+++ b/py_neuromodulation/nm_features.py
@@ -1,28 +1,25 @@
 import numpy as np
 
-from py_neuromodulation import (
-    nm_hjorth_raw,
-    nm_mne_connectivity,
-    nm_sharpwaves,
-    nm_coherence,
-    nm_fooof,
-    nm_nolds,
-    nm_features_abc,
-    nm_oscillatory,
-    nm_bursts,
-    nm_linelength,
-    nm_bispectra,
-)
+from py_neuromodulation.nm_hjorth_raw import Hjorth, Raw
+from py_neuromodulation.nm_oscillatory import BandPower, STFT, FFT, Welch
+from py_neuromodulation.nm_sharpwaves import SharpwaveAnalyzer
+from py_neuromodulation.nm_fooof import FooofAnalyzer
+from py_neuromodulation.nm_nolds import Nolds
+from py_neuromodulation.nm_coherence import NM_Coherence
+from py_neuromodulation.nm_bursts import Burst
+from py_neuromodulation.nm_linelength import LineLength
+from py_neuromodulation.nm_mne_connectivity import MNEConnectivity
+from py_neuromodulation.nm_bispectra import Bispectra
+
+from py_neuromodulation.nm_features_abc import Feature
 
 
 class Features:
-    """Class for calculating features."""
+    """Class for calculating features.p"""
 
-    features: list[nm_features_abc.Feature] = []
+    # features: list[nm_features_abc.Feature] = []
 
-    def __init__(
-        self, s: dict, ch_names: list[str], sfreq: int | float
-    ) -> None:
+    def __init__(self, s: dict, ch_names: list[str], sfreq: float) -> None:
         """_summary_
 
         Parameters
@@ -31,7 +28,7 @@ def __init__(
             _description_
         ch_names : list[str]
             _description_
-        sfreq : int | float
+        sfreq : float
             _description_
 
         Raises
@@ -40,40 +37,41 @@ def __init__(
             _description_
         """
 
-        self.features = []
+        self.features: list[Feature] = []
 
+        feature: Feature
         for feature in s["features"]:
-            if s["features"][feature] is False:
+            if not s["features"][feature]:
                 continue
             match feature:
                 case "raw_hjorth":
-                    FeatureClass = nm_hjorth_raw.Hjorth
+                    FeatureClass = Hjorth
                 case "return_raw":
-                    FeatureClass = nm_hjorth_raw.Raw
+                    FeatureClass = Raw
                 case "bandpass_filter":
-                    FeatureClass = nm_oscillatory.BandPower
+                    FeatureClass = BandPower
                 case "stft":
-                    FeatureClass = nm_oscillatory.STFT
+                    FeatureClass = STFT
                 case "fft":
-                    FeatureClass = nm_oscillatory.FFT
+                    FeatureClass = FFT
                 case "welch":
-                    FeatureClass = nm_oscillatory.Welch
+                    FeatureClass = Welch
                 case "sharpwave_analysis":
-                    FeatureClass = nm_sharpwaves.SharpwaveAnalyzer
+                    FeatureClass = SharpwaveAnalyzer
                 case "fooof":
-                    FeatureClass = nm_fooof.FooofAnalyzer
+                    FeatureClass = FooofAnalyzer
                 case "nolds":
-                    FeatureClass = nm_nolds.Nolds
+                    FeatureClass = Nolds
                 case "coherence":
-                    FeatureClass = nm_coherence.NM_Coherence
+                    FeatureClass = NM_Coherence
                 case "bursts":
-                    FeatureClass = nm_bursts.Burst
+                    FeatureClass = Burst
                 case "linelength":
-                    FeatureClass = nm_linelength.LineLength
+                    FeatureClass = LineLength
                 case "mne_connectivity":
-                    FeatureClass = nm_mne_connectivity.MNEConnectivity
+                    FeatureClass = MNEConnectivity
                 case "bispectrum":
-                    FeatureClass = nm_bispectra.Bispectra
+                    FeatureClass = Bispectra
                 case _:
                     raise ValueError(f"Unknown feature found. Got: {feature}.")
 
@@ -81,7 +79,7 @@ def __init__(
             f_obj = FeatureClass(s, ch_names, sfreq)
             self.features.append(f_obj)
 
-    def register_new_feature(self, feature: nm_features_abc.Feature) -> None:
+    def register_new_feature(self, feature: Feature) -> None:
         """Register new feature.
 
         Parameters
@@ -105,7 +103,7 @@ def estimate_features(self, data: np.ndarray) -> dict:
         dat (dict): naming convention : channel_method_feature_(f_band)
         """
 
-        features_compute = {}
+        features_compute: dict = {}
 
         for feature in self.features:
             features_compute = feature.calc_feature(
diff --git a/py_neuromodulation/nm_features_abc.py b/py_neuromodulation/nm_features_abc.py
index 3d5143a6..093f6f2a 100644
--- a/py_neuromodulation/nm_features_abc.py
+++ b/py_neuromodulation/nm_features_abc.py
@@ -1,13 +1,11 @@
 from abc import ABC, abstractmethod
 import numpy as np
-from typing import Iterable
+from collections.abc import Iterable
 
 
 class Feature(ABC):
     @abstractmethod
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: int | float
-    ) -> None:
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         pass
 
     @staticmethod
@@ -15,19 +13,19 @@ def __init__(
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         """Method to check passed settings"""
         pass
 
     @abstractmethod
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         """
         Feature calculation method. Each method needs to loop through all channels
 
         Parameters
         ----------
-        data : np.array
+        data : np.ndarray
             (channels, time)
         features_compute : dict
         ch_names : Iterable[str]
diff --git a/py_neuromodulation/nm_filter.py b/py_neuromodulation/nm_filter.py
index cb4651ba..e2ec45c8 100644
--- a/py_neuromodulation/nm_filter.py
+++ b/py_neuromodulation/nm_filter.py
@@ -1,12 +1,10 @@
 """Module for filter functionality."""
 
-import logging
-
-logger = logging.getLogger("PynmLogger")
-
-import mne
-from mne.filter import _overlap_add_filter
+from mne.filter import create_filter, _overlap_add_filter
 import numpy as np
+from typing import cast
+
+from py_neuromodulation import logger
 
 
 class MNEFilter:
@@ -23,13 +21,13 @@ class MNEFilter:
     ----------
     f_ranges : list of lists
         Frequency ranges. Inner lists must be of length 2.
-    sfreq : int | float
+    sfreq : float
         Sampling frequency.
     filter_length : str, optional
         Filter length. Human readable (e.g. "1000ms", "1s"), by default "999ms"
-    l_trans_bandwidth : int | float | str, optional
+    l_trans_bandwidth : float | str, optional
         Length of the lower transition band or "auto", by default 4
-    h_trans_bandwidth : int | float | str, optional
+    h_trans_bandwidth : float | str, optional
         Length of the higher transition band or "auto", by default 4
     verbose : bool | None, optional
         Verbosity level, by default None
@@ -42,11 +40,11 @@ class MNEFilter:
 
     def __init__(
         self,
-        f_ranges: list[list[int | float | None]] | list[int | float | None],
-        sfreq: int | float,
+        f_ranges: list[list[float | None]] | list[float | None],
+        sfreq: float,
         filter_length: str | float = "999ms",
-        l_trans_bandwidth: int | float | str = 4,
-        h_trans_bandwidth: int | float | str = 4,
+        l_trans_bandwidth: float | str = 4,
+        h_trans_bandwidth: float | str = 4,
         verbose: bool | int | str | None = None,
     ) -> None:
         filter_bank = []
@@ -59,7 +57,7 @@ def __init__(
 
         for f_range in f_ranges:
             try:
-                filt = mne.filter.create_filter(
+                filt = create_filter(
                     None,
                     sfreq,
                     l_freq=f_range[0],
@@ -70,8 +68,8 @@ def __init__(
                     filter_length=filter_length,  # type: ignore
                     verbose=verbose,
                 )
-            except:
-                filt = mne.filter.create_filter(
+            except ValueError:
+                filt = create_filter(
                     None,
                     sfreq,
                     l_freq=f_range[0],
@@ -109,10 +107,7 @@ def filter_data(self, data: np.ndarray) -> np.ndarray:
 
         filtered = np.array(
             [
-                [
-                    np.convolve(filt, chan, mode="same")
-                    for filt in self.filter_bank
-                ]
+                [np.convolve(filt, chan, mode="same") for filt in self.filter_bank]
                 for chan in data
             ]
         )
@@ -124,9 +119,7 @@ def filter_data(self, data: np.ndarray) -> np.ndarray:
             filtered = filtered[
                 :,
                 :,
-                middle_index
-                - data.shape[1] // 2 : middle_index
-                + data.shape[1] // 2,
+                middle_index - data.shape[1] // 2 : middle_index + data.shape[1] // 2,
             ]
 
         return filtered
@@ -135,17 +128,14 @@ def filter_data(self, data: np.ndarray) -> np.ndarray:
 class NotchFilter:
     def __init__(
         self,
-        sfreq: int | float,
-        line_noise: int | float | None = None,
+        sfreq: float,
+        line_noise: float | None = None,
         freqs: np.ndarray | None = None,
         notch_widths: int | np.ndarray | None = 3,
-        trans_bandwidth: int = 6.8,
+        trans_bandwidth: float = 6.8,
     ) -> None:
-        if line_noise is None and freqs is None:
-            raise ValueError(
-                "Either line_noise or freqs must be defined if notch_filter is"
-                "activated."
-            )
+        self.test_settings(line_noise, sfreq)
+
         if freqs is None:
             freqs = np.arange(line_noise, sfreq / 2, line_noise, dtype=int)
 
@@ -175,20 +165,16 @@ def __init__(
                 notch_widths = notch_widths[0] * np.ones_like(freqs)
             elif len(notch_widths) != len(freqs):
                 raise ValueError(
-                    "notch_widths must be None, scalar, or the "
-                    "same length as freqs"
+                    "notch_widths must be None, scalar, or the " "same length as freqs"
                 )
+        notch_widths = cast(np.ndarray, notch_widths)  # For MyPy only, no runtime cost
 
         # Speed this up by computing the fourier coefficients once
         tb_half = trans_bandwidth / 2.0
-        lows = [
-            freq - nw / 2.0 - tb_half for freq, nw in zip(freqs, notch_widths)
-        ]
-        highs = [
-            freq + nw / 2.0 + tb_half for freq, nw in zip(freqs, notch_widths)
-        ]
-
-        self.filter_bank = mne.filter.create_filter(
+        lows = [freq - nw / 2.0 - tb_half for freq, nw in zip(freqs, notch_widths)]
+        highs = [freq + nw / 2.0 + tb_half for freq, nw in zip(freqs, notch_widths)]
+
+        self.filter_bank = create_filter(
             data=None,
             sfreq=sfreq,
             l_freq=highs,
@@ -217,3 +203,10 @@ def process(self, data: np.ndarray) -> np.ndarray:
             copy=True,
             pad="reflect_limited",
         )
+
+    def test_settings(self, line_noise, freqs):
+        if line_noise is None and freqs is None:
+            raise ValueError(
+                "Either line_noise or freqs must be defined if notch_filter is"
+                "activated."
+            )
diff --git a/py_neuromodulation/nm_filter_preprocessing.py b/py_neuromodulation/nm_filter_preprocessing.py
index 3da9f784..868fd3e0 100644
--- a/py_neuromodulation/nm_filter_preprocessing.py
+++ b/py_neuromodulation/nm_filter_preprocessing.py
@@ -1,25 +1,24 @@
 import numpy as np
 
-from py_neuromodulation import nm_filter
+from py_neuromodulation.nm_filter import MNEFilter
 
 
 class PreprocessingFilter:
-
-    def __init__(self, settings: dict, sfreq: int | float) -> None:
+    def __init__(self, settings: dict, sfreq: float) -> None:
         self.s = settings
         self.sfreq = sfreq
         self.filters = []
 
-        if self.s["preprocessing_filter"]["bandstop_filter"] is True:
+        if self.s["preprocessing_filter"]["bandstop_filter"]:
             self.filters.append(
-                nm_filter.MNEFilter(
+                MNEFilter(
                     f_ranges=[
-                        self.s["preprocessing_filter"][
-                            "bandstop_filter_settings"
-                        ]["frequency_high_hz"],
-                        self.s["preprocessing_filter"][
-                            "bandstop_filter_settings"
-                        ]["frequency_low_hz"],
+                        self.s["preprocessing_filter"]["bandstop_filter_settings"][
+                            "frequency_high_hz"
+                        ],
+                        self.s["preprocessing_filter"]["bandstop_filter_settings"][
+                            "frequency_low_hz"
+                        ],
                     ],
                     sfreq=self.sfreq,
                     filter_length=self.sfreq - 1,
@@ -27,43 +26,43 @@ def __init__(self, settings: dict, sfreq: int | float) -> None:
                 )
             )
 
-        if self.s["preprocessing_filter"]["bandpass_filter"] is True:
+        if self.s["preprocessing_filter"]["bandpass_filter"]:
             self.filters.append(
-                nm_filter.MNEFilter(
+                MNEFilter(
                     f_ranges=[
-                        self.s["preprocessing_filter"][
-                            "bandpass_filter_settings"
-                        ]["frequency_low_hz"],
-                        self.s["preprocessing_filter"][
-                            "bandpass_filter_settings"
-                        ]["frequency_high_hz"],
+                        self.s["preprocessing_filter"]["bandpass_filter_settings"][
+                            "frequency_low_hz"
+                        ],
+                        self.s["preprocessing_filter"]["bandpass_filter_settings"][
+                            "frequency_high_hz"
+                        ],
                     ],
                     sfreq=self.sfreq,
                     filter_length=self.sfreq - 1,
                     verbose=False,
                 )
             )
-        if self.s["preprocessing_filter"]["lowpass_filter"] is True:
+        if self.s["preprocessing_filter"]["lowpass_filter"]:
             self.filters.append(
-                nm_filter.MNEFilter(
+                MNEFilter(
                     f_ranges=[
                         None,
-                        self.s["preprocessing_filter"][
-                            "lowpass_filter_settings"
-                        ]["frequency_cutoff_hz"],
+                        self.s["preprocessing_filter"]["lowpass_filter_settings"][
+                            "frequency_cutoff_hz"
+                        ],
                     ],
                     sfreq=self.sfreq,
                     filter_length=self.sfreq - 1,
                     verbose=False,
                 )
             )
-        if self.s["preprocessing_filter"]["highpass_filter"] is True:
+        if self.s["preprocessing_filter"]["highpass_filter"]:
             self.filters.append(
-                nm_filter.MNEFilter(
+                MNEFilter(
                     f_ranges=[
-                        self.s["preprocessing_filter"][
-                            "highpass_filter_settings"
-                        ]["frequency_cutoff_hz"],
+                        self.s["preprocessing_filter"]["highpass_filter_settings"][
+                            "frequency_cutoff_hz"
+                        ],
                         None,
                     ],
                     sfreq=self.sfreq,
@@ -85,7 +84,5 @@ def process(self, data: np.ndarray) -> np.ndarray:
         """
 
         for filter in self.filters:
-            data = filter.filter_data(
-                data if len(data.shape) == 2 else data[:, 0, :]
-            )
+            data = filter.filter_data(data if len(data.shape) == 2 else data[:, 0, :])
         return data if len(data.shape) == 2 else data[:, 0, :]
diff --git a/py_neuromodulation/nm_fooof.py b/py_neuromodulation/nm_fooof.py
index 5d2be7b5..df756206 100644
--- a/py_neuromodulation/nm_fooof.py
+++ b/py_neuromodulation/nm_fooof.py
@@ -1,17 +1,14 @@
-import logging
-from typing import Iterable
-
+from collections.abc import Iterable
 import numpy as np
 from fooof import FOOOF
 from scipy import fft
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
+from py_neuromodulation import logger
 
 
-class FooofAnalyzer(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class FooofAnalyzer(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.settings_fooof = settings["fooof"]
         self.sfreq = sfreq
         self.ch_names = ch_names
@@ -20,16 +17,15 @@ def __init__(
         self.ap_mode = "knee" if self.settings_fooof["knee"] else "fixed"
         self.max_n_peaks = self.settings_fooof["max_n_peaks"]
 
-        self.num_samples = int(
-            self.settings_fooof["windowlength_ms"] * sfreq / 1000
-        )
+        self.num_samples = int(self.settings_fooof["windowlength_ms"] * sfreq / 1000)
 
         self.f_vec = np.arange(0, int(self.num_samples / 2) + 1, 1)
 
+    @staticmethod
     def test_settings(
         s: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         assert isinstance(s["fooof"]["aperiodic"]["exponent"], bool)
         assert isinstance(s["fooof"]["aperiodic"]["offset"], bool)
@@ -39,9 +35,7 @@ def test_settings(
         assert isinstance(s["fooof"]["periodic"]["height_over_ap"], bool)
         assert isinstance(s["fooof"]["knee"], bool)
         assert isinstance(s["fooof"]["windowlength_ms"], (int, float))
-        assert (
-            s["fooof"]["windowlength_ms"] <= s["segment_length_features_ms"]
-        ), (
+        assert s["fooof"]["windowlength_ms"] <= s["segment_length_features_ms"], (
             "fooof windowlength_ms needs to be smaller equal than segment_length_features_ms "
             f"got windowlength_ms: {s['fooof']['windowlength_ms']} and {s['segment_length_features_ms']}"
         )
@@ -51,7 +45,7 @@ def test_settings(
             and s["fooof"]["freq_range_hz"][1] < sfreq
         ), f"fooof frequency range needs to be below sfreq, got {s['fooof']['freq_range_hz']}"
 
-    def _get_spectrum(self, data: np.array):
+    def _get_spectrum(self, data: np.ndarray):
         """return absolute value fft spectrum"""
 
         data = data[-self.num_samples :]
@@ -61,7 +55,7 @@ def _get_spectrum(self, data: np.array):
 
     def calc_feature(
         self,
-        data: np.array,
+        data: np.ndarray,
         features_compute: dict,
     ) -> dict:
         for ch_idx, ch_name in enumerate(self.ch_names):
@@ -78,7 +72,7 @@ def calc_feature(
                 )
                 fm.fit(self.f_vec, spectrum, self.freq_range)
             except Exception as e:
-                logging.critical(e, exc_info=True)
+                logger.critical(e, exc_info=True)
 
             if fm.fooofed_spectrum_ is None:
                 FIT_PASSED = False
@@ -100,39 +94,28 @@ def calc_feature(
                 )
 
             if self.settings_fooof["aperiodic"]["knee"]:
-                if FIT_PASSED is False:
+                if not FIT_PASSED:
                     knee_freq = None
                 else:
                     if fm.get_params("aperiodic_params", "exponent") != 0:
                         knee_fooof = fm.get_params("aperiodic_params", "knee")
                         knee_freq = np.nan_to_num(
                             knee_fooof
-                            ** (
-                                1
-                                / fm.get_params("aperiodic_params", "exponent")
-                            )
+                            ** (1 / fm.get_params("aperiodic_params", "exponent"))
                         )
                     else:
                         knee_freq = None
 
-                features_compute[f"{ch_name}_fooof_a_knee_frequency"] = (
-                    knee_freq
-                )
+                features_compute[f"{ch_name}_fooof_a_knee_frequency"] = knee_freq
 
             peaks_bw = (
-                fm.get_params("peak_params", "BW")
-                if FIT_PASSED is True
-                else None
+                fm.get_params("peak_params", "BW") if FIT_PASSED is True else None
             )
             peaks_cf = (
-                fm.get_params("peak_params", "CF")
-                if FIT_PASSED is True
-                else None
+                fm.get_params("peak_params", "CF") if FIT_PASSED is True else None
             )
             peaks_pw = (
-                fm.get_params("peak_params", "PW")
-                if FIT_PASSED is True
-                else None
+                fm.get_params("peak_params", "PW") if FIT_PASSED is True else None
             )
 
             if type(peaks_bw) is np.float64 or peaks_bw is None:
diff --git a/py_neuromodulation/nm_generator.py b/py_neuromodulation/nm_generator.py
index b6667daa..226cad06 100644
--- a/py_neuromodulation/nm_generator.py
+++ b/py_neuromodulation/nm_generator.py
@@ -1,12 +1,11 @@
-from typing import Iterator
-
+from collections.abc import Iterator
 import numpy as np
 
 
 def raw_data_generator(
     data: np.ndarray,
     settings: dict,
-    sfreq: int,
+    sfreq: float,
 ) -> Iterator[np.ndarray]:
     """
     This generator function mimics online data acquisition.
@@ -16,7 +15,7 @@ def raw_data_generator(
         ieeg_raw (np array): shape (channels, time)
         sfreq: int
         sfreq_new: int
-        offset_time: int | float
+        offset_time: float
     Returns
     -------
         np.array: new batch for run function of full segment length shape
@@ -28,10 +27,10 @@ def raw_data_generator(
     ratio_samples_features = sfreq / sfreq_new
 
     ratio_counter = 0
-    for cnt in range(data.shape[1]+1):  # shape + 1 guarantees that the last sample is also included
-
+    for cnt in range(
+        data.shape[1] + 1
+    ):  # shape + 1 guarantees that the last sample is also included
         if (cnt - offset_start) >= ratio_samples_features * ratio_counter:
-
             ratio_counter += 1
 
-            yield data[:, np.floor(cnt-offset_start).astype(int) : cnt]
+            yield data[:, np.floor(cnt - offset_start).astype(int) : cnt]
diff --git a/py_neuromodulation/nm_hjorth_raw.py b/py_neuromodulation/nm_hjorth_raw.py
index 4975ded6..6019d603 100644
--- a/py_neuromodulation/nm_hjorth_raw.py
+++ b/py_neuromodulation/nm_hjorth_raw.py
@@ -1,14 +1,11 @@
-import enum
 import numpy as np
-from typing import Iterable
+from collections.abc import Iterable
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
 
 
-class Hjorth(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class Hjorth(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.s = settings
         self.ch_names = ch_names
 
@@ -16,46 +13,36 @@ def __init__(
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         # no settings to test
         pass
 
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         for ch_idx, ch_name in enumerate(self.ch_names):
-            features_compute[
-                "_".join([ch_name, "RawHjorth_Activity"])
-            ] = np.nan_to_num(np.var(data[ch_idx, :]))
-            deriv_variance = np.nan_to_num(np.var(np.diff(data[ch_idx, :])))
-            mobility = np.nan_to_num(
-                np.sqrt(deriv_variance / np.var(data[ch_idx, :]))
+            features_compute["_".join([ch_name, "RawHjorth_Activity"])] = np.nan_to_num(
+                np.var(data[ch_idx, :])
             )
-            features_compute[
-                "_".join([ch_name, "RawHjorth_Mobility"])
-            ] = mobility
+            deriv_variance = np.nan_to_num(np.var(np.diff(data[ch_idx, :])))
+            mobility = np.nan_to_num(np.sqrt(deriv_variance / np.var(data[ch_idx, :])))
+            features_compute["_".join([ch_name, "RawHjorth_Mobility"])] = mobility
 
-            dat_deriv_2_var = np.nan_to_num(
-                np.var(np.diff(np.diff(data[ch_idx, :])))
-            )
-            deriv_mobility = np.nan_to_num(
-                np.sqrt(dat_deriv_2_var / deriv_variance)
+            dat_deriv_2_var = np.nan_to_num(np.var(np.diff(np.diff(data[ch_idx, :]))))
+            deriv_mobility = np.nan_to_num(np.sqrt(dat_deriv_2_var / deriv_variance))
+            features_compute["_".join([ch_name, "RawHjorth_Complexity"])] = (
+                np.nan_to_num(deriv_mobility / mobility)
             )
-            features_compute[
-                "_".join([ch_name, "RawHjorth_Complexity"])
-            ] = np.nan_to_num(deriv_mobility / mobility)
 
         return features_compute
 
 
-class Raw(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class Raw(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.ch_names = ch_names
 
     def calc_feature(
         self,
-        data: np.array,
+        data: np.ndarray,
         features_compute: dict,
     ) -> dict:
         for ch_idx, ch_name in enumerate(self.ch_names):
@@ -67,7 +54,7 @@ def calc_feature(
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         # no settings to test
         pass
diff --git a/py_neuromodulation/nm_kalmanfilter.py b/py_neuromodulation/nm_kalmanfilter.py
index b2ceb0e4..efde0fe6 100644
--- a/py_neuromodulation/nm_kalmanfilter.py
+++ b/py_neuromodulation/nm_kalmanfilter.py
@@ -1,5 +1,5 @@
 from numpy import array, cov
-from typing import Iterable
+from collections.abc import Iterable
 
 from filterpy.kalman import KalmanFilter
 
@@ -29,30 +29,34 @@ def define_KF(Tp, sigma_w, sigma_v):
     f.F = array([[1, Tp], [0, 1]])
     f.H = array([[1, 0]])
     f.R = sigma_v
-    f.Q = array([[(sigma_w**2)*(Tp**3)/3, (sigma_w**2)*(Tp**2)/2],
-                 [(sigma_w**2)*(Tp**2)/2, (sigma_w**2)*Tp]])
+    f.Q = array(
+        [
+            [(sigma_w**2) * (Tp**3) / 3, (sigma_w**2) * (Tp**2) / 2],
+            [(sigma_w**2) * (Tp**2) / 2, (sigma_w**2) * Tp],
+        ]
+    )
     f.P = cov([[1, 0], [0, 1]])
     return f
 
+
 def test_kf_settings(
-        s: dict,
-        ch_names: Iterable[str],
-        sfreq: int | float,
-    ):
-        assert isinstance(s["kalman_filter_settings"]["Tp"], (float, int))
-        assert isinstance(s["kalman_filter_settings"]["sigma_w"], (float, int))
-        assert isinstance(s["kalman_filter_settings"]["sigma_v"], (float, int))
-        assert s["kalman_filter_settings"][
-            "frequency_bands"
-        ], "No frequency bands specified for Kalman filter."
-        assert isinstance(
-            s["kalman_filter_settings"]["frequency_bands"], list
-        ), "Frequency bands for Kalman filter must be specified as a list."
-        assert (
-            item
-            in s["frequency_ranges_hz"].values()
-            for item in s["kalman_filter_settings"]["frequency_bands"]
-        ), (
-            "Frequency bands for Kalman filter must also be specified in "
-            "bandpass_filter_settings."
-        )
\ No newline at end of file
+    s: dict,
+    ch_names: Iterable[str],
+    sfreq: float,
+):
+    assert isinstance(s["kalman_filter_settings"]["Tp"], (float, int))
+    assert isinstance(s["kalman_filter_settings"]["sigma_w"], (float, int))
+    assert isinstance(s["kalman_filter_settings"]["sigma_v"], (float, int))
+    assert s["kalman_filter_settings"][
+        "frequency_bands"
+    ], "No frequency bands specified for Kalman filter."
+    assert isinstance(
+        s["kalman_filter_settings"]["frequency_bands"], list
+    ), "Frequency bands for Kalman filter must be specified as a list."
+    assert (
+        item in s["frequency_ranges_hz"].values()
+        for item in s["kalman_filter_settings"]["frequency_bands"]
+    ), (
+        "Frequency bands for Kalman filter must also be specified in "
+        "bandpass_filter_settings."
+    )
diff --git a/py_neuromodulation/nm_linelength.py b/py_neuromodulation/nm_linelength.py
index 3794cfa4..0666e15b 100644
--- a/py_neuromodulation/nm_linelength.py
+++ b/py_neuromodulation/nm_linelength.py
@@ -1,33 +1,31 @@
 import numpy as np
-from typing import Iterable
+from collections.abc import Iterable
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
 
 
-class LineLength(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class LineLength(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.s = settings
         self.ch_names = ch_names
 
     @staticmethod
-    def get_line_length(x: np.ndarray) -> np.ndarray:
+    def get_line_length(x: np.ndarray) -> np.floating:
         return np.mean(np.abs(np.diff(x)) / (x.shape[0] - 1))
 
     @staticmethod
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         # no settings to be checked
         pass
 
     def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         for ch_idx, ch_name in enumerate(self.ch_names):
-            features_compute[
-                "_".join([ch_name, "LineLength"])
-            ] = self.get_line_length(data[ch_idx, :])
+            features_compute["_".join([ch_name, "LineLength"])] = self.get_line_length(
+                data[ch_idx, :]
+            )
 
         return features_compute
diff --git a/py_neuromodulation/nm_mne_connectivity.py b/py_neuromodulation/nm_mne_connectivity.py
index a13e4fdc..f2ee6893 100644
--- a/py_neuromodulation/nm_mne_connectivity.py
+++ b/py_neuromodulation/nm_mne_connectivity.py
@@ -1,13 +1,13 @@
-from typing import Iterable
+from collections.abc import Iterable
 import numpy as np
 
 import mne
 from mne_connectivity import spectral_connectivity_epochs
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
 
 
-class MNEConnectivity(nm_features_abc.Feature):
+class MNEConnectivity(Feature):
     def __init__(
         self,
         settings: dict,
@@ -21,30 +21,26 @@ def __init__(
         self.sfreq = sfreq
 
         self.fbands = list(self.s["frequency_ranges_hz"].keys())
-        self.fband_ranges = []
+        self.fband_ranges: list = []
 
     @staticmethod
     def test_settings(
         settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         # TODO: Double check passed parameters with mne_connectivity
         pass
 
     @staticmethod
-    def get_epoched_data(
-        raw: mne.io.RawArray, epoch_length: float = 1
-    ) -> np.array:
+    def get_epoched_data(raw: mne.io.RawArray, epoch_length: float = 1) -> np.ndarray:
         time_samples_s = raw.get_data().shape[1] / raw.info["sfreq"]
         if epoch_length > time_samples_s:
-             raise ValueError(
+            raise ValueError(
                 f"the intended epoch length for mne connectivity: {epoch_length}s"
                 f" are longer than the passed data array {np.round(time_samples_s, 2)}s"
-             )
-        events = mne.make_fixed_length_events(
-            raw, duration=epoch_length, overlap=0
-        )
+            )
+        events = mne.make_fixed_length_events(raw, duration=epoch_length, overlap=0)
         event_id = {"rest": 1}
 
         epochs = mne.Epochs(
@@ -64,7 +60,7 @@ def get_epoched_data(
         return epochs
 
     def estimate_connectivity(self, epochs: mne.Epochs):
-        # n_jobs is here kept to 1, since setup of the multiprocessing Pool 
+        # n_jobs is here kept to 1, since setup of the multiprocessing Pool
         # takes longer than most batch computing sizes
 
         spec_out = spectral_connectivity_epochs(
@@ -79,14 +75,13 @@ def estimate_connectivity(self, epochs: mne.Epochs):
         )
         return spec_out
 
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
-
+    def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         raw = mne.io.RawArray(
             data=data,
             info=mne.create_info(ch_names=self.ch_names, sfreq=self.sfreq),
         )
         epochs = self.get_epoched_data(raw)
-        # there need to be minimum 2 of two epochs, otherwise mne_connectivity 
+        # there need to be minimum 2 of two epochs, otherwise mne_connectivity
         # is not correctly initialized
 
         spec_out = self.estimate_connectivity(epochs)
@@ -105,8 +100,8 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
         dat_conn = spec_out.get_data()
         for conn in np.arange(dat_conn.shape[0]):
             for fband_idx, fband in enumerate(self.fbands):
-                features_compute[
-                    "_".join(["ch1", self.method, str(conn), fband])
-                ] = np.mean(dat_conn[conn, self.fband_ranges[fband_idx]])
+                features_compute["_".join(["ch1", self.method, str(conn), fband])] = (
+                    np.mean(dat_conn[conn, self.fband_ranges[fband_idx]])
+                )
 
         return features_compute
diff --git a/py_neuromodulation/nm_nolds.py b/py_neuromodulation/nm_nolds.py
index 6eff4700..8db9e1b9 100644
--- a/py_neuromodulation/nm_nolds.py
+++ b/py_neuromodulation/nm_nolds.py
@@ -1,28 +1,25 @@
 import numpy as np
-from typing import Iterable
+from collections.abc import Iterable
 import nolds
-import warnings
 
-from py_neuromodulation import nm_features_abc, nm_oscillatory
+from py_neuromodulation.nm_features_abc import Feature
+from py_neuromodulation.nm_oscillatory import BandPower
+from py_neuromodulation import logger
 
 
-class Nolds(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class Nolds(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.s = settings
         self.ch_names = ch_names
 
         if len(self.s["nolds_features"]["data"]["frequency_bands"]) > 0:
-            self.bp_filter = nm_oscillatory.BandPower(
-                settings, ch_names, sfreq, use_kf=False
-            )
+            self.bp_filter = BandPower(settings, ch_names, sfreq, use_kf=False)
 
     @staticmethod
     def test_settings(
         s: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
         nolds_feature_cols = [
             "sample_entropy",
@@ -32,21 +29,18 @@ def test_settings(
             "detrended_fluctutaion_analysis",
         ]
         if sum([s["nolds_features"][f] for f in nolds_feature_cols]) == 0:
-            warnings.warn(
-                "nolds feature enabled, but no nolds_feature type selected"
-            )
+            logger.warn("nolds feature enabled, but no nolds_feature type selected")
 
         for fb in s["nolds_features"]["data"]["frequency_bands"]:
-            assert fb in list(
-                s["frequency_ranges_hz"].keys()
+            assert (
+                fb in list(s["frequency_ranges_hz"].keys())
             ), f"{fb} selected in nolds_features, but not defined in s['frequency_ranges_hz']"
 
     def calc_feature(
         self,
-        data: np.array,
+        data: np.ndarray,
         features_compute: dict,
     ) -> dict:
-
         data = np.nan_to_num(data)
         if self.s["nolds_features"]["data"]["raw"]:
             features_compute = self.calc_nolds(data, features_compute)
@@ -63,28 +57,27 @@ def calc_feature(
         return features_compute
 
     def calc_nolds(
-        self, data: np.array, features_compute: dict, data_str: str = "raw"
+        self, data: np.ndarray, features_compute: dict, data_str: str = "raw"
     ) -> dict:
-
         for ch_idx, ch_name in enumerate(self.ch_names):
             dat = data[ch_idx, :]
             empty_arr = dat.sum() == 0
             if self.s["nolds_features"]["sample_entropy"]:
-                features_compute[
-                    f"{ch_name}_nolds_sample_entropy"
-                ] = nolds.sampen(dat) if not empty_arr else 0
+                features_compute[f"{ch_name}_nolds_sample_entropy"] = (
+                    nolds.sampen(dat) if not empty_arr else 0
+                )
             if self.s["nolds_features"]["correlation_dimension"]:
                 features_compute[
                     f"{ch_name}_nolds_correlation_dimension_{data_str}"
                 ] = nolds.corr_dim(dat, emb_dim=2) if not empty_arr else 0
             if self.s["nolds_features"]["lyapunov_exponent"]:
-                features_compute[
-                    f"{ch_name}_nolds_lyapunov_exponent_{data_str}"
-                ] = nolds.lyap_r(dat) if not empty_arr else 0
+                features_compute[f"{ch_name}_nolds_lyapunov_exponent_{data_str}"] = (
+                    nolds.lyap_r(dat) if not empty_arr else 0
+                )
             if self.s["nolds_features"]["hurst_exponent"]:
-                features_compute[
-                    f"{ch_name}_nolds_hurst_exponent_{data_str}"
-                ] = nolds.hurst_rs(dat) if not empty_arr else 0
+                features_compute[f"{ch_name}_nolds_hurst_exponent_{data_str}"] = (
+                    nolds.hurst_rs(dat) if not empty_arr else 0
+                )
             if self.s["nolds_features"]["detrended_fluctutaion_analysis"]:
                 features_compute[
                     f"{ch_name}_nolds_detrended_fluctutaion_analysis_{data_str}"
diff --git a/py_neuromodulation/nm_normalization.py b/py_neuromodulation/nm_normalization.py
index b4b14cd7..1fe6459d 100644
--- a/py_neuromodulation/nm_normalization.py
+++ b/py_neuromodulation/nm_normalization.py
@@ -1,8 +1,11 @@
 """Module for real-time data normalization."""
+
 from enum import Enum
 
 from sklearn import preprocessing
 import numpy as np
+
+
 class NORM_METHODS(Enum):
     MEAN = "mean"
     MEDIAN = "median"
@@ -15,7 +18,7 @@ class NORM_METHODS(Enum):
 
 
 def test_normalization_settings(
-    normalization_time_s: int | float, normalization_method: str, clip: bool
+    normalization_time_s: float, normalization_method: str, clip: bool
 ):
     assert isinstance(
         normalization_time_s,
@@ -37,11 +40,11 @@ def test_normalization_settings(
 class RawNormalizer:
     def __init__(
         self,
-        sfreq: int | float,
-        sampling_rate_features_hz: int,
+        sfreq: float,
+        sampling_rate_features_hz: float,
         normalization_method: str = "zscore",
-        normalization_time_s: int | float = 30,
-        clip: bool | int | float = False,
+        normalization_time_s: float = 30,
+        clip: float = 0,
     ) -> None:
         """Normalize raw data.
 
@@ -53,7 +56,7 @@ def __init__(
             data is normalized via subtraction of the 'mean' or 'median' and
             subsequent division by the 'mean' or 'median'. For z-scoring enter
             'zscore'.
-        clip : int | float, optional
+        clip : float, optional
             value at which to clip after normalization
         """
 
@@ -63,11 +66,11 @@ def __init__(
         self.clip = clip
         self.num_samples_normalize = int(normalization_time_s * sfreq)
         self.add_samples = int(sfreq / sampling_rate_features_hz)
-        self.previous = None
+        self.previous: np.ndarray = np.array([])  # Default empty array
 
     def process(self, data: np.ndarray) -> np.ndarray:
         data = data.T
-        if self.previous is None:
+        if self.previous.size == 0:  # Check if empty
             self.previous = data
             return data.T
 
@@ -85,14 +88,17 @@ def process(self, data: np.ndarray) -> np.ndarray:
 
         return data.T
 
+    def test_settings(self, normalization_time_s, normalization_method, clip):
+        test_normalization_settings(normalization_time_s, normalization_method, clip)
+
 
 class FeatureNormalizer:
     def __init__(
         self,
-        sampling_rate_features_hz: int,
+        sampling_rate_features_hz: float,
         normalization_method: str = "zscore",
-        normalization_time_s: int | float = 30,
-        clip: bool | int | float = False,
+        normalization_time_s: float = 30,
+        clip: float = 0,
     ) -> None:
         """Normalize raw data.
 
@@ -104,21 +110,20 @@ def __init__(
             data is normalized via subtraction of the 'mean' or 'median' and
             subsequent division by the 'mean' or 'median'. For z-scoring enter
             'zscore'.
-        clip : int | float, optional
+        clip : float, optional
             value at which to clip after normalization
         """
-
-        test_normalization_settings(normalization_time_s, normalization_method, clip)
+        self.test_settings(normalization_time_s, normalization_method, clip)
 
         self.method = normalization_method
         self.clip = clip
         self.num_samples_normalize = int(
             normalization_time_s * sampling_rate_features_hz
         )
-        self.previous = None
+        self.previous: np.ndarray = np.array([])
 
     def process(self, data: np.ndarray) -> np.ndarray:
-        if self.previous is None:
+        if self.previous.size == 0:
             self.previous = data
             return data
 
@@ -136,23 +141,44 @@ def process(self, data: np.ndarray) -> np.ndarray:
 
         return data
 
+    def test_settings(self, normalization_time_s, normalization_method, clip):
+        test_normalization_settings(normalization_time_s, normalization_method, clip)
+
+
 """
 Functions to check for NaN's before deciding which Numpy function to call
 """
+
+
 def nan_mean(data, axis):
-    return np.nanmean(data, axis=axis) if np.any(np.isnan(sum(data))) else np.mean(data, axis=axis)
+    return (
+        np.nanmean(data, axis=axis)
+        if np.any(np.isnan(sum(data)))
+        else np.mean(data, axis=axis)
+    )
+
 
 def nan_std(data, axis):
-    return np.nanstd(data, axis=axis) if np.any(np.isnan(sum(data))) else np.std(data, axis=axis)
+    return (
+        np.nanstd(data, axis=axis)
+        if np.any(np.isnan(sum(data)))
+        else np.std(data, axis=axis)
+    )
+
 
 def nan_median(data, axis):
-    return np.nanmedian(data, axis=axis) if np.any(np.isnan(sum(data))) else np.median(data, axis=axis)
+    return (
+        np.nanmedian(data, axis=axis)
+        if np.any(np.isnan(sum(data)))
+        else np.median(data, axis=axis)
+    )
+
 
 def _normalize_and_clip(
     current: np.ndarray,
     previous: np.ndarray,
     method: str,
-    clip: int | float | bool,
+    clip: float | bool,
     description: str,
 ) -> tuple[np.ndarray, np.ndarray]:
     """Normalize data."""
@@ -166,34 +192,39 @@ def _normalize_and_clip(
         case NORM_METHODS.ZSCORE.value:
             current = (current - nan_mean(previous, axis=0)) / nan_std(previous, axis=0)
         case NORM_METHODS.ZSCORE_MEDIAN.value:
-            current = (current - nan_median(previous, axis=0)) / nan_std(previous, axis=0)
+            current = (current - nan_median(previous, axis=0)) / nan_std(
+                previous, axis=0
+            )
         # For the following methods we check for the shape of current
         # when current is a 1D array, then it is the post-processing normalization,
         # and we need to expand, and remove the extra dimension afterwards
         # When current is a 2D array, then it is pre-processing normalization, and
         # there's no need for expanding.
-        case (NORM_METHODS.QUANTILE.value | 
-              NORM_METHODS.ROBUST.value | 
-              NORM_METHODS.MINMAX.value | 
-              NORM_METHODS.POWER.value):
-            
+        case (
+            NORM_METHODS.QUANTILE.value
+            | NORM_METHODS.ROBUST.value
+            | NORM_METHODS.MINMAX.value
+            | NORM_METHODS.POWER.value
+        ):
             norm_methods = {
-                NORM_METHODS.QUANTILE.value : lambda: preprocessing.QuantileTransformer(n_quantiles=300),
-                NORM_METHODS.ROBUST.value : preprocessing.RobustScaler,
-                NORM_METHODS.MINMAX.value : preprocessing.MinMaxScaler,
-                NORM_METHODS.POWER.value : preprocessing.PowerTransformer
+                NORM_METHODS.QUANTILE.value: lambda: preprocessing.QuantileTransformer(
+                    n_quantiles=300
+                ),
+                NORM_METHODS.ROBUST.value: preprocessing.RobustScaler,
+                NORM_METHODS.MINMAX.value: preprocessing.MinMaxScaler,
+                NORM_METHODS.POWER.value: preprocessing.PowerTransformer,
             }
-                
+
             current = (
                 norm_methods[method]()
                 .fit(np.nan_to_num(previous))
                 .transform(
                     # if post-processing: pad dimensions to 2
-                    np.reshape(current, (2-len(current.shape))*(1,) + current.shape)
-                    )
-                .squeeze() # if post-processing: remove extra dimension
+                    np.reshape(current, (2 - len(current.shape)) * (1,) + current.shape)
+                )
+                .squeeze()  # if post-processing: remove extra dimension
             )
-            
+
         case _:
             raise ValueError(
                 f"Only {[e.value for e in NORM_METHODS]} are supported as "
@@ -205,9 +236,9 @@ def _normalize_and_clip(
     return current, previous
 
 
-def _clip(data: np.ndarray, clip: bool | int | float) -> np.ndarray:
+def _clip(data: np.ndarray, clip: float) -> np.ndarray:
     """Clip data."""
-    if clip is True:
+    if not clip:
         clip = 3.0  # default value
     else:
         clip = float(clip)
diff --git a/py_neuromodulation/nm_oscillatory.py b/py_neuromodulation/nm_oscillatory.py
index c019613b..2b26c1ba 100644
--- a/py_neuromodulation/nm_oscillatory.py
+++ b/py_neuromodulation/nm_oscillatory.py
@@ -1,19 +1,19 @@
-from typing import Iterable
-
+from collections.abc import Iterable
 import numpy as np
-from scipy import fft, signal
+from scipy.signal import welch, stft
+from scipy.fft import rfft, rfftfreq
 
-from py_neuromodulation import nm_filter, nm_features_abc, nm_kalmanfilter
+from py_neuromodulation.nm_kalmanfilter import define_KF
+from py_neuromodulation.nm_features_abc import Feature
+from py_neuromodulation.nm_filter import MNEFilter
 
 
-class OscillatoryFeature(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class OscillatoryFeature(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.s = settings
         self.sfreq = sfreq
         self.ch_names = ch_names
-        self.KF_dict = {}
+        self.KF_dict: dict = {}
 
         self.f_ranges_dict = settings["frequency_ranges_hz"]
         self.fband_names = list(settings["frequency_ranges_hz"].keys())
@@ -23,7 +23,7 @@ def __init__(
     def test_settings_osc(
         s: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
         osc_feature_name: str,
     ):
         assert (
@@ -48,9 +48,7 @@ def test_settings_osc(
                 f"s['segment_length_features_ms'] = {s['segment_length_features_ms']}",
             )
         else:
-            for seg_length in s[osc_feature_name][
-                "segment_lengths_ms"
-            ].values():
+            for seg_length in s[osc_feature_name]["segment_lengths_ms"].values():
                 assert isinstance(
                     seg_length, int
                 ), f"segment length has to be type int, got {seg_length}"
@@ -60,20 +58,14 @@ def test_settings_osc(
 
         assert isinstance(s["frequency_ranges_hz"], dict)
 
-        assert (
-            isinstance(value, list)
-            for value in s["frequency_ranges_hz"].values()
-        )
+        assert (isinstance(value, list) for value in s["frequency_ranges_hz"].values())
         assert (len(value) == 2 for value in s["frequency_ranges_hz"].values())
 
         assert (
-            isinstance(value[0], list)
-            for value in s["frequency_ranges_hz"].values()
+            isinstance(value[0], list) for value in s["frequency_ranges_hz"].values()
         )
 
-        assert (
-            len(value[0]) == 2 for value in s["frequency_ranges_hz"].values()
-        )
+        assert (len(value[0]) == 2 for value in s["frequency_ranges_hz"].values())
 
         assert (
             isinstance(value[1], (float, int))
@@ -83,9 +75,7 @@ def test_settings_osc(
     def init_KF(self, feature: str) -> None:
         for f_band in self.s["kalman_filter_settings"]["frequency_bands"]:
             for channel in self.ch_names:
-                self.KF_dict[
-                    "_".join([channel, feature, f_band])
-                ] = nm_kalmanfilter.define_KF(
+                self.KF_dict["_".join([channel, feature, f_band])] = define_KF(
                     self.s["kalman_filter_settings"]["Tp"],
                     self.s["kalman_filter_settings"]["sigma_w"],
                     self.s["kalman_filter_settings"]["sigma_v"],
@@ -102,29 +92,29 @@ def estimate_osc_features(
         self,
         features_compute: dict,
         data: np.ndarray,
-        feature_name: np.ndarray,
+        feature_name: str,
         est_name: str,
     ):
         for feature_est_name in list(self.s[est_name]["features"].keys()):
-            if self.s[est_name]["features"][feature_est_name] is True:
+            if self.s[est_name]["features"][feature_est_name]:
                 # switch case for feature_est_name
                 match feature_est_name:
                     case "mean":
-                        features_compute[
-                            f"{feature_name}_{feature_est_name}"
-                        ] = np.nanmean(data)
+                        features_compute[f"{feature_name}_{feature_est_name}"] = (
+                            np.nanmean(data)
+                        )
                     case "median":
-                        features_compute[
-                            f"{feature_name}_{feature_est_name}"
-                        ] = np.nanmedian(data)
+                        features_compute[f"{feature_name}_{feature_est_name}"] = (
+                            np.nanmedian(data)
+                        )
                     case "std":
-                        features_compute[
-                            f"{feature_name}_{feature_est_name}"
-                        ] = np.nanstd(data)
+                        features_compute[f"{feature_name}_{feature_est_name}"] = (
+                            np.nanstd(data)
+                        )
                     case "max":
-                        features_compute[
-                            f"{feature_name}_{feature_est_name}"
-                        ] = np.nanmax(data)
+                        features_compute[f"{feature_name}_{feature_est_name}"] = (
+                            np.nanmax(data)
+                        )
 
         return features_compute
 
@@ -145,9 +135,7 @@ def __init__(
 
         window_ms = self.s["fft_settings"]["windowlength_ms"]
         self.window_samples = int(-np.floor(window_ms / 1000 * sfreq))
-        self.freqs = fft.rfftfreq(
-            -self.window_samples, 1 / np.floor(self.sfreq)
-        )
+        self.freqs = rfftfreq(-self.window_samples, 1 / np.floor(self.sfreq))
 
         self.feature_params = []
         for ch_idx, ch_name in enumerate(self.ch_names):
@@ -159,12 +147,12 @@ def __init__(
                 self.feature_params.append((ch_idx, feature_name, idx_range))
 
     @staticmethod
-    def test_settings(s: dict, ch_names: Iterable[str], sfreq: int | float):
+    def test_settings(s: dict, ch_names: Iterable[str], sfreq: float):
         OscillatoryFeature.test_settings_osc(s, ch_names, sfreq, "fft_settings")
 
     def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         data = data[:, self.window_samples :]
-        Z = np.abs(fft.rfft(data))
+        Z = np.abs(rfft(data))
 
         if self.log_transform:
             Z = np.log10(Z)
@@ -206,13 +194,11 @@ def __init__(
                 self.feature_params.append((ch_idx, feature_name, f_range))
 
     @staticmethod
-    def test_settings(s: dict, ch_names: Iterable[str], sfreq: int | float):
-        OscillatoryFeature.test_settings_osc(
-            s, ch_names, sfreq, "welch_settings"
-        )
+    def test_settings(s: dict, ch_names: Iterable[str], sfreq: float):
+        OscillatoryFeature.test_settings_osc(s, ch_names, sfreq, "welch_settings")
 
     def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
-        freqs, Z = signal.welch(
+        freqs, Z = welch(
             data,
             fs=self.sfreq,
             window="hann",
@@ -226,9 +212,7 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         for ch_idx, feature_name, f_range in self.feature_params:
             Z_ch = Z[ch_idx]
 
-            idx_range = np.where((freqs >= f_range[0]) & (freqs <= f_range[1]))[
-                0
-            ]
+            idx_range = np.where((freqs >= f_range[0]) & (freqs <= f_range[1]))[0]
 
             features_compute = self.estimate_osc_features(
                 features_compute,
@@ -268,13 +252,11 @@ def __init__(
                 self.feature_params.append((ch_idx, feature_name, f_range))
 
     @staticmethod
-    def test_settings(s: dict, ch_names: Iterable[str], sfreq: int | float):
-        OscillatoryFeature.test_settings_osc(
-            s, ch_names, sfreq, "stft_settings"
-        )
+    def test_settings(s: dict, ch_names: Iterable[str], sfreq: float):
+        OscillatoryFeature.test_settings_osc(s, ch_names, sfreq, "stft_settings")
 
     def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
-        freqs, _, Zxx = signal.stft(
+        freqs, _, Zxx = stft(
             data,
             fs=self.sfreq,
             window="hamming",
@@ -286,9 +268,7 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
             Z = np.log10(Z)
         for ch_idx, feature_name, f_range in self.feature_params:
             Z_ch = Z[ch_idx]
-            idx_range = np.where((freqs >= f_range[0]) & (freqs <= f_range[1]))[
-                0
-            ]
+            idx_range = np.where((freqs >= f_range[0]) & (freqs <= f_range[1]))[0]
 
             features_compute = self.estimate_osc_features(
                 features_compute,
@@ -316,12 +296,12 @@ def __init__(
         settings: dict,
         ch_names: Iterable[str],
         sfreq: float,
-        use_kf: bool = None,
+        use_kf: bool | None = None,
     ) -> None:
         super().__init__(settings, ch_names, sfreq)
         bp_settings = self.s["bandpass_filter_settings"]
 
-        self.bandpass_filter = nm_filter.MNEFilter(
+        self.bandpass_filter = MNEFilter(
             f_ranges=list(self.f_ranges_dict.values()),
             sfreq=self.sfreq,
             filter_length=self.sfreq - 1,
@@ -330,9 +310,7 @@ def __init__(
 
         self.log_transform = bp_settings["log_transform"]
 
-        if use_kf is True or (
-            use_kf is None and bp_settings["kalman_filter"] is True
-        ):
+        if use_kf is True or (use_kf is None and bp_settings["kalman_filter"] is True):
             self.init_KF("bandpass_activity")
 
         bp_features = ["activity", "mobility", "complexity"]
@@ -344,7 +322,7 @@ def __init__(
                 seglength_ms = seglengths[f_band]
                 seglen = int(np.floor(self.sfreq / 1000 * seglength_ms))
                 for bp_feature, v in bp_settings["bandpower_features"].items():
-                    if v is True:
+                    if v:
                         if bp_feature not in bp_features:
                             raise ValueError()
                         feature_name = "_".join(
@@ -363,23 +341,19 @@ def __init__(
                         )
 
     @staticmethod
-    def test_settings(s: dict, ch_names: Iterable[str], sfreq: int | float):
+    def test_settings(s: dict, ch_names: Iterable[str], sfreq: float):
         OscillatoryFeature.test_settings_osc(
             s, ch_names, sfreq, "bandpass_filter_settings"
         )
 
         assert (
             isinstance(value, bool)
-            for value in s["bandpass_filter_settings"][
-                "bandpower_features"
-            ].values()
+            for value in s["bandpass_filter_settings"]["bandpower_features"].values()
         )
 
         assert any(
             value is True
-            for value in s["bandpass_filter_settings"][
-                "bandpower_features"
-            ].values()
+            for value in s["bandpass_filter_settings"]["bandpower_features"].values()
         ), "Set at least one bandpower_feature to True."
 
         for fband_name, seg_length_fband in s["bandpass_filter_settings"][
@@ -417,15 +391,11 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
         ) in self.feature_params:
             if bp_feature == "activity":
                 if self.log_transform:
-                    feature_calc = np.log10(
-                        np.var(data[ch_idx, f_band_idx, -seglen:])
-                    )
+                    feature_calc = np.log10(np.var(data[ch_idx, f_band_idx, -seglen:]))
                 else:
                     feature_calc = np.var(data[ch_idx, f_band_idx, -seglen:])
             elif bp_feature == "mobility":
-                deriv_variance = np.var(
-                    np.diff(data[ch_idx, f_band_idx, -seglen:])
-                )
+                deriv_variance = np.var(np.diff(data[ch_idx, f_band_idx, -seglen:]))
                 feature_calc = np.sqrt(
                     deriv_variance / np.var(data[ch_idx, f_band_idx, -seglen:])
                 )
diff --git a/py_neuromodulation/nm_plots.py b/py_neuromodulation/nm_plots.py
index b0f5d61d..9e6582f3 100644
--- a/py_neuromodulation/nm_plots.py
+++ b/py_neuromodulation/nm_plots.py
@@ -1,16 +1,15 @@
-from scipy import stats
-import os
 import numpy as np
+import pandas as pd
+from scipy.stats import zscore as scipy_zscore
 from matplotlib import pyplot as plt
 from matplotlib import gridspec
-from typing import Optional
 import seaborn as sb
-import pandas as pd
-import logging
+from pathlib import PurePath
 
-logger = logging.getLogger("PynmLogger")
-
-from py_neuromodulation import nm_IO, nm_stats
+from py_neuromodulation.nm_IO import read_plot_modules
+from py_neuromodulation.nm_stats import permutationTestSpearmansRho
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import logger
 
 
 def plot_df_subjects(
@@ -19,8 +18,8 @@ def plot_df_subjects(
     y_col="performance_test",
     hue=None,
     title="channel specific performances",
-    PATH_SAVE: str = None,
-    figsize_tuple: tuple = (5, 3),
+    PATH_SAVE: _PathLike = "",
+    figsize_tuple: tuple[float, float] = (5, 3),
 ):
     alpha_box = 0.4
     plt.figure(figsize=figsize_tuple, dpi=300)
@@ -38,9 +37,7 @@ def plot_df_subjects(
         notch=False,
         whiskerprops={"linewidth": 2, "zorder": 10, "alpha": alpha_box},
         capprops={"alpha": alpha_box},
-        medianprops=dict(
-            linestyle="-", linewidth=5, color="gray", alpha=alpha_box
-        ),
+        medianprops=dict(linestyle="-", linewidth=5, color="gray", alpha=alpha_box),
     )
 
     ax = sb.stripplot(
@@ -57,7 +54,7 @@ def plot_df_subjects(
         n_hues = df[hue].nunique()
 
         handles, labels = ax.get_legend_handles_labels()
-        l = plt.legend(
+        plt.legend(
             handles[0:n_hues],
             labels[0:n_hues],
             bbox_to_anchor=(1.05, 1),
@@ -68,7 +65,7 @@ def plot_df_subjects(
     plt.title(title)
     plt.ylabel(y_col)
     plt.xticks(rotation=90)
-    if PATH_SAVE is not None:
+    if PATH_SAVE:
         plt.savefig(
             PATH_SAVE,
             bbox_inches="tight",
@@ -78,29 +75,27 @@ def plot_df_subjects(
 
 
 def plot_epoch(
-    X_epoch: np.array,
-    y_epoch: np.array,
+    X_epoch: np.ndarray,
+    y_epoch: np.ndarray,
     feature_names: list,
-    z_score: bool = None,
+    z_score: bool | None = None,
     epoch_len: int = 4,
     sfreq: int = 10,
-    str_title: str = None,
-    str_label: str = None,
-    ytick_labelsize: float = None,
+    str_title: str = "",
+    str_label: str = "",
+    ytick_labelsize: float | None = None,
 ):
     if z_score is None:
-        X_epoch = stats.zscore(
+        X_epoch = scipy_zscore(
             np.nan_to_num(np.nanmean(np.squeeze(X_epoch), axis=0)),
             axis=0,
             nan_policy="omit",
         ).T
-    y_epoch = np.stack(np.array(y_epoch))
+    y_epoch = np.stack([np.array(y_epoch)])
     plt.figure(figsize=(6, 6))
     plt.subplot(211)
     plt.imshow(X_epoch, aspect="auto")
-    plt.yticks(
-        np.arange(0, len(feature_names), 1), feature_names, size=ytick_labelsize
-    )
+    plt.yticks(np.arange(0, len(feature_names), 1), feature_names, size=ytick_labelsize)
     plt.xticks(
         np.arange(0, X_epoch.shape[1], 1),
         np.round(np.arange(-epoch_len / 2, epoch_len / 2, 1 / sfreq), 2),
@@ -133,10 +128,10 @@ def plot_epoch(
 
 
 def reg_plot(
-    x_col: str, y_col: str, data: pd.DataFrame, out_path_save: str = None
+    x_col: str, y_col: str, data: pd.DataFrame, out_path_save: str | None = None
 ):
     plt.figure(figsize=(4, 4), dpi=300)
-    rho, p = nm_stats.permutationTestSpearmansRho(
+    rho, p = permutationTestSpearmansRho(
         data[x_col],
         data[y_col],
         False,
@@ -156,8 +151,8 @@ def reg_plot(
 def plot_bar_performance_per_channel(
     ch_names,
     performances: dict,
-    PATH_OUT: str,
-    sub: str = None,
+    PATH_OUT: _PathLike,
+    sub: str | None = None,
     save_str: str = "ch_comp_bar_plt.png",
     performance_metric: str = "Balanced Accuracy",
 ):
@@ -175,7 +170,7 @@ def plot_bar_performance_per_channel(
     plt.xlabel("channels")
     plt.ylabel(performance_metric)
     plt.savefig(
-        os.path.join(PATH_OUT, save_str),
+        PurePath(PATH_OUT, save_str),
         bbox_inches="tight",
     )
     plt.close()
@@ -183,21 +178,21 @@ def plot_bar_performance_per_channel(
 
 def plot_corr_matrix(
     feature: pd.DataFrame,
-    feature_file: str = None,
-    ch_name: str = None,
-    feature_names: list[str] = None,
+    feature_file: str = "",
+    ch_name: str = "",
+    feature_names: list[str] = [],
     show_plot=True,
-    OUT_PATH: str = None,
+    OUT_PATH: _PathLike = "",
     feature_name_plt="Features_corr_matr",
     save_plot: bool = False,
-    save_plot_name: str = None,
-    figsize: tuple[int] = (7, 7),
-    title: str = None,
+    save_plot_name: str = "",
+    figsize: tuple[float, float] = (7, 7),
+    title: str = "",
     cbar_vmin: float = -1,
     cbar_vmax: float = 1.0,
 ):
     # cut out channel name for each column
-    if ch_name is not None:
+    if not ch_name:
         feature_col_name = [
             i[len(ch_name) + 1 :] for i in feature_names if ch_name in i
         ]
@@ -205,7 +200,9 @@ def plot_corr_matrix(
         feature_col_name = feature.columns
 
     plt.figure(figsize=figsize)
-    if feature_names is not None:
+    if (
+        len(feature_names) > 0
+    ):  # Checking length to accomodate for tests passing a pandas Index
         corr = feature[feature_names].corr()
     else:
         corr = feature.corr()
@@ -217,8 +214,8 @@ def plot_corr_matrix(
         vmax=cbar_vmax,
         cmap="viridis",
     )
-    if title is None:
-        if ch_name is not None:
+    if not title:
+        if ch_name:
             plt.title("Correlation matrix features channel: " + str(ch_name))
         else:
             plt.title("Correlation matrix")
@@ -229,22 +226,23 @@ def plot_corr_matrix(
     #    plt.xticks([])
     #    plt.yticks([])
 
-    if save_plot and save_plot_name is None:
-        plt_path = get_plt_path(
-            OUT_PATH=OUT_PATH,
-            feature_file=feature_file,
-            ch_name=ch_name,
-            str_plt_type=feature_name_plt,
-            # feature_name=feature_names.__str__,  # This here raises an error in os.path.join in line 251
+    if save_plot:
+        plt_path = (
+            PurePath(OUT_PATH, save_plot_name)
+            if save_plot_name
+            else get_plt_path(
+                OUT_PATH=OUT_PATH,
+                feature_file=feature_file,
+                ch_name=ch_name,
+                str_plt_type=feature_name_plt,
+                feature_name="_".join(feature_names),
+            )
         )
-    if save_plot and save_plot_name is not None:
-        plt_path = os.path.join(OUT_PATH, save_plot_name)
 
-    if save_plot:
         plt.savefig(plt_path, bbox_inches="tight")
         logger.info(f"Correlation matrix figure saved to {plt_path}")
 
-    if show_plot is False:
+    if not show_plot:
         plt.close()
 
     plt.tight_layout()
@@ -257,12 +255,12 @@ def plot_feature_series_time(features) -> None:
 
 
 def get_plt_path(
-    OUT_PATH: str | None = None,
-    feature_file: str | None = None,
-    ch_name: str | None = None,
-    str_plt_type: str | None = None,
-    feature_name: str | None = None,
-) -> None:
+    OUT_PATH: _PathLike = "",
+    feature_file: str = "",
+    ch_name: str = "",
+    str_plt_type: str = "",
+    feature_name: str = "",
+) -> _PathLike:
     """[summary]
 
     Parameters
@@ -278,29 +276,14 @@ def get_plt_path(
     feature_name : str, optional
         e.g. bandpower, stft, sharpwave_prominence, by default None
     """
-    if None not in (ch_name, OUT_PATH, feature_file):
-        if feature_name is None:
-            plt_path = os.path.join(
-                OUT_PATH,
-                feature_file,
-                str_plt_type + "_ch_" + ch_name + ".png",
-            )
-        else:
-            plt_path = os.path.join(
-                OUT_PATH,
-                feature_file,
-                str_plt_type + "_ch_" + ch_name + "_" + feature_name + ".png",
-            )
-    elif None not in (OUT_PATH, feature_file) and ch_name is None:
-        plt_path = os.path.join(
-            OUT_PATH,
-            feature_file,
-            str_plt_type + "_ch_" + feature_name + ".png",
-        )
+    filename = (
+        str_plt_type
+        + (("_ch_" + ch_name) if ch_name else "")
+        + (("_" + feature_name) if feature_name else "")
+        + ".png"
+    )
 
-    else:
-        plt_path = os.getcwd() + ".png"
-    return plt_path
+    return PurePath(OUT_PATH, feature_file, filename)
 
 
 def plot_epochs_avg(
@@ -308,32 +291,30 @@ def plot_epochs_avg(
     y_epoch: np.ndarray,
     epoch_len: int,
     sfreq: int,
-    feature_names: list[str] = None,
-    feature_str_add: str = None,
+    feature_names: list[str] = [],
+    feature_str_add: str = "",
     cut_ch_name_cols: bool = True,
-    ch_name: str = None,
-    label_name: str = None,
+    ch_name: str = "",
+    label_name: str = "",
     normalize_data: bool = True,
     show_plot: bool = True,
     save: bool = False,
-    OUT_PATH: str = None,
-    feature_file: str = None,
+    OUT_PATH: _PathLike = "",
+    feature_file: str = "",
     str_title: str = "Movement aligned features",
     ytick_labelsize=None,
     figsize_x: float = 8,
     figsize_y: float = 8,
 ) -> None:
     # cut channel name of for axis + "_" for more dense plot
-    if feature_names is None:
+    if not feature_names:
         if cut_ch_name_cols and None not in (ch_name, feature_names):
             feature_names = [
-                i[len(ch_name) + 1 :]
-                for i in list(feature_names)
-                if ch_name in i
+                i[len(ch_name) + 1 :] for i in list(feature_names) if ch_name in i
             ]
 
     if normalize_data:
-        X_epoch_mean = stats.zscore(
+        X_epoch_mean = scipy_zscore(
             np.nanmean(np.squeeze(X_epoch), axis=0), axis=0, nan_policy="omit"
         ).T
     else:
@@ -346,9 +327,7 @@ def plot_epochs_avg(
     gs = gridspec.GridSpec(2, 1, height_ratios=[2.5, 1])
     plt.subplot(gs[0])
     plt.imshow(X_epoch_mean, aspect="auto")
-    plt.yticks(
-        np.arange(0, len(feature_names), 1), feature_names, size=ytick_labelsize
-    )
+    plt.yticks(np.arange(0, len(feature_names), 1), feature_names, size=ytick_labelsize)
     plt.xticks(
         np.arange(0, X_epoch.shape[1], int(X_epoch.shape[1] / 10)),
         np.round(np.arange(-epoch_len / 2, epoch_len / 2, epoch_len / 10), 2),
@@ -391,7 +370,7 @@ def plot_epochs_avg(
         )
         plt.savefig(plt_path, bbox_inches="tight")
         logger.info(f"Feature epoch average figure saved to: {str(plt_path)}")
-    if show_plot is False:
+    if not show_plot:
         plt.close()
 
 
@@ -404,9 +383,7 @@ def plot_grid_elec_3d(
     ax = plt.axes(projection="3d")
 
     if cortex_grid is not None:
-        grid_color = (
-            np.ones(cortex_grid.shape[0]) if grid_color is None else grid_color
-        )
+        grid_color = np.ones(cortex_grid.shape[0]) if grid_color is None else grid_color
         _ = ax.scatter3D(
             cortex_grid[:, 0],
             cortex_grid[:, 1],
@@ -426,7 +403,7 @@ def plot_grid_elec_3d(
             ecog_strip[:, 1],
             ecog_strip[:, 2],
             c=strip_color,
-            s=500,
+            s=500,  # Bug? Third argument is s, what is this value?
             alpha=0.8,
             cmap="gray",
             marker="o",
@@ -435,16 +412,16 @@ def plot_grid_elec_3d(
 
 def plot_all_features(
     df: pd.DataFrame,
-    time_limit_low_s: float = None,
-    time_limit_high_s: float = None,
+    time_limit_low_s: float | None = None,
+    time_limit_high_s: float | None = None,
     normalize: bool = True,
     ytick_labelsize: int = 4,
-    clim_low: float = None,
-    clim_high: float = None,
+    clim_low: float | None = None,
+    clim_high: float | None = None,
     save: bool = False,
     title="all_feature_plt.pdf",
-    OUT_PATH: str = None,
-    feature_file: str = None,
+    OUT_PATH: _PathLike = "",
+    feature_file: str = "",
 ):
     if time_limit_high_s is not None:
         df = df[df["time"] < time_limit_high_s * 1000]
@@ -452,8 +429,8 @@ def plot_all_features(
         df = df[df["time"] > time_limit_low_s * 1000]
 
     cols_plt = [c for c in df.columns if c != "time"]
-    if normalize is True:
-        data_plt = stats.zscore(df[cols_plt], nan_policy="omit")
+    if normalize:
+        data_plt = scipy_zscore(df[cols_plt], nan_policy="omit")
     else:
         data_plt = df[cols_plt]
 
@@ -477,8 +454,8 @@ def plot_all_features(
     plt.colorbar()
     plt.tight_layout()
 
-    if save is True:
-        plt_path = os.path.join(OUT_PATH, feature_file, title)
+    if save:
+        plt_path = PurePath(OUT_PATH, feature_file, title)
         plt.savefig(plt_path, bbox_inches="tight")
 
 
@@ -488,7 +465,7 @@ def __init__(
         ecog_strip: np.ndarray | None = None,
         grid_cortex: np.ndarray | None = None,
         grid_subcortex: np.ndarray | None = None,
-        sess_right: Optional[bool] = False,
+        sess_right: bool | None = False,
         proj_matrix_cortex: np.ndarray | None = None,
     ) -> None:
         self.grid_cortex = grid_cortex
@@ -510,22 +487,22 @@ def __init__(
             self.x_stn,
             self.y_stn,
             self.z_stn,
-        ) = nm_IO.read_plot_modules()
+        ) = read_plot_modules()
 
     def plot_grid_elec_3d(self) -> None:
         plot_grid_elec_3d(np.array(self.grid_cortex), np.array(self.ecog_strip))
 
     def plot_cortex(
         self,
-        grid_cortex: Optional[np.ndarray] = None,
-        grid_color: Optional[np.ndarray] = None,
-        ecog_strip: Optional[np.ndarray] = None,
-        strip_color: Optional[np.ndarray] = None,
-        sess_right: Optional[bool] = None,
+        grid_cortex: np.ndarray | pd.DataFrame | None = None,
+        grid_color: np.ndarray | None = None,
+        ecog_strip: np.ndarray | None = None,
+        strip_color: np.ndarray | None = None,
+        sess_right: bool | None = None,
         save: bool = False,
-        OUT_PATH: str = None,
-        feature_file: str = None,
-        feature_str_add: str = None,
+        OUT_PATH: _PathLike = "",
+        feature_file: str = "",
+        feature_str_add: str = "",
         show_plot: bool = True,
         title: str = "Cortical grid",
         set_clim: bool = True,
@@ -546,8 +523,8 @@ def plot_cortex(
         if ecog_strip is None:
             ecog_strip = self.ecog_strip
 
-        if sess_right is True:
-            grid_cortex[0, :] = grid_cortex[0, :] * -1
+        if sess_right:
+            grid_cortex[0, :] = grid_cortex[0, :] * -1  # type: ignore # Handled above
 
         fig, axes = plt.subplots(1, 1, facecolor=(1, 1, 1), figsize=(14, 9))
         axes.scatter(self.x_ecog, self.y_ecog, c="gray", s=0.01)
@@ -555,9 +532,7 @@ def plot_cortex(
 
         if grid_cortex is not None:
             grid_color = (
-                np.ones(grid_cortex.shape[0])
-                if grid_color is None
-                else grid_color
+                np.ones(grid_cortex.shape[0]) if grid_color is None else grid_color
             )
 
             pos_ecog = axes.scatter(
@@ -573,9 +548,7 @@ def plot_cortex(
                 pos_ecog.set_clim(lower_clim, upper_clim)
         if ecog_strip is not None:
             strip_color = (
-                np.ones(ecog_strip.shape[0])
-                if strip_color is None
-                else strip_color
+                np.ones(ecog_strip.shape[0]) if strip_color is None else strip_color
             )
 
             pos_ecog = axes.scatter(
@@ -600,13 +573,10 @@ def plot_cortex(
             plt_path = get_plt_path(
                 OUT_PATH,
                 feature_file,
-                ch_name=None,
                 str_plt_type="PLOT_CORTEX",
                 feature_name=feature_str_add,
             )
             plt.savefig(plt_path, bbox_inches="tight")
-            logger.info(
-                f"Feature epoch average figure saved to: {str(plt_path)}"
-            )
-        if show_plot is False:
+            logger.info(f"Feature epoch average figure saved to: {str(plt_path)}")
+        if not show_plot:
             plt.close()
diff --git a/py_neuromodulation/nm_projection.py b/py_neuromodulation/nm_projection.py
index 236ec3db..90ff6d7d 100644
--- a/py_neuromodulation/nm_projection.py
+++ b/py_neuromodulation/nm_projection.py
@@ -1,9 +1,7 @@
-from typing import Optional, Union
-
 import numpy as np
 import pandas as pd
 
-from py_neuromodulation import nm_plots
+from py_neuromodulation.nm_plots import NM_Plot
 
 
 class Projection:
@@ -16,7 +14,6 @@ def __init__(
         nm_channels: pd.DataFrame,
         plot_projection: bool = False,
     ) -> None:
-
         self.test_settings(settings)
 
         self.grid_cortex = grid_cortex
@@ -24,25 +21,17 @@ def __init__(
         self.coords = coords
         self.nm_channels = nm_channels
         self.project_cortex = settings["postprocessing"]["project_cortex"]
-        self.project_subcortex = settings["postprocessing"][
-            "project_subcortex"
-        ]
-        self.max_dist_cortex = settings["project_cortex_settings"][
-            "max_dist_mm"
-        ]
-        self.max_dist_subcortex = settings["project_subcortex_settings"][
-            "max_dist_mm"
-        ]
-        self.ecog_channels: Optional[list] = None
-        self.lfp_channels: Optional[list] = None
+        self.project_subcortex = settings["postprocessing"]["project_subcortex"]
+        self.max_dist_cortex = settings["project_cortex_settings"]["max_dist_mm"]
+        self.max_dist_subcortex = settings["project_subcortex_settings"]["max_dist_mm"]
+        self.ecog_channels: list  # None case never handled, no need for default value
+        self.lfp_channels: list  # None case never handled, no need for default value
 
-        self.idx_chs_ecog: list = (
-            []
-        )  # feature series indexes for ecog channels
+        self.idx_chs_ecog: list = []  # feature series indexes for ecog channels
         self.names_chs_ecog: list = []  # feature series name of ecog features
         self.idx_chs_lfp: list = []  # feature series indexes for lfp channels
         self.names_chs_lfp: list = []  # feature series name of lfp features
-        self.feature_names: Optional[list] = None
+        self.feature_names: list | None = None
         self.initialized: bool = False
 
         self.remove_not_used_ch_from_coords()  # remove beforehand non used channels from coords
@@ -85,8 +74,8 @@ def __init__(
                 self.proj_matrix_subcortex.sum(axis=1)
             )[0]
 
-        if plot_projection is True:
-            nmplotter = nm_plots.NM_Plot(
+        if plot_projection:
+            nmplotter = NM_Plot(
                 ecog_strip=self.ecog_strip,
                 grid_cortex=self.grid_cortex.to_numpy(),
                 grid_subcortex=self.grid_subcortex.to_numpy(),
@@ -97,25 +86,19 @@ def __init__(
 
     @staticmethod
     def test_settings(s: dict):
-        if s["postprocessing"]["project_cortex"] is True:
-            assert isinstance(
-                s["project_cortex_settings"]["max_dist_mm"], (float, int)
-            )
-        if s["postprocessing"]["project_subcortex"] is True:
+        if s["postprocessing"]["project_cortex"]:
+            assert isinstance(s["project_cortex_settings"]["max_dist_mm"], (float, int))
+        if s["postprocessing"]["project_subcortex"]:
             assert isinstance(
                 s["project_subcortex_settings"]["max_dist_mm"], (float, int)
             )
 
     def remove_not_used_ch_from_coords(self):
-        ch_not_used = self.nm_channels.query(
-            '(used==0) or (status=="bad")'
-        ).name
+        ch_not_used = self.nm_channels.query('(used==0) or (status=="bad")').name
         if len(ch_not_used) > 0:
             for ch in ch_not_used:
                 for key_ in self.coords:
-                    for idx, ch_coords in enumerate(
-                        self.coords[key_]["ch_names"]
-                    ):
+                    for idx, ch_coords in enumerate(self.coords[key_]["ch_names"]):
                         if ch.startswith(ch_coords):
                             # delete index
                             self.coords[key_]["positions"] = np.delete(
@@ -125,7 +108,7 @@ def remove_not_used_ch_from_coords(self):
 
     def calc_proj_matrix(
         self,
-        max_dist: Union[int, float],
+        max_dist: float,
         grid: np.ndarray,
         coord_array: np.ndarray,
     ) -> np.ndarray:
@@ -142,12 +125,10 @@ def calc_proj_matrix(
 
         proj_matrix = np.zeros(distance_matrix.shape)
         for grid_point in range(distance_matrix.shape[0]):
-            used_channels = np.where(
-                distance_matrix[grid_point, :] < max_dist
-            )[0]
+            used_channels = np.where(distance_matrix[grid_point, :] < max_dist)[0]
 
             rec_distances = distance_matrix[grid_point, used_channels]
-            sum_distances = np.sum(1 / rec_distances)
+            sum_distances: float = np.sum(1 / rec_distances)
 
             for _, used_channel in enumerate(used_channels):
                 proj_matrix[grid_point, used_channel] = (
@@ -164,12 +145,11 @@ def calc_projection_matrix(self):
             cortical projection_matrix in shape [grid contacts, channel contact] defaults to None
         proj_matrix_subcortex (np.array)
             subcortical projection_matrix in shape [grid contacts, channel contact] defaults to None
-        """        
+        """
 
         proj_matrix_run = np.empty(2, dtype=object)
 
-        if self.sess_right is True:
-
+        if self.sess_right:
             if self.project_cortex:
                 cortex_grid_right = np.copy(self.grid_cortex)
                 cortex_grid_right[:, 0] = cortex_grid_right[:, 0] * -1
@@ -309,10 +289,7 @@ def project_features(self, feature_series: pd.Series) -> pd.Series:
 
         dat_cortex = (
             np.vstack(
-                [
-                    feature_series.iloc[idx_ch].values
-                    for idx_ch in self.idx_chs_ecog
-                ]
+                [feature_series.iloc[idx_ch].values for idx_ch in self.idx_chs_ecog]
             )
             if self.project_cortex
             else None
@@ -320,31 +297,33 @@ def project_features(self, feature_series: pd.Series) -> pd.Series:
 
         dat_subcortex = (
             np.vstack(
-                [
-                    feature_series.iloc[idx_ch].values
-                    for idx_ch in self.idx_chs_lfp
-                ]
+                [feature_series.iloc[idx_ch].values for idx_ch in self.idx_chs_lfp]
             )
             if self.project_subcortex
             else None
         )
 
         # project data
+        proj_cortex_array: (
+            np.ndarray
+        )  # get_projected_cortex_subcortex_data can return None
+        proj_subcortex_array: (
+            np.ndarray
+        )  # but None is not handled and will throw error in the code below
         (
             proj_cortex_array,
             proj_subcortex_array,
-        ) = self.get_projected_cortex_subcortex_data(dat_cortex, dat_subcortex)
+        ) = self.get_projected_cortex_subcortex_data(dat_cortex, dat_subcortex)  # type: ignore # Ingore None return
 
-        features_new = {}
+        features_new: dict = {}
         # proj_cortex_array has shape grid_points x feature_number
         if self.project_cortex:
-
             features_new = features_new | {
                 "gridcortex_"
                 + str(act_grid_point)
                 + "_"
                 + feature_name: proj_cortex_array[act_grid_point, feature_idx]
-                for feature_idx, feature_name in enumerate(self.feature_names)
+                for feature_idx, feature_name in enumerate(self.feature_names)  # type: ignore # Empty list handled above
                 for act_grid_point in self.active_cortex_gridpoints
             }
         if self.project_subcortex:
@@ -352,10 +331,8 @@ def project_features(self, feature_series: pd.Series) -> pd.Series:
                 "gridsubcortex_"
                 + str(act_grid_point)
                 + "_"
-                + feature_name: proj_subcortex_array[
-                    act_grid_point, feature_idx
-                ]
-                for feature_idx, feature_name in enumerate(self.feature_names)
+                + feature_name: proj_subcortex_array[act_grid_point, feature_idx]
+                for feature_idx, feature_name in enumerate(self.feature_names)  # type: ignore # Empty list handled above
                 for act_grid_point in self.active_subcortex_gridpoints
             }
         feature_series = pd.concat([feature_series, pd.Series(features_new)])
@@ -364,9 +341,9 @@ def project_features(self, feature_series: pd.Series) -> pd.Series:
 
     def get_projected_cortex_subcortex_data(
         self,
-        dat_cortex: Optional[np.ndarray] = None,
-        dat_subcortex: Optional[np.ndarray] = None,
-    ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
+        dat_cortex: np.ndarray | None = None,
+        dat_subcortex: np.ndarray | None = None,
+    ) -> tuple[np.ndarray | None, np.ndarray | None]:
         """Project cortical and subcortical data to predefined projection matrices
 
         Parameters
diff --git a/py_neuromodulation/nm_rereference.py b/py_neuromodulation/nm_rereference.py
index 7f9279e3..c98bffc8 100644
--- a/py_neuromodulation/nm_rereference.py
+++ b/py_neuromodulation/nm_rereference.py
@@ -1,21 +1,20 @@
 """Re-referencing Module."""
+
 import numpy as np
 import pandas as pd
 
 
 class ReReferencer:
-    ref_matrix: np.ndarray
-
     def __init__(
         self,
-        sfreq: int | float,
+        sfreq: float,
         nm_channels: pd.DataFrame,
     ) -> None:
         """Initialize real-time rereference information.
 
         Parameters
         ----------
-        sfreq : int | float
+        sfreq : float
             Sampling frequency. Is not used, only kept for compatibility.
         nm_channels : Pandas DataFrame
             Dataframe containing information about rereferencing, as
@@ -26,9 +25,10 @@ def __init__(
             ValueError: rereferencing using undefined channel
             ValueError: rereferencing to same channel
         """
-        nm_channels = nm_channels[nm_channels["used"] == 1].reset_index(
-            drop=True
-        )
+
+        self.ref_matrix: np.ndarray | None
+
+        nm_channels = nm_channels[nm_channels["used"] == 1].reset_index(drop=True)
         # (channels_used,) = np.where((nm_channels.used == 1))
 
         ch_names = nm_channels["name"].tolist()
diff --git a/py_neuromodulation/nm_resample.py b/py_neuromodulation/nm_resample.py
index 511d8138..7516afd0 100644
--- a/py_neuromodulation/nm_resample.py
+++ b/py_neuromodulation/nm_resample.py
@@ -1,14 +1,16 @@
 """Module for resampling."""
-import mne
+
 import numpy as np
 
+from mne.filter import resample as mne_resample
+
 
 class Resampler:
     """Resample data.
 
     Parameters
     ----------
-    sfreq : int | float
+    sfreq : float
         Original sampling frequency.
 
     Attributes
@@ -19,14 +21,12 @@ class Resampler:
 
     def __init__(
         self,
-        sfreq: int | float,
-        resample_freq_hz: int | float,
+        sfreq: float,
+        resample_freq_hz: float,
     ) -> None:
+        self.test_settings(resample_freq_hz)
 
-        assert isinstance(resample_freq_hz, (float, int))
-
-        self.sfreq_new: int | float = resample_freq_hz
-        ratio = float(self.sfreq_new / sfreq)
+        ratio = float(resample_freq_hz / sfreq)
         if ratio == 1.0:
             self.up = 0.0
         else:
@@ -47,4 +47,7 @@ def process(self, data: np.ndarray) -> np.ndarray:
         """
         if not self.up:
             return data
-        return mne.filter.resample(data.astype(np.float64), up=self.up, down=1.0)
+        return mne_resample(data.astype(np.float64), up=self.up, down=1.0)
+
+    def test_settings(self, resample_freq_hz):
+        assert isinstance(resample_freq_hz, (float, int))
diff --git a/py_neuromodulation/nm_run_analysis.py b/py_neuromodulation/nm_run_analysis.py
index cc68ca38..74078955 100644
--- a/py_neuromodulation/nm_run_analysis.py
+++ b/py_neuromodulation/nm_run_analysis.py
@@ -1,62 +1,48 @@
 """This module contains the class to process a given batch of data."""
 
-from enum import Enum
-import math
-import os
 from time import time
-from typing import Protocol, Type
-import logging
-
-logger = logging.getLogger("PynmLogger")
+from typing import Protocol
 
 import numpy as np
 import pandas as pd
 
-from py_neuromodulation import (
-    nm_features,
-    nm_filter,
-    nm_IO,
-    nm_normalization,
-    nm_projection,
-    nm_rereference,
-    nm_resample,
-    nm_filter_preprocessing,
-)
+from py_neuromodulation import nm_IO, logger
+from py_neuromodulation.nm_types import _PathLike
+
+from py_neuromodulation.nm_features import Features
 
-_PathLike = str | os.PathLike
+# Perhaps have all the followings in a Preprocessor dictionary like for Features
+from py_neuromodulation.nm_filter_preprocessing import PreprocessingFilter
+from py_neuromodulation.nm_filter import NotchFilter
+from py_neuromodulation.nm_resample import Resampler
+from py_neuromodulation.nm_rereference import ReReferencer
+from py_neuromodulation.nm_normalization import RawNormalizer, FeatureNormalizer
+from py_neuromodulation.nm_projection import Projection
 
 
 class Preprocessor(Protocol):
     def process(self, data: np.ndarray) -> np.ndarray:
         pass
 
-    def test_settings(self, settings: dict): ...
-
 
 _PREPROCESSING_CONSTRUCTORS = [
+    "raw_resampling",
     "notch_filter",
     "re_referencing",
     "raw_normalization",
-    "raw_resample",
+    "preprocessing_filter",
 ]
 
 
-class GRIDS(Enum):
-    """Definition of possible projection grid types"""
-
-    CORTEX = "cortex"
-    SUBCORTEX = "subcortex"
-
-
 class DataProcessor:
     def __init__(
         self,
-        sfreq: int | float,
+        sfreq: float,
         settings: dict | _PathLike,
         nm_channels: pd.DataFrame | _PathLike,
         coord_names: list | None = None,
         coord_list: list | None = None,
-        line_noise: int | float | None = None,
+        line_noise: float | None = None,
         path_grids: _PathLike | None = None,
         verbose: bool = True,
     ) -> None:
@@ -82,12 +68,12 @@ def __init__(
         self.settings = self._load_settings(settings)
         self.nm_channels = self._load_nm_channels(nm_channels)
 
-        self.sfreq_features = self.settings["sampling_rate_features_hz"]
-        self._sfreq_raw_orig = sfreq
-        self.sfreq_raw = math.floor(sfreq)
-        self.line_noise = line_noise
-        self.path_grids = path_grids
-        self.verbose = verbose
+        self.sfreq_features: float = self.settings["sampling_rate_features_hz"]
+        self._sfreq_raw_orig: float = sfreq
+        self.sfreq_raw: float = sfreq // 1
+        self.line_noise: float | None = line_noise
+        self.path_grids: _PathLike | None = path_grids
+        self.verbose: bool = verbose
 
         self.features_previous = None
 
@@ -96,39 +82,35 @@ def __init__(
         self.preprocessors: list[Preprocessor] = []
         for preprocessing_method in self.settings["preprocessing"]:
             settings_str = f"{preprocessing_method}_settings"
+            preprocessor: Preprocessor
             match preprocessing_method:
                 case "raw_resampling":
-                    preprocessor = nm_resample.Resampler(
+                    # Preprocessors are supposed to call test_settings in the constructor
+                    preprocessor = Resampler(
                         sfreq=self.sfreq_raw, **self.settings[settings_str]
                     )
-                    self.sfreq_raw = preprocessor.sfreq_new
-                    self.preprocessors.append(preprocessor)
                 case "notch_filter":
-                    preprocessor = nm_filter.NotchFilter(
+                    preprocessor = NotchFilter(
                         sfreq=self.sfreq_raw,
                         line_noise=self.line_noise,
                         **self.settings.get(settings_str, {}),
                     )
-                    self.preprocessors.append(preprocessor)
                 case "re_referencing":
-                    preprocessor = nm_rereference.ReReferencer(
+                    preprocessor = ReReferencer(
                         sfreq=self.sfreq_raw,
                         nm_channels=self.nm_channels,
                     )
-                    self.preprocessors.append(preprocessor)
                 case "raw_normalization":
-                    preprocessor = nm_normalization.RawNormalizer(
+                    preprocessor = RawNormalizer(
                         sfreq=self.sfreq_raw,
                         sampling_rate_features_hz=self.sfreq_features,
                         **self.settings.get(settings_str, {}),
                     )
-                    self.preprocessors.append(preprocessor)
                 case "preprocessing_filter":
-                    preprocessor = nm_filter_preprocessing.PreprocessingFilter(
+                    preprocessor = PreprocessingFilter(
                         settings=self.settings,
                         sfreq=self.sfreq_raw,
                     )
-                    self.preprocessors.append(preprocessor)
                 case _:
                     raise ValueError(
                         "Invalid preprocessing method. Must be one of"
@@ -136,14 +118,16 @@ def __init__(
                         f" {preprocessing_method}"
                     )
 
+            self.preprocessors.append(preprocessor)
+
         if self.settings["postprocessing"]["feature_normalization"]:
             settings_str = "feature_normalization_settings"
-            self.feature_normalizer = nm_normalization.FeatureNormalizer(
+            self.feature_normalizer = FeatureNormalizer(
                 sampling_rate_features_hz=self.sfreq_features,
                 **self.settings.get(settings_str, {}),
             )
 
-        self.features = nm_features.Features(
+        self.features = Features(
             s=self.settings,
             ch_names=self.ch_names_used,
             sfreq=self.sfreq_raw,
@@ -174,12 +158,12 @@ def _add_coordinates(coord_names: list[str], coord_list: list) -> dict:
         dict with (sub)cortex_left and (sub)cortex_right ch_names and positions
         """
 
-        def is_left_coord(val: int | float, coord_region: str) -> bool:
+        def is_left_coord(val: float, coord_region: str) -> bool:
             if coord_region.split("_")[1] == "left":
                 return val < 0
             return val > 0
 
-        coords = {}
+        coords: dict[str, dict[str, list | np.ndarray]] = {}
 
         for coord_region in [
             coord_loc + "_" + lat
@@ -188,26 +172,22 @@ def is_left_coord(val: int | float, coord_region: str) -> bool:
         ]:
             coords[coord_region] = {}
 
-            ch_type = (
-                "ECOG" if "cortex" == coord_region.split("_")[0] else "LFP"
-            )
+            ch_type = "ECOG" if "cortex" == coord_region.split("_")[0] else "LFP"
 
             coords[coord_region]["ch_names"] = [
                 coord_name
                 for coord_name, ch in zip(coord_names, coord_list)
-                if is_left_coord(ch[0], coord_region)
-                and (ch_type in coord_name)
+                if is_left_coord(ch[0], coord_region) and (ch_type in coord_name)
             ]
 
             # multiply by 1000 to get m instead of mm
             positions = []
             for coord, coord_name in zip(coord_list, coord_names):
-                if is_left_coord(coord[0], coord_region) and (
-                    ch_type in coord_name
-                ):
+                if is_left_coord(coord[0], coord_region) and (ch_type in coord_name):
                     positions.append(coord)
-            positions = np.array(positions, dtype=np.float64) * 1000
-            coords[coord_region]["positions"] = positions
+            coords[coord_region]["positions"] = (
+                np.array(positions, dtype=np.float64) * 1000
+            )
 
         return coords
 
@@ -216,15 +196,11 @@ def _get_ch_info(
     ) -> tuple[list[str], list[str], list[int], np.ndarray]:
         """Get used feature and label info from nm_channels"""
         nm_channels = self.nm_channels
-        ch_names_used = nm_channels[nm_channels["used"] == 1][
-            "new_name"
-        ].tolist()
+        ch_names_used = nm_channels[nm_channels["used"] == 1]["new_name"].tolist()
         ch_types_used = nm_channels[nm_channels["used"] == 1]["type"].tolist()
 
         # used channels for feature estimation
-        feature_idx = np.where(nm_channels["used"] & ~nm_channels["target"])[
-            0
-        ].tolist()
+        feature_idx = np.where(nm_channels["used"] & ~nm_channels["target"])[0].tolist()
 
         # If multiple targets exist, select only the first
         label_idx = np.where(nm_channels["target"] == 1)[0]
@@ -234,8 +210,7 @@ def _get_ch_info(
     @staticmethod
     def _get_grids(
         settings: dict,
-        path_grids: str | _PathLike | None,
-        grid_type: Type[GRIDS],
+        path_grids: _PathLike | None,
     ) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
         """Read settings specified grids
 
@@ -243,7 +218,6 @@ def _get_grids(
         ----------
         settings : dict
         path_grids : str
-        grid_type : GRIDS
 
         Returns
         -------
@@ -251,21 +225,19 @@ def _get_grids(
             grid_cortex, grid_subcortex,
             might be None if not specified in settings
         """
-        if settings["postprocessing"]["project_cortex"] is True:
-            grid_cortex = nm_IO.read_grid(path_grids, grid_type.CORTEX.name)
+        if settings["postprocessing"]["project_cortex"]:
+            grid_cortex = nm_IO.read_grid(path_grids, "cortex")
         else:
             grid_cortex = None
-        if settings["postprocessing"]["project_subcortex"] is True:
-            grid_subcortex = nm_IO.read_grid(
-                path_grids, grid_type.SUBCORTEX.name
-            )
+        if settings["postprocessing"]["project_subcortex"]:
+            grid_subcortex = nm_IO.read_grid(path_grids, "subcortex")
         else:
             grid_subcortex = None
         return grid_cortex, grid_subcortex
 
     def _get_projection(
         self, settings: dict, nm_channels: pd.DataFrame
-    ) -> nm_projection.Projection | None:
+    ) -> Projection | None:
         """Return projection of used coordinated and grids"""
 
         if not any(
@@ -276,10 +248,8 @@ def _get_projection(
         ):
             return None
 
-        grid_cortex, grid_subcortex = self._get_grids(
-            self.settings, self.path_grids, GRIDS
-        )
-        projection = nm_projection.Projection(
+        grid_cortex, grid_subcortex = self._get_grids(self.settings, self.path_grids)
+        projection = Projection(
             settings=settings,
             grid_cortex=grid_cortex,
             grid_subcortex=grid_subcortex,
@@ -321,7 +291,8 @@ def _set_coords(
             )
 
         return self._add_coordinates(
-            coord_names=coord_names, coord_list=coord_list
+            coord_names=coord_names,
+            coord_list=coord_list,  # type: ignore # None case handled above
         )
 
     def process(self, data: np.ndarray) -> pd.Series:
@@ -367,22 +338,16 @@ def process(self, data: np.ndarray) -> pd.Series:
 
         # project features to grid
         if self.projection:
-            features_current = self.projection.project_features(
-                features_current
-            )
+            features_current = self.projection.project_features(features_current)
 
         # check for all features, where the channel had a NaN, that the feature is also put to NaN
         if nan_channels.sum() > 0:
             for ch in list(np.array(self.ch_names_used)[nan_channels]):
-                features_current.loc[
-                    features_current.index.str.contains(ch)
-                ] = np.nan
+                features_current.loc[features_current.index.str.contains(ch)] = np.nan
 
-        if self.verbose is True:
+        if self.verbose:
             logger.info(
-                "Last batch took: "
-                + str(np.round(time() - start_time, 2))
-                + " seconds"
+                "Last batch took: " + str(np.round(time() - start_time, 2)) + " seconds"
             )
 
         return features_current
@@ -396,7 +361,7 @@ def save_sidecar(
         """Save sidecar incuding fs, coords, sess_right to
         out_path_root and subfolder 'folder_name'.
         """
-        sidecar = {
+        sidecar: dict = {
             "original_fs": self._sfreq_raw_orig,
             "final_fs": self.sfreq_raw,
             "sfreq": self.sfreq_features,
@@ -405,14 +370,10 @@ def save_sidecar(
             sidecar["coords"] = self.projection.coords
             if self.settings["postprocessing"]["project_cortex"]:
                 sidecar["grid_cortex"] = self.projection.grid_cortex
-                sidecar["proj_matrix_cortex"] = (
-                    self.projection.proj_matrix_cortex
-                )
+                sidecar["proj_matrix_cortex"] = self.projection.proj_matrix_cortex
             if self.settings["postprocessing"]["project_subcortex"]:
                 sidecar["grid_subcortex"] = self.projection.grid_subcortex
-                sidecar["proj_matrix_subcortex"] = (
-                    self.projection.proj_matrix_subcortex
-                )
+                sidecar["proj_matrix_subcortex"] = self.projection.proj_matrix_subcortex
         if additional_args is not None:
             sidecar = sidecar | additional_args
 
@@ -421,9 +382,7 @@ def save_sidecar(
     def save_settings(self, out_path_root: _PathLike, folder_name: str) -> None:
         nm_IO.save_settings(self.settings, out_path_root, folder_name)
 
-    def save_nm_channels(
-        self, out_path_root: _PathLike, folder_name: str
-    ) -> None:
+    def save_nm_channels(self, out_path_root: _PathLike, folder_name: str) -> None:
         nm_IO.save_nm_channels(self.nm_channels, out_path_root, folder_name)
 
     def save_features(
diff --git a/py_neuromodulation/nm_settings.py b/py_neuromodulation/nm_settings.py
index be0f2ee8..59c6b7a9 100644
--- a/py_neuromodulation/nm_settings.py
+++ b/py_neuromodulation/nm_settings.py
@@ -1,17 +1,13 @@
 """Module for handling settings."""
-import pathlib
 
-import pandas as pd
-
-from py_neuromodulation import nm_IO
+from py_neuromodulation import PYNM_DIR
+from py_neuromodulation.nm_IO import read_settings
 
 
 def get_default_settings() -> dict:
     """Read default settings from nm_settings.json"""
-    settings_path = str(
-        pathlib.Path(__file__).parent.resolve() / "nm_settings.json"
-    )
-    return nm_IO.read_settings(settings_path)
+    settings_path = str(PYNM_DIR / "nm_settings.json")
+    return read_settings(settings_path)
 
 
 def reset_settings(
diff --git a/py_neuromodulation/nm_sharpwaves.py b/py_neuromodulation/nm_sharpwaves.py
index f64595f8..2d65ea82 100644
--- a/py_neuromodulation/nm_sharpwaves.py
+++ b/py_neuromodulation/nm_sharpwaves.py
@@ -1,28 +1,25 @@
 import numpy as np
 from scipy import signal
 from mne.filter import create_filter
-from typing import Iterable
+from collections.abc import Iterable
 
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features_abc import Feature
 
 
 class NoValidTroughException(Exception):
     pass
 
 
-class SharpwaveAnalyzer(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
-
+class SharpwaveAnalyzer(Feature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.sw_settings = settings["sharpwave_analysis_settings"]
         self.sfreq = sfreq
         self.ch_names = ch_names
-        self.list_filter = []
-        for filter_range in settings["sharpwave_analysis_settings"][
-            "filter_ranges_hz"
-        ]:
+        self.list_filter: list = []
+        self.trough: list = []
+        self.troughs_idx: list = []
 
+        for filter_range in settings["sharpwave_analysis_settings"]["filter_ranges_hz"]:
             if filter_range[0] is None:
                 self.list_filter.append(("no_filter", None))
             else:
@@ -44,9 +41,9 @@ def __init__(
                 )
 
         # initialize used features
-        self.used_features = list()
+        self.used_features = []
         for feature_name, val in self.sw_settings["sharpwave_features"].items():
-            if val is True:
+            if val:
                 self.used_features.append(feature_name)
 
         # initialize attributes
@@ -55,12 +52,8 @@ def __init__(
         # initializing estimator functions, respecitive for all sharpwave features
         fun_names = []
         for used_feature in self.used_features:
-            estimator_list_feature = (
-                []
-            )  # one feature can have multiple estimators
-            for estimator, est_features in self.sw_settings[
-                "estimator"
-            ].items():
+            estimator_list_feature = []  # one feature can have multiple estimators
+            for estimator, est_features in self.sw_settings["estimator"].items():
                 if est_features is not None:
                     for est_feature in est_features:
                         if used_feature == est_feature:
@@ -69,21 +62,18 @@ def __init__(
 
         self.estimator_names = fun_names
         self.estimator_functions = [
-            [
-                getattr(np, est_name)
-                for est_name in self.estimator_names[feature_idx]
-            ]
+            [getattr(np, est_name) for est_name in self.estimator_names[feature_idx]]
             for feature_idx in range(len(self.estimator_names))
         ]
 
     def _initialize_sw_features(self) -> None:
         """Resets used attributes to empty lists"""
         for feature_name in self.used_features:
-            setattr(self, feature_name, list())
+            setattr(self, feature_name, [])
         if "trough" not in self.used_features:
             # trough attribute is still necessary, even if it is not specified in settings
-            self.trough = list()
-        self.troughs_idx = list()
+            self.trough = []
+        self.troughs_idx = []
 
     def _get_peaks_around(self, trough_ind, arr_ind_peaks, filtered_dat):
         """Find the closest peaks to the right and left side a given trough.
@@ -105,12 +95,16 @@ def _get_peaks_around(self, trough_ind, arr_ind_peaks, filtered_dat):
         peak_right_val (np.ndarray): value of righ peak
         """
 
-        try: peak_right_idx = arr_ind_peaks[arr_ind_peaks > trough_ind][0]
-        except IndexError: raise NoValidTroughException("No valid trough")
+        try:
+            peak_right_idx = arr_ind_peaks[arr_ind_peaks > trough_ind][0]
+        except IndexError:
+            raise NoValidTroughException("No valid trough")
+
+        try:
+            peak_left_idx = arr_ind_peaks[arr_ind_peaks < trough_ind][-1]
+        except IndexError:
+            raise NoValidTroughException("No valid trough")
 
-        try: peak_left_idx  = arr_ind_peaks[arr_ind_peaks < trough_ind][-1]
-        except IndexError: raise NoValidTroughException("No valid trough")
-       
         return (
             peak_left_idx,
             peak_right_idx,
@@ -120,7 +114,7 @@ def _get_peaks_around(self, trough_ind, arr_ind_peaks, filtered_dat):
 
     def calc_feature(
         self,
-        data: np.array,
+        data: np.ndarray,
         features_compute: dict,
     ) -> dict:
         """Given a new data batch, the peaks, troughs and sharpwave features
@@ -139,32 +133,26 @@ def calc_feature(
         """
         for ch_idx, ch_name in enumerate(self.ch_names):
             for filter_name, filter in self.list_filter:
-                self.data_process_sw = (data[ch_idx, :] 
-                    if filter_name == "no_filter" 
+                self.data_process_sw = (
+                    data[ch_idx, :]
+                    if filter_name == "no_filter"
                     else signal.fftconvolve(data[ch_idx, :], filter, mode="same")
                 )
 
                 # check settings if troughs and peaks are analyzed
 
-                dict_ch_features = {}
+                dict_ch_features: dict[str, dict[str, float]] = {}
 
                 for detect_troughs in [False, True]:
-
-                    if detect_troughs is False:
-                        if (
-                            self.sw_settings["detect_peaks"]["estimate"]
-                            is False
-                        ):
+                    if not detect_troughs:
+                        if self.sw_settings["detect_peaks"]["estimate"] is False:
                             continue
                         key_name_pt = "Peak"
                         # the detect_troughs loop start with peaks, s.t. data does not
                         # need to be flipped
 
-                    if detect_troughs is True:
-                        if (
-                            self.sw_settings["detect_troughs"]["estimate"]
-                            is False
-                        ):
+                    if detect_troughs:
+                        if self.sw_settings["detect_troughs"]["estimate"] is False:
                             continue
                         key_name_pt = "Trough"
 
@@ -174,9 +162,7 @@ def calc_feature(
                     self.analyze_waveform()
 
                     # for each feature take the respective fun.
-                    for feature_idx, feature_name in enumerate(
-                        self.used_features
-                    ):
+                    for feature_idx, feature_name in enumerate(self.used_features):
                         for est_idx, estimator_name in enumerate(
                             self.estimator_names[feature_idx]
                         ):
@@ -184,9 +170,7 @@ def calc_feature(
                                 [
                                     ch_name,
                                     "Sharpwave",
-                                    self.estimator_names[feature_idx][
-                                        est_idx
-                                    ].title(),
+                                    self.estimator_names[feature_idx][est_idx].title(),
                                     feature_name,
                                     filter_name,
                                 ]
@@ -203,9 +187,7 @@ def calc_feature(
                                 dict_ch_features[key_name] = {}
                             dict_ch_features[key_name][key_name_pt] = val
 
-                if self.sw_settings[
-                    "apply_estimator_between_peaks_and_troughs"
-                ]:
+                if self.sw_settings["apply_estimator_between_peaks_and_troughs"]:
                     # apply between 'Trough' and 'Peak' the respective function again
                     # save only the 'est_fun' (e.g. max) between them
 
@@ -226,9 +208,7 @@ def calc_feature(
                     # write all "flatted" key value pairs in features_
                     for key, value in dict_ch_features.items():
                         for key_sub, value_sub in dict_ch_features[key].items():
-                            features_compute[
-                                key + "_analyze_" + key_sub
-                            ] = value_sub
+                            features_compute[key + "_analyze_" + key_sub] = value_sub
 
         return features_compute
 
@@ -256,19 +236,18 @@ def analyze_waveform(self) -> None:
 
         """ Find left and right peak indexes for each trough """
         peak_pointer = 0
-        peak_idx_left = []
-        peak_idx_right = []
+        peak_idx_left_list = []
+        peak_idx_right_list = []
         first_valid = last_valid = 0
 
         for i, trough_idx in enumerate(troughs):
-            
             # Locate peak right of current trough
             while peak_pointer < peaks.size and peaks[peak_pointer] < trough_idx:
                 peak_pointer += 1
 
-            if peak_pointer - 1 < 0: 
-                # If trough has no peak to it's left, it's not valid 
-                first_valid = i + 1 # Try with next one
+            if peak_pointer - 1 < 0:
+                # If trough has no peak to it's left, it's not valid
+                first_valid = i + 1  # Try with next one
                 continue
 
             if peak_pointer == peaks.size:
@@ -276,13 +255,13 @@ def analyze_waveform(self) -> None:
                 continue
 
             last_valid = i
-            peak_idx_left.append(peaks[peak_pointer - 1])
-            peak_idx_right.append(peaks[peak_pointer])
+            peak_idx_left_list.append(peaks[peak_pointer - 1])
+            peak_idx_right_list.append(peaks[peak_pointer])
+
+        troughs = troughs[first_valid : last_valid + 1]  # Remove non valid troughs
 
-        troughs = troughs[first_valid:last_valid + 1] # Remove non valid troughs
-        
-        peak_idx_left = np.array(peak_idx_left, dtype=np.integer)
-        peak_idx_right = np.array(peak_idx_right, dtype=np.integer)
+        peak_idx_left = np.array(peak_idx_left_list, dtype=np.integer)
+        peak_idx_right = np.array(peak_idx_right_list, dtype=np.integer)
 
         peak_left = self.data_process_sw[peak_idx_left]
         peak_right = self.data_process_sw[peak_idx_right]
@@ -291,11 +270,13 @@ def analyze_waveform(self) -> None:
         # No need to store trough data as it is not used anywhere else in the program
         # self.trough.append(trough)
         # self.troughs_idx.append(trough_idx)
-         
+
         """ Calculate features (vectorized) """
-        
+
         if self.sw_settings["sharpwave_features"]["interval"]:
-            self.interval = np.concatenate(([0], np.diff(troughs))) * (1000 / self.sfreq)
+            self.interval = np.concatenate((np.zeros(1), np.diff(troughs))) * (
+                1000 / self.sfreq
+            )
 
         if self.sw_settings["sharpwave_features"]["peak_left"]:
             self.peak_left = peak_left
@@ -306,60 +287,81 @@ def analyze_waveform(self) -> None:
         if self.sw_settings["sharpwave_features"]["sharpness"]:
             # sharpess is calculated on a +- 5 ms window
             # valid troughs need 5 ms of margin on both siddes
-            troughs_valid = troughs[np.logical_and(
-                                troughs - int(5 * (1000 / self.sfreq)) > 0, 
-                                troughs + int(5 * (1000 / self.sfreq)) < self.data_process_sw.shape[0])]
+            troughs_valid = troughs[
+                np.logical_and(
+                    troughs - int(5 * (1000 / self.sfreq)) > 0,
+                    troughs + int(5 * (1000 / self.sfreq))
+                    < self.data_process_sw.shape[0],
+                )
+            ]
 
             self.sharpness = (
-                        (self.data_process_sw[troughs_valid] - self.data_process_sw[troughs_valid - int(5 * (1000 / self.sfreq))]) +
-                        (self.data_process_sw[troughs_valid] - self.data_process_sw[troughs_valid + int(5 * (1000 / self.sfreq))])
-                        ) / 2
+                (
+                    self.data_process_sw[troughs_valid]
+                    - self.data_process_sw[troughs_valid - int(5 * (1000 / self.sfreq))]
+                )
+                + (
+                    self.data_process_sw[troughs_valid]
+                    - self.data_process_sw[troughs_valid + int(5 * (1000 / self.sfreq))]
+                )
+            ) / 2
 
-        if (self.sw_settings["sharpwave_features"]["rise_steepness"] or
-            self.sw_settings["sharpwave_features"]["decay_steepness"]):
-            
+        if (
+            self.sw_settings["sharpwave_features"]["rise_steepness"]
+            or self.sw_settings["sharpwave_features"]["decay_steepness"]
+        ):
             # steepness is calculated as the first derivative
-            steepness = np.concatenate(([0],np.diff(self.data_process_sw)))
+            steepness: np.ndarray = np.concatenate(
+                (np.zeros(1), np.diff(self.data_process_sw))
+            )
 
-            if self.sw_settings["sharpwave_features"]["rise_steepness"]: # left peak -> trough
+            if self.sw_settings["sharpwave_features"][
+                "rise_steepness"
+            ]:  # left peak -> trough
                 # + 1 due to python syntax, s.t. the last element is included
-                self.rise_steepness = np.array([
-                    np.max(np.abs(steepness[peak_idx_left[i] : troughs[i] + 1]))
-                    for i in range(trough_idx.size)
-                ])
-                
-            if self.sw_settings["sharpwave_features"]["decay_steepness"]: # trough -> right peak
-                self.decay_steepness = np.array([
-                    np.max(np.abs(steepness[troughs[i] : peak_idx_right[i] + 1]))
-                    for i in range(trough_idx.size)
-                ])
-
-            if (self.sw_settings["sharpwave_features"]["rise_steepness"] and
-                self.sw_settings["sharpwave_features"]["decay_steepness"] and
-                self.sw_settings["sharpwave_features"]["slope_ratio"]):
+                self.rise_steepness = np.array(
+                    [
+                        np.max(np.abs(steepness[peak_idx_left[i] : troughs[i] + 1]))
+                        for i in range(trough_idx.size)
+                    ]
+                )
+
+            if self.sw_settings["sharpwave_features"][
+                "decay_steepness"
+            ]:  # trough -> right peak
+                self.decay_steepness = np.array(
+                    [
+                        np.max(np.abs(steepness[troughs[i] : peak_idx_right[i] + 1]))
+                        for i in range(trough_idx.size)
+                    ]
+                )
+
+            if (
+                self.sw_settings["sharpwave_features"]["rise_steepness"]
+                and self.sw_settings["sharpwave_features"]["decay_steepness"]
+                and self.sw_settings["sharpwave_features"]["slope_ratio"]
+            ):
                 self.slope_ratio = self.rise_steepness - self.decay_steepness
 
         if self.sw_settings["sharpwave_features"]["prominence"]:
             self.prominence = np.abs((peak_right + peak_left) / 2 - trough_values)
 
         if self.sw_settings["sharpwave_features"]["decay_time"]:
-            self.decay_time = (peak_idx_left - troughs) * (1000 / self.sfreq) # ms
+            self.decay_time = (peak_idx_left - troughs) * (1000 / self.sfreq)  # ms
 
         if self.sw_settings["sharpwave_features"]["rise_time"]:
-            self.rise_time = (peak_idx_right - troughs) * (1000 / self.sfreq) # ms
+            self.rise_time = (peak_idx_right - troughs) * (1000 / self.sfreq)  # ms
 
         if self.sw_settings["sharpwave_features"]["width"]:
             self.width = peak_idx_right - peak_idx_left  # ms
-    
+
     @staticmethod
     def test_settings(
         s: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
     ):
-        for filter_range in s["sharpwave_analysis_settings"][
-            "filter_ranges_hz"
-        ]:
+        for filter_range in s["sharpwave_analysis_settings"]["filter_ranges_hz"]:
             assert isinstance(
                 filter_range[0],
                 int,
@@ -376,19 +378,17 @@ def test_settings(
                 f"got sfreq {sfreq} and filter range {filter_range}"
             )
         # check if all features are also enbled via an estimator
-        used_features = list()
+        used_features = []
         for feature_name, val in s["sharpwave_analysis_settings"][
             "sharpwave_features"
         ].items():
             assert isinstance(
                 val, bool
             ), f"sharpwave_feature type {feature_name} has to be of type bool, got {val}"
-            if val is True:
+            if val:
                 used_features.append(feature_name)
         for used_feature in used_features:
-            estimator_list_feature = (
-                []
-            )  # one feature can have multiple estimators
+            estimator_list_feature = []  # one feature can have multiple estimators
             for estimator, est_features in s["sharpwave_analysis_settings"][
                 "estimator"
             ].items():
diff --git a/py_neuromodulation/nm_stats.py b/py_neuromodulation/nm_stats.py
index d245c536..a328f0d8 100644
--- a/py_neuromodulation/nm_stats.py
+++ b/py_neuromodulation/nm_stats.py
@@ -81,7 +81,7 @@ def permutationTestSpearmansRho(x, y, plot_distr=True, x_unit=None, p=5000):
     else:
         p_val = len(np.where(pD >= gT)[0]) / p
 
-    if plot_distr is True:
+    if plot_distr:
         plt.hist(pD, bins=30, label="permutation results")
         plt.axvline(gT, color="orange", label="ground truth")
         plt.title("ground truth " + x_unit + "=" + str(gT) + " p=" + str(p_val))
@@ -134,7 +134,7 @@ def permutationTest(x, y, plot_distr=True, x_unit=None, p=5000):
     else:
         p_val = len(np.where(pD >= gT)[0]) / p
 
-    if plot_distr is True:
+    if plot_distr:
         plt.hist(pD, bins=30, label="permutation results")
         plt.axvline(gT, color="orange", label="ground truth")
         plt.title("ground truth " + x_unit + "=" + str(gT) + " p=" + str(p_val))
@@ -171,16 +171,14 @@ def permutationTest_relative(x, y, plot_distr=True, x_unit=None, p=5000):
             else:
                 l_.append((y[i], x[i]))
         pD.append(
-            np.abs(
-                np.average(np.array(l_)[:, 0]) - np.average(np.array(l_)[:, 1])
-            )
+            np.abs(np.average(np.array(l_)[:, 0]) - np.average(np.array(l_)[:, 1]))
         )
     if gT < 0:
         p_val = len(np.where(pD <= gT)[0]) / p
     else:
         p_val = len(np.where(pD >= gT)[0]) / p
 
-    if plot_distr is True:
+    if plot_distr:
         plt.hist(pD, bins=30, label="permutation results")
         plt.axvline(gT, color="orange", label="ground truth")
         plt.title("ground truth " + x_unit + "=" + str(gT) + " p=" + str(p_val))
@@ -217,15 +215,13 @@ def permutation_numba_onesample(x, y, n_perm, two_tailed=True):
     float
         P-value of permutation test
     """
-    if two_tailed is True:
+    if two_tailed:
         zeroed = x - y
         z = np.abs(np.mean(zeroed))
         p = np.empty(n_perm)
         # Run the simulation n_perm times
         for i in np.arange(n_perm):
-            sign = np.random.choice(
-                a=np.array([-1.0, 1.0]), size=len(x), replace=True
-            )
+            sign = np.random.choice(a=np.array([-1.0, 1.0]), size=len(x), replace=True)
             p[i] = np.abs(np.mean(zeroed * sign))
     else:
         zeroed = x - y
@@ -233,9 +229,7 @@ def permutation_numba_onesample(x, y, n_perm, two_tailed=True):
         p = np.empty(n_perm)
         # Run the simulation n_perm times
         for i in np.arange(n_perm):
-            sign = np.random.choice(
-                a=np.array([-1.0, 1.0]), size=len(x), replace=True
-            )
+            sign = np.random.choice(a=np.array([-1.0, 1.0]), size=len(x), replace=True)
             p[i] = np.mean(zeroed * sign)
         # Return p-value
     return z, (np.sum(p >= z)) / n_perm
@@ -267,7 +261,7 @@ def permutation_numba_twosample(x, y, n_perm, two_tailed=True):
     float
         P-value of permutation test
     """
-    if two_tailed is True:
+    if two_tailed:
         z = np.abs(np.mean(x) - np.mean(y))
         pS = np.concatenate((x, y), axis=0)
         half = int(len(pS) / 2)
@@ -319,9 +313,7 @@ def cluster_wise_p_val_correction(p_arr, p_sig=0.05, num_permutations=10000):
     for cluster_i in np.arange(num_clusters):
         # first cluster is assigned to be 1 from measure.label
         index_cluster[cluster_i] = np.where(labels == cluster_i + 1)[0]
-        p_cluster_sum[cluster_i] = np.sum(
-            np.array(1 - p_arr)[index_cluster[cluster_i]]
-        )
+        p_cluster_sum[cluster_i] = np.sum(np.array(1 - p_arr)[index_cluster[cluster_i]])
     # p_min corresponds to the most unlikely cluster
     p_min = np.max(p_cluster_sum)
 
@@ -330,13 +322,9 @@ def cluster_wise_p_val_correction(p_arr, p_sig=0.05, num_permutations=10000):
     # loop through random permutation cycles
     r_per_arr = np.zeros(num_permutations)
     for r in range(num_permutations):
-        r_per = np.random.randint(
-            low=0, high=p_arr.shape[0], size=p_arr.shape[0]
-        )
+        r_per = np.random.randint(low=0, high=p_arr.shape[0], size=p_arr.shape[0])
 
-        labels, num_clusters = measure.label(
-            p_arr[r_per] <= p_sig, return_num=True
-        )
+        labels, num_clusters = measure.label(p_arr[r_per] <= p_sig, return_num=True)
 
         index_cluster = {}
         if num_clusters == 0:
@@ -400,7 +388,7 @@ def cluster(iterable):
 
         Returns
         -------
-        cluster_labels : np.array
+        cluster_labels : np.ndarray
             Array of shape (len(iterable), 1), where each value indicates the
             number of the cluster. Values are 0 if the item does not belong to
             a cluster
@@ -444,9 +432,7 @@ def calculate_null_distribution(p_arr_, p_sig_, n_perm_):
         # loop through random permutation cycles
         r_per_arr = np.zeros(n_perm_)
         for r in range(n_perm_):
-            r_per = np.random.randint(
-                low=0, high=p_arr_.shape[0], size=p_arr_.shape[0]
-            )
+            r_per = np.random.randint(low=0, high=p_arr_.shape[0], size=p_arr_.shape[0])
             labels_, n_clusters = cluster(p_arr_[r_per] <= p_sig_)
 
             cluster_ind = {}
@@ -456,9 +442,7 @@ def calculate_null_distribution(p_arr_, p_sig_, n_perm_):
                 p_sum = np.zeros(n_clusters)
                 for ind in range(n_clusters):
                     cluster_ind[ind] = np.where(labels_ == ind + 1)[0]
-                    p_sum[ind] = np.sum(
-                        np.asarray(1 - p_arr_[r_per])[cluster_ind[ind]]
-                    )
+                    p_sum[ind] = np.sum(np.asarray(1 - p_arr_[r_per])[cluster_ind[ind]])
                 r_per_arr[r] = np.max(p_sum)
         return r_per_arr
 
diff --git a/py_neuromodulation/nm_stream_abc.py b/py_neuromodulation/nm_stream_abc.py
index 76d8d003..d27d3fa3 100644
--- a/py_neuromodulation/nm_stream_abc.py
+++ b/py_neuromodulation/nm_stream_abc.py
@@ -1,52 +1,25 @@
-"""Module that contains PNStream ABC."""
+"""Module that contains NMStream ABC."""
 
 from abc import ABC, abstractmethod
-import os
-import pathlib
-import _pickle as cPickle
+from pathlib import Path
+import pickle
 
-from .utils import _logging  # logger initialization
-
-# Logger use in different modules: logger = logging.getLogger("PynmLogger")
+import pandas as pd
 
+from py_neuromodulation import nm_IO, PYNM_DIR
+from py_neuromodulation.nm_run_analysis import DataProcessor
+from py_neuromodulation.nm_settings import get_default_settings
+from py_neuromodulation.nm_types import _PathLike
 
-import pandas as pd
-from sklearn import base
-
-from py_neuromodulation import (
-    nm_features,
-    nm_IO,
-    nm_plots,
-    nm_run_analysis,
-    nm_settings,
-)
-
-_PathLike = str | os.PathLike
-
-
-class PNStream(ABC):
-
-    settings: dict
-    nm_channels: pd.DataFrame
-    run_analysis: nm_run_analysis.DataProcessor
-    features: nm_features.Features
-    coords: dict
-    sfreq: int | float
-    sfreq_feature: int | float = None
-    path_grids: _PathLike | None
-    model: base.BaseEstimator | None
-    sess_right: bool | None
-    verbose: bool
-    PATH_OUT: _PathLike | None
-    PATH_OUT_folder_name: _PathLike | None
 
+class NMStream(ABC):
     def __init__(
         self,
-        sfreq: int | float,
+        sfreq: float,
         nm_channels: pd.DataFrame | _PathLike,
         settings: dict | _PathLike | None = None,
-        line_noise: int | float | None = 50,
-        sampling_rate_features_hz: int | float | None = None,
+        line_noise: float | None = 50,
+        sampling_rate_features_hz: float | None = None,
         path_grids: _PathLike | None = None,
         coord_names: list | None = None,
         coord_list: list | None = None,
@@ -56,15 +29,15 @@ def __init__(
 
         Parameters
         ----------
-        sfreq : int | float
+        sfreq : float
             sampling frequency of data in Hertz
         nm_channels : pd.DataFrame | _PathLike
             parametrization of channels (see nm_define_channels.py for initialization)
         settings : dict | _PathLike | None, optional
             features settings can be a dictionary or path to the nm_settings.json, by default the py_neuromodulation/nm_settings.json are read
-        line_noise : int | float | None, optional
+        line_noise : float | None, optional
             line noise, by default 50
-        sampling_rate_features_hz : int | float | None, optional
+        sampling_rate_features_hz : float | None, optional
             feature sampling rate, by default None
         path_grids : _PathLike | None, optional
             path to grid_cortex.tsv and/or gird_subcortex.tsv, by default Non
@@ -78,13 +51,11 @@ def __init__(
         self.settings = self._load_settings(settings)
 
         if sampling_rate_features_hz is not None:
-            self.settings["sampling_rate_features_hz"] = (
-                sampling_rate_features_hz
-            )
+            self.settings["sampling_rate_features_hz"] = sampling_rate_features_hz
 
         self.nm_channels = self._load_nm_channels(nm_channels)
         if path_grids is None:
-            path_grids = pathlib.Path(__file__).parent.resolve()
+            path_grids = PYNM_DIR
         self.path_grids = path_grids
         self.verbose = verbose
         self.sfreq = sfreq
@@ -95,7 +66,7 @@ def __init__(
         self.projection = None
         self.model = None
 
-        self.run_analysis = nm_run_analysis.DataProcessor(
+        self.run_analysis = DataProcessor(
             sfreq=self.sfreq,
             settings=self.settings,
             nm_channels=self.nm_channels,
@@ -107,12 +78,12 @@ def __init__(
         )
 
     @abstractmethod
-    def run(self):
+    def run(self) -> pd.DataFrame:
         """Reinitialize the stream
         This might be handy in case the nm_channels or nm_settings changed
         """
 
-        self.run_analysis = nm_run_analysis.DataProcessor(
+        self.run_analysis = DataProcessor(
             sfreq=self.sfreq,
             settings=self.settings,
             nm_channels=self.nm_channels,
@@ -124,9 +95,7 @@ def run(self):
         )
 
     @abstractmethod
-    def _add_timestamp(
-        self, feature_series: pd.Series, idx: int | None = None
-    ) -> pd.Series:
+    def _add_timestamp(self, feature_series: pd.Series, cnt_samples: int) -> pd.Series:
         """Add to feature_series "time" keyword
         For Bids specify with fs_features, for real time analysis with current time stamp
         """
@@ -160,30 +129,30 @@ def _load_settings(settings: dict | _PathLike | None) -> dict:
         if isinstance(settings, dict):
             return settings
         if settings is None:
-            return nm_settings.get_default_settings()
+            return get_default_settings()
         return nm_IO.read_settings(str(settings))
 
     def load_model(self, model_name: _PathLike) -> None:
         """Load sklearn model, that utilizes predict"""
         with open(model_name, "rb") as fid:
-            self.model = cPickle.load(fid)
+            self.model = pickle.load(fid)
 
     def save_after_stream(
         self,
-        out_path_root: _PathLike | None = None,
+        out_path_root: _PathLike = "",
         folder_name: str = "sub",
         feature_arr: pd.DataFrame | None = None,
     ) -> None:
         """Save features, settings, nm_channels and sidecar after run"""
 
-        if out_path_root is None:
-            out_path_root = os.getcwd()
+        out_path_root = Path.cwd() if not out_path_root else Path(out_path_root)
+
         # create derivate folder_name output folder if doesn't exist
-        if os.path.exists(os.path.join(out_path_root, folder_name)) is False:
-            os.makedirs(os.path.join(out_path_root, folder_name))
+        (out_path_root / folder_name).mkdir(parents=True, exist_ok=True)
 
         self.PATH_OUT = out_path_root
         self.PATH_OUT_folder_name = folder_name
+
         self.save_sidecar(out_path_root, folder_name)
 
         if feature_arr is not None:
@@ -201,9 +170,7 @@ def save_features(
     ) -> None:
         nm_IO.save_features(feature_arr, out_path_root, folder_name)
 
-    def save_nm_channels(
-        self, out_path_root: _PathLike, folder_name: str
-    ) -> None:
+    def save_nm_channels(self, out_path_root: _PathLike, folder_name: str) -> None:
         self.run_analysis.save_nm_channels(out_path_root, folder_name)
 
     def save_settings(self, out_path_root: _PathLike, folder_name: str) -> None:
@@ -213,6 +180,4 @@ def save_sidecar(self, out_path_root: _PathLike, folder_name: str) -> None:
         """Save sidecar incduing fs, coords, sess_right to
         out_path_root and subfolder 'folder_name'"""
         additional_args = {"sess_right": self.sess_right}
-        self.run_analysis.save_sidecar(
-            out_path_root, folder_name, additional_args
-        )
+        self.run_analysis.save_sidecar(out_path_root, folder_name, additional_args)
diff --git a/py_neuromodulation/nm_stream_offline.py b/py_neuromodulation/nm_stream_offline.py
index 708dcbb8..95acae5f 100644
--- a/py_neuromodulation/nm_stream_offline.py
+++ b/py_neuromodulation/nm_stream_offline.py
@@ -1,38 +1,29 @@
 """Module for offline data streams."""
 
-import os
-from joblib import Parallel, delayed
 import numpy as np
 import pandas as pd
 from itertools import count
-import logging
-
-logger = logging.getLogger("PynmLogger")
-
 import mne
 
-from py_neuromodulation import (
-    nm_generator,
-    nm_IO,
-    nm_stream_abc,
-    nm_define_nmchannels,
-)
+from joblib import Parallel, delayed
 
-_PathLike = str | os.PathLike
+from py_neuromodulation.nm_generator import raw_data_generator
+from py_neuromodulation.nm_stream_abc import NMStream
+from py_neuromodulation.nm_define_nmchannels import get_default_channels_from_data
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation import logger
 
 
-class _OfflineStream(nm_stream_abc.PNStream):
+class _OfflineStream(NMStream):
     """Offline stream base class.
     This class can be inhereted for different types of offline streams, e.g. epoch-based or continuous.
 
     Parameters
     ----------
-    nm_stream_abc : nm_stream_abc.PNStream
+    nm_stream_abc : nm_stream_abc.NMStream
     """
 
-    def _add_target(
-        self, feature_series: pd.Series, data: np.ndarray
-    ) -> pd.Series:
+    def _add_target(self, feature_series: pd.Series, data: np.ndarray) -> pd.Series:
         """Add target channels to feature series.
 
         Parameters
@@ -57,15 +48,11 @@ def _add_target(
                 ].to_list()
                 self.target_idx_initialized = True
 
-            for target_idx, target_name in zip(
-                self.target_indexes, self.target_names
-            ):
+            for target_idx, target_name in zip(self.target_indexes, self.target_names):
                 feature_series[target_name] = data[target_idx, -1]
         return feature_series
 
-    def _add_timestamp(
-        self, feature_series: pd.Series, cnt_samples: int
-    ) -> pd.Series:
+    def _add_timestamp(self, feature_series: pd.Series, cnt_samples: int) -> pd.Series:
         """Add time stamp in ms.
 
         Due to normalization run_analysis needs to keep track of the counted
@@ -74,7 +61,7 @@ def _add_timestamp(
         feature_series["time"] = cnt_samples * 1000 / self.sfreq
 
         if self.verbose:
-            logging.info(
+            logger.info(
                 str(np.round(feature_series["time"] / 1000, 2))
                 + " seconds of data processed"
             )
@@ -90,7 +77,7 @@ def _handle_data(self, data: np.ndarray | pd.DataFrame) -> np.ndarray:
                     "If data is passed as an array, the first dimension must"
                     " match the number of channel names in `nm_channels`.\n"
                     f" Number of data channels (data.shape[0]): {data.shape[0]}\n"
-                    f" Length of nm_channels[\"name\"]: {len(names_expected)}."
+                    f' Length of nm_channels["name"]: {len(names_expected)}.'
                 )
             return data
         names_data = data.columns.to_list()
@@ -102,7 +89,7 @@ def _handle_data(self, data: np.ndarray | pd.DataFrame) -> np.ndarray:
                 "If data is passed as a DataFrame, the"
                 "column names must match the channel names in `nm_channels`.\n"
                 f"Input dataframe column names: {names_data}\n"
-                f"Expected (from nm_channels[\"name\"]): : {names_expected}."
+                f'Expected (from nm_channels["name"]): : {names_expected}.'
             )
         return data.to_numpy().transpose()
 
@@ -117,19 +104,17 @@ def _check_settings_for_parallel(self):
             raise ValueError(
                 "Parallel processing is not possible with raw_normalization normalization."
             )
-        if self.settings["postprocessing"]["feature_normalization"] is True:
+        if self.settings["postprocessing"]["feature_normalization"]:
             raise ValueError(
                 "Parallel processing is not possible with feature normalization."
             )
-        if self.settings["features"]["bursts"] is True:
+        if self.settings["features"]["bursts"]:
             raise ValueError(
                 "Parallel processing is not possible with burst estimation."
             )
 
     def _process_batch(self, data_batch, cnt_samples):
-        feature_series = self.run_analysis.process(
-            data_batch.astype(np.float64)
-        )
+        feature_series = self.run_analysis.process(data_batch.astype(np.float64))
         feature_series = self._add_timestamp(feature_series, cnt_samples)
         feature_series = self._add_target(
             feature_series=feature_series, data=data_batch
@@ -144,7 +129,7 @@ def _run_offline(
         parallel: bool = False,
         n_jobs: int = -2,
     ) -> pd.DataFrame:
-        generator = nm_generator.raw_data_generator(
+        generator = raw_data_generator(
             data=data,
             settings=self.settings,
             sfreq=self.sfreq,
@@ -174,9 +159,7 @@ def _run_offline(
                 feature_series = self.run_analysis.process(
                     data_batch.astype(np.float64)
                 )
-                feature_series = self._add_timestamp(
-                    feature_series, cnt_samples
-                )
+                feature_series = self._add_timestamp(feature_series, cnt_samples)
 
                 feature_series = self._add_target(
                     feature_series=feature_series, data=data_batch
@@ -193,11 +176,11 @@ def _run_offline(
 
     def plot_raw_signal(
         self,
-        sfreq: float = None,
-        data: np.array = None,
-        lowpass: float = None,
-        highpass: float = None,
-        picks: list = None,
+        sfreq: float | None = None,
+        data: np.ndarray | None = None,
+        lowpass: float | None = None,
+        highpass: float | None = None,
+        picks: list | None = None,
         plot_time: bool = True,
         plot_psd: bool = False,
     ) -> None:
@@ -207,7 +190,7 @@ def plot_raw_signal(
         ----------
         sfreq : float
             sampling frequency [Hz]
-        data : np.array, optional
+        data : np.ndarray, optional
             data (n_channels, n_times), by default None
         plot_time : bool, optional
             mne.io.RawArray.plot(), by default True
@@ -236,9 +219,7 @@ def plot_raw_signal(
             ch_types = ["ecog" for i in range(data.shape[0])]
 
         # create mne.RawArray
-        info = mne.create_info(
-            ch_names=ch_names, sfreq=sfreq, ch_types=ch_types
-        )
+        info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
         raw = mne.io.RawArray(data, info)
 
         if picks is not None:
@@ -253,12 +234,12 @@ def plot_raw_signal(
 class Stream(_OfflineStream):
     def __init__(
         self,
-        sfreq: int | float,
-        data: np.ndarray | pd.DataFrame = None,
-        nm_channels: pd.DataFrame | _PathLike = None,
+        sfreq: float,
+        data: np.ndarray | pd.DataFrame | None = None,
+        nm_channels: pd.DataFrame | _PathLike | None = None,
         settings: dict | _PathLike | None = None,
-        sampling_rate_features_hz: float = None,
-        line_noise: int | float | None = 50,
+        sampling_rate_features_hz: float | None = None,
+        line_noise: float | None = 50,
         path_grids: _PathLike | None = None,
         coord_names: list | None = None,
         coord_list: list | None = None,
@@ -268,7 +249,7 @@ def __init__(
 
         Parameters
         ----------
-        sfreq : int | float
+        sfreq : float
             sampling frequency of data in Hertz
         data : np.ndarray | pd.DataFrame | None, optional
             data to be streamed with shape (n_channels, n_time), by default None
@@ -276,9 +257,9 @@ def __init__(
             parametrization of channels (see nm_define_channels.py for initialization)
         settings : dict | _PathLike | None, optional
             features settings can be a dictionary or path to the nm_settings.json, by default the py_neuromodulation/nm_settings.json are read
-        line_noise : int | float | None, optional
+        line_noise : float | None, optional
             line noise, by default 50
-        sampling_rate_features_hz : int | float | None, optional
+        sampling_rate_features_hz : float | None, optional
             feature sampling rate, by default None
         path_grids : _PathLike | None, optional
             path to grid_cortex.tsv and/or gird_subcortex.tsv, by default Non
@@ -291,9 +272,7 @@ def __init__(
         """
 
         if nm_channels is None and data is not None:
-            nm_channels = nm_define_nmchannels.get_default_channels_from_data(
-                data
-            )
+            nm_channels = get_default_channels_from_data(data)
 
         if nm_channels is None and data is None:
             raise ValueError(
@@ -314,11 +293,11 @@ def __init__(
 
         self.data = data
 
-        self.target_idx_initialized = False
+        self.target_idx_initialized: bool = False
 
     def run(
         self,
-        data: np.ndarray | pd.DataFrame = None,
+        data: np.ndarray | pd.DataFrame | None = None,
         out_path_root: _PathLike | None = None,
         folder_name: str = "sub",
         parallel: bool = False,
@@ -351,7 +330,7 @@ def run(
         elif self.data is None and data is None:
             raise ValueError("No data passed to run function.")
 
-        if parallel is True:
+        if parallel:
             self._check_settings_for_parallel()
 
         return self._run_offline(
diff --git a/py_neuromodulation/nm_types.py b/py_neuromodulation/nm_types.py
new file mode 100644
index 00000000..1c115e4b
--- /dev/null
+++ b/py_neuromodulation/nm_types.py
@@ -0,0 +1,3 @@
+from os import PathLike
+
+_PathLike = str | PathLike
diff --git a/pyproject.toml b/pyproject.toml
index 5ca718f3..cafe1024 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -36,13 +36,10 @@ dependencies = [
   "matplotlib >= 3.3.4",
   "mne-bids >= 0.8",
   "mne-connectivity",
-  "mrmr-selection",
+  "mrmr_selection",
   "nolds",
   "numpy >= 1.21.2",
   "pandas >= 1.2.2",
-  "pip",
-  "pynput",
-  "pybids",
   "scikit-image",
   "scikit-learn >= 0.24.2",
   "scikit-optimize",
@@ -52,13 +49,13 @@ dependencies = [
   "pyparrm",
   "pyarrow>=14.0.2",
   "joblib>=1.3.2",
-  "wget",
   "hatch>=1.9.4",
+  "statsmodels"
 ]
 
 [project.optional-dependencies]
 dev = [
-    "black>=24.2.0",
+    "ruff",
     "pytest>=8.0.2",
     "pytest-cov",
     "pytest-sugar",
@@ -89,9 +86,8 @@ dev-dependencies = [
 bugtracker = "https://github.com/neuromodulation/py_neuromodulation/issues"
 repository = "https://github.com/neuromodulation/py_neuromodulation"
 
-[tool.black]
-line-length = 80
-target-version = ['py310']
+[tool.ruff]
+target-version = "py310"
 
 [tool.hatch.build.targets.sdist]
 exclude = ["/.github", "/docs"]
@@ -101,4 +97,7 @@ packages = ["py_neuromodulation"]
 exclude = [
   "py_neuromodulation/data/derivatives",
   "py_neuromodulation/ConnectivityDecoding/connectome_folder",
-]
\ No newline at end of file
+]
+
+[tool.mypy]
+ignore_missing_imports = true
\ No newline at end of file


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 93cae015..83eaf7c6 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -39,9 +39,7 @@ def setup_default_stream_fast_compute():
         line_noise,
         coord_list,
         coord_names,
-    ) = nm_IO.read_BIDS_data(
-        PATH_RUN=PATH_RUN, BIDS_PATH=PATH_BIDS, datatype=datatype
-    )
+    ) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN, BIDS_PATH=PATH_BIDS, datatype=datatype)
 
     nm_channels = nm_define_nmchannels.set_channels(
         ch_names=raw.ch_names,
@@ -102,16 +100,12 @@ def setup_databatch():
         line_noise,
         coord_list,
         coord_names,
-    ) = nm_IO.read_BIDS_data(
-        PATH_RUN=PATH_RUN, BIDS_PATH=PATH_BIDS, datatype=datatype
-    )
+    ) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN, BIDS_PATH=PATH_BIDS, datatype=datatype)
 
     settings = nm_settings.get_default_settings()
     settings = nm_settings.set_settings_fast_compute(settings)
 
-    generator = nm_generator.raw_data_generator(
-        data, settings, int(np.floor(sfreq))
-    )
+    generator = nm_generator.raw_data_generator(data, settings, int(np.floor(sfreq)))
     data_batch = next(generator, None)
 
     return [raw.ch_names, raw.get_channel_types(), raw.info["bads"], data_batch]
diff --git a/tests/test_all_examples.py b/tests/test_all_examples.py
index 917c37ff..46cbf404 100644
--- a/tests/test_all_examples.py
+++ b/tests/test_all_examples.py
@@ -1,6 +1,5 @@
 from pathlib import Path
 import pytest
-import subprocess
 import runpy
 
 
diff --git a/tests/test_all_features.py b/tests/test_all_features.py
index 3c6c55d9..39ab02e3 100644
--- a/tests/test_all_features.py
+++ b/tests/test_all_features.py
@@ -1,16 +1,13 @@
 import numpy as np
-import pandas as pd
-import pytest
 
 from py_neuromodulation import (
     nm_settings,
-    nm_stream_offline,
     nm_define_nmchannels,
-    nm_stream_abc,
+    Stream,
 )
 
 
-def get_example_stream(test_arr: np.array) -> nm_stream_abc.PNStream:
+def get_example_stream(test_arr: np.ndarray) -> Stream:
     settings = nm_settings.get_default_settings()
     settings["features"]["raw_hjorth"] = True
     settings["features"]["return_raw"] = True
@@ -27,15 +24,14 @@ def get_example_stream(test_arr: np.array) -> nm_stream_abc.PNStream:
 
     nm_channels = nm_define_nmchannels.get_default_channels_from_data(test_arr)
 
-    stream = nm_stream_offline.Stream(
+    stream = Stream(
         sfreq=1000, nm_channels=nm_channels, settings=settings, verbose=True
     )
     return stream
 
 
 def test_all_features_random_array():
-    """This test runs's through all enabled features, and check's if they break
-    """
+    """This test runs's through all enabled features, and check's if they break"""
     np.random.seed(0)
     arr = np.random.random([2, 2000])
     stream = get_example_stream(arr)
@@ -46,7 +42,6 @@ def test_all_features_random_array():
 
 
 def test_all_features_zero_array():
-
     arr = np.zeros([2, 2000])
     stream = get_example_stream(arr)
 
@@ -54,7 +49,6 @@ def test_all_features_zero_array():
 
 
 def test_all_features_NaN_array():
-
     arr = np.empty([2, 2000])
     arr[:] = np.nan
 
diff --git a/tests/test_bispectra.py b/tests/test_bispectra.py
index b54d74ad..34eb70ec 100644
--- a/tests/test_bispectra.py
+++ b/tests/test_bispectra.py
@@ -1,15 +1,10 @@
-import pytest
-import mne
 import numpy as np
 
 from py_neuromodulation import (
-    nm_analysis,
-    nm_decode,
     nm_define_nmchannels,
     nm_IO,
-    nm_plots,
     nm_settings,
-    nm_stream_offline,
+    Stream,
 )
 
 
@@ -29,9 +24,7 @@ def test_bispectrum():
         line_noise,
         coord_list,
         coord_names,
-    ) = nm_IO.read_BIDS_data(
-        PATH_RUN=PATH_RUN, BIDS_PATH=PATH_BIDS, datatype=datatype
-    )
+    ) = nm_IO.read_BIDS_data(PATH_RUN=PATH_RUN, BIDS_PATH=PATH_BIDS, datatype=datatype)
 
     ch_names = raw.ch_names[4]
     ch_types = raw.get_channel_types()[4]
@@ -51,7 +44,7 @@ def test_bispectrum():
 
     settings["features"]["bispectrum"] = True
 
-    stream = nm_stream_offline.Stream(
+    stream = Stream(
         settings=settings,
         nm_channels=nm_channels,
         path_grids=None,
@@ -64,7 +57,4 @@ def test_bispectrum():
 
     features = stream.run(np.expand_dims(data[3, :], axis=0))
 
-    assert (
-        features["ECOG_RIGHT_1_Bispectrum_phase_mean_whole_fband_range"].sum()
-        != 0
-    )
+    assert features["ECOG_RIGHT_1_Bispectrum_phase_mean_whole_fband_range"].sum() != 0
diff --git a/tests/test_bursts.py b/tests/test_bursts.py
index b46b81d3..8d016ea9 100644
--- a/tests/test_bursts.py
+++ b/tests/test_bursts.py
@@ -3,8 +3,6 @@
 from py_neuromodulation import (
     nm_bursts,
     nm_settings,
-    nm_stream_offline,
-    nm_define_nmchannels,
 )
 
 
@@ -90,9 +88,7 @@ def test_bursting_duration():
     # the percentile of the hilbert transform of a continuous oscillation will be high
     # select better max amplitude
     bursts = nm_bursts.Burst(settings, ch_names, sfreq)
-    f_burst = bursts.calc_feature(
-        beta_wave + np.random.random([NUM_CH, 1 * sfreq]), {}
-    )
+    f_burst = bursts.calc_feature(beta_wave + np.random.random([NUM_CH, 1 * sfreq]), {})
 
     assert (
         f["ch0_bursts_low beta_amplitude_max"]
diff --git a/tests/test_feature_sampling_rates.py b/tests/test_feature_sampling_rates.py
index 4179fe0f..45ba3e1e 100644
--- a/tests/test_feature_sampling_rates.py
+++ b/tests/test_feature_sampling_rates.py
@@ -2,18 +2,11 @@
 import pandas as pd
 import pytest
 
-from py_neuromodulation import (
-    nm_settings,
-    nm_stream_offline,
-    nm_define_nmchannels,
-    nm_stream_abc,
-)
+from py_neuromodulation import nm_settings, nm_stream_offline, nm_define_nmchannels
 
 
-def get_example_settings(test_arr: np.array) -> nm_stream_abc.PNStream:
-    settings = nm_settings.set_settings_fast_compute(
-        nm_settings.get_default_settings()
-    )
+def get_example_settings(test_arr: np.ndarray) -> tuple[dict, pd.DataFrame]:
+    settings = nm_settings.set_settings_fast_compute(nm_settings.get_default_settings())
 
     nm_channels = nm_define_nmchannels.get_default_channels_from_data(test_arr)
 
diff --git a/tests/test_fooof.py b/tests/test_fooof.py
index 1ed10636..c6577896 100644
--- a/tests/test_fooof.py
+++ b/tests/test_fooof.py
@@ -2,12 +2,9 @@
 
 
 def test_fooof_features(setup_default_stream_fast_compute):
-
     data, stream = setup_default_stream_fast_compute
 
-    generator = nm_generator.raw_data_generator(
-        data, stream.settings, stream.sfreq
-    )
+    generator = nm_generator.raw_data_generator(data, stream.settings, stream.sfreq)
     data_batch = next(generator, None)
     feature_series = stream.run_analysis.process(data_batch)
     # since the settings can define searching for "max_n_peaks" peaks
diff --git a/tests/test_initalization_offline_stream.py b/tests/test_initalization_offline_stream.py
index 066542cf..6def2603 100644
--- a/tests/test_initalization_offline_stream.py
+++ b/tests/test_initalization_offline_stream.py
@@ -8,7 +8,7 @@ def test_stream_init():
     """Test if stream initialization with passed data will setup nm_channels correctly"""
     np.random.seed(0)
     data = np.random.random((10, 1000))
-    sfreq = 100
+    sfreq = 500
     stream = nm.Stream(sfreq=sfreq, data=data, sampling_rate_features_hz=11)
 
     assert stream.nm_channels.shape[0] == 10
diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py
index b3eae0e5..851a8be1 100644
--- a/tests/test_multiprocessing.py
+++ b/tests/test_multiprocessing.py
@@ -1,6 +1,5 @@
 import py_neuromodulation as pn
 import numpy as np
-from py_neuromodulation import nm_settings
 import pytest
 
 
@@ -53,6 +52,8 @@ def test_multiprocessing_and_sequntial_features(get_stream):
             # This tests fails on Linux and Windows but passes on Mac OS; no idea why
             continue
 
-        assert features_sequential[column].equals(
+        assert features_sequential[
+            column
+        ].equals(
             features_multiprocessing[column]
         ), f"Column {column} is not equal between sequential and parallel dataframes computation"
diff --git a/tests/test_nan_values.py b/tests/test_nan_values.py
index 97b80fc5..31e21e07 100644
--- a/tests/test_nan_values.py
+++ b/tests/test_nan_values.py
@@ -21,9 +21,5 @@ def test_stream_with_none_data():
 
     # and check if all features of the second channel are not None
     assert len(
-        [
-            f
-            for f in features.columns
-            if "ch1" in f and features[f].notna().all()
-        ]
+        [f for f in features.columns if "ch1" in f and features[f].notna().all()]
     ) == len([f for f in features if "ch1" in f])
diff --git a/tests/test_nm_filter.py b/tests/test_nm_filter.py
index 79b52caf..be963f52 100644
--- a/tests/test_nm_filter.py
+++ b/tests/test_nm_filter.py
@@ -1,4 +1,5 @@
 """Test the nm_filter module."""
+
 import numpy as np
 from py_neuromodulation import nm_filter
 import pytest
diff --git a/tests/test_nm_resample.py b/tests/test_nm_resample.py
index b6701101..83acae9e 100644
--- a/tests/test_nm_resample.py
+++ b/tests/test_nm_resample.py
@@ -1,4 +1,5 @@
 """Test the nm_resample module."""
+
 import numpy as np
 
 from py_neuromodulation import nm_resample
diff --git a/tests/test_normalization_settings.py b/tests/test_normalization_settings.py
index 8e1ac5dc..67d37fe0 100644
--- a/tests/test_normalization_settings.py
+++ b/tests/test_normalization_settings.py
@@ -1,5 +1,3 @@
-import os
-import unittest
 import pytest
 import numpy as np
 
@@ -75,8 +73,7 @@ def test_zscore_feature_analysis():
         data_normed = norm.process(data_to_norm)
 
     expect_res = (
-        norm.previous[:, 0].std() * data_normed[0, 0]
-        + norm.previous[:, 0].mean()
+        norm.previous[:, 0].std() * data_normed[0, 0] + norm.previous[:, 0].mean()
     )
 
     assert pytest.approx(expect_res, 0.1) == data_to_norm[0, 0]
@@ -98,13 +95,10 @@ def test_zscore_raw_analysis():
         data_normed = norm.process(data_to_norm)
 
     expect_res = (
-        norm.previous[:, 0].std() * data_normed[0, 0]
-        + norm.previous[:, 0].mean()
+        norm.previous[:, 0].std() * data_normed[0, 0] + norm.previous[:, 0].mean()
     )
 
-    np.testing.assert_allclose(
-        expect_res, data_to_norm[0, 0], rtol=0.1, atol=0.1
-    )
+    np.testing.assert_allclose(expect_res, data_to_norm[0, 0], rtol=0.1, atol=0.1)
 
 
 def test_all_norm_methods_raw():
diff --git a/tests/test_notch_filter.py b/tests/test_notch_filter.py
index 3e951e36..47447953 100644
--- a/tests/test_notch_filter.py
+++ b/tests/test_notch_filter.py
@@ -1,12 +1,10 @@
 import numpy as np
-import pytest
-from scipy import fft, signal
+from scipy import fft
 
 from py_neuromodulation import nm_filter
 
 
 def test_notch_filter_setup():
-
     # by Nyquist theorem, frequencies are computed up to half sfreq
     for sfreq in [150, 200, 500, 1000]:
         line_noise = 50
diff --git a/tests/test_osc_features.py b/tests/test_osc_features.py
index b3ce00da..e958baed 100644
--- a/tests/test_osc_features.py
+++ b/tests/test_osc_features.py
@@ -130,8 +130,7 @@ def test_fft_beta_osc():
 
     assert (
         features_out["ch1_fft_beta_mean"] > features_out["ch1_fft_theta_mean"]
-        and features_out["ch1_fft_beta_mean"]
-        > features_out["ch1_fft_gamma_mean"]
+        and features_out["ch1_fft_beta_mean"] > features_out["ch1_fft_gamma_mean"]
     )
 
 
@@ -203,8 +202,7 @@ def test_stft_beta_osc():
 
     assert (
         features_out["ch1_stft_beta_mean"] > features_out["ch1_stft_theta_mean"]
-        and features_out["ch1_stft_beta_mean"]
-        > features_out["ch1_stft_gamma_mean"]
+        and features_out["ch1_stft_beta_mean"] > features_out["ch1_stft_gamma_mean"]
     )
 
 
@@ -243,10 +241,8 @@ def test_welch_beta_osc():
     features_out = stft_obj.calc_feature(data, {})
 
     assert (
-        features_out["ch1_welch_beta_mean"]
-        > features_out["ch1_welch_theta_mean"]
-        and features_out["ch1_welch_beta_mean"]
-        > features_out["ch1_welch_gamma_mean"]
+        features_out["ch1_welch_beta_mean"] > features_out["ch1_welch_theta_mean"]
+        and features_out["ch1_welch_beta_mean"] > features_out["ch1_welch_gamma_mean"]
     )
 
 
@@ -333,9 +329,7 @@ def test_bp_zero_data():
 
     settings["bandpass_filter_settings"]["log_transform"] = False
     settings["bandpass_filter_settings"]["kalman_filter"] = False
-    settings["bandpass_filter_settings"]["bandpower_features"][
-        "activity"
-    ] = True
+    settings["bandpass_filter_settings"]["bandpower_features"]["activity"] = True
 
     settings["frequency_ranges_hz"] = {"theta": [4, 8], "beta": [10, 20]}
     stft_obj = nm_oscillatory.BandPower(settings, ch_names, sfreq)
@@ -361,9 +355,7 @@ def test_bp_random_data():
 
     settings["bandpass_filter_settings"]["log_transform"] = False
     settings["bandpass_filter_settings"]["kalman_filter"] = False
-    settings["bandpass_filter_settings"]["bandpower_features"][
-        "activity"
-    ] = True
+    settings["bandpass_filter_settings"]["bandpower_features"]["activity"] = True
 
     stft_obj = nm_oscillatory.BandPower(settings, ch_names, sfreq)
     stft_obj.test_settings(settings, ch_names, sfreq)
@@ -397,9 +389,7 @@ def test_bp_beta_osc():
 
     settings["bandpass_filter_settings"]["log_transform"] = False
     settings["bandpass_filter_settings"]["kalman_filter"] = False
-    settings["bandpass_filter_settings"]["bandpower_features"][
-        "activity"
-    ] = True
+    settings["bandpass_filter_settings"]["bandpower_features"]["activity"] = True
 
     bp_obj = nm_oscillatory.BandPower(settings, ch_names, sfreq)
     bp_obj.test_settings(settings, ch_names, sfreq)
diff --git a/tests/test_preprocessing_filter.py b/tests/test_preprocessing_filter.py
index 139e680b..a205faa8 100644
--- a/tests/test_preprocessing_filter.py
+++ b/tests/test_preprocessing_filter.py
@@ -6,7 +6,6 @@
 
 
 def test_preprocessing_within_pipeline(setup_default_stream_fast_compute):
-
     data, stream = setup_default_stream_fast_compute
 
     stream.settings["preprocessing"].append("preprocessing_filter")
@@ -25,13 +24,10 @@ def test_preprocessing_within_pipeline(setup_default_stream_fast_compute):
 
 
 def test_preprocessing_filter_lowpass():
-
     data_batch = np.random.random([1, 1000])
 
     settings = nm_settings.get_default_settings()
-    settings["preprocessing"] = settings["preprocessing"].append(
-        "preprocessing_filter"
-    )
+    settings["preprocessing"] = settings["preprocessing"].append("preprocessing_filter")
     settings["preprocessing_filter"]["lowpass_filter"] = True
     settings["preprocessing_filter"]["highpass_filter"] = False
     settings["preprocessing_filter"]["bandpass_filter"] = False
@@ -55,13 +51,10 @@ def test_preprocessing_filter_lowpass():
 
 
 def test_preprocessing_filter_highpass():
-
     data_batch = np.random.random([1, 1000])
 
     settings = nm_settings.get_default_settings()
-    settings["preprocessing"] = settings["preprocessing"].append(
-        "preprocessing_filter"
-    )
+    settings["preprocessing"] = settings["preprocessing"].append("preprocessing_filter")
     settings["preprocessing_filter"]["highpass_filter"] = True
     settings["preprocessing_filter"]["lowpass_filter"] = False
     settings["preprocessing_filter"]["bandpass_filter"] = False
@@ -85,21 +78,18 @@ def test_preprocessing_filter_highpass():
 
 
 def test_preprocessing_filter_bandstop():
-
     data_batch = np.random.random([1, 1000])
 
     settings = nm_settings.get_default_settings()
-    settings["preprocessing"] = settings["preprocessing"].append(
-        "preprocessing_filter"
-    )
+    settings["preprocessing"] = settings["preprocessing"].append("preprocessing_filter")
     settings["preprocessing_filter"]["bandstop_filter"] = True
     settings["preprocessing_filter"]["bandpass_filter"] = False
     settings["preprocessing_filter"]["lowpass_filter"] = False
     settings["preprocessing_filter"]["highpass_filter"] = False
 
-    settings["preprocessing_filter"]["bandstop_filter_settings"][
-        "frequency_low_hz"
-    ] = 100
+    settings["preprocessing_filter"]["bandstop_filter_settings"]["frequency_low_hz"] = (
+        100
+    )
     settings["preprocessing_filter"]["bandstop_filter_settings"][
         "frequency_high_hz"
     ] = 160
@@ -118,21 +108,18 @@ def test_preprocessing_filter_bandstop():
 
 
 def test_preprocessing_filter_bandpass():
-
     data_batch = np.random.random([1, 1000])
 
     settings = nm_settings.get_default_settings()
-    settings["preprocessing"] = settings["preprocessing"].append(
-        "preprocessing_filter"
-    )
+    settings["preprocessing"] = settings["preprocessing"].append("preprocessing_filter")
     settings["preprocessing_filter"]["bandstop_filter"] = False
     settings["preprocessing_filter"]["bandpass_filter"] = True
     settings["preprocessing_filter"]["lowpass_filter"] = False
     settings["preprocessing_filter"]["highpass_filter"] = False
 
-    settings["preprocessing_filter"]["bandpass_filter_settings"][
-        "frequency_low_hz"
-    ] = 100
+    settings["preprocessing_filter"]["bandpass_filter_settings"]["frequency_low_hz"] = (
+        100
+    )
     settings["preprocessing_filter"]["bandpass_filter_settings"][
         "frequency_high_hz"
     ] = 160
diff --git a/tests/test_rereference.py b/tests/test_rereference.py
index 7a2144b8..64339e8a 100644
--- a/tests/test_rereference.py
+++ b/tests/test_rereference.py
@@ -1,5 +1,3 @@
-import math
-import os
 import numpy as np
 from numpy.testing import assert_allclose
 import pandas as pd
@@ -7,9 +5,6 @@
 
 from py_neuromodulation.nm_rereference import ReReferencer
 from py_neuromodulation import (
-    nm_generator,
-    nm_settings,
-    nm_IO,
     nm_define_nmchannels,
 )
 
@@ -66,8 +61,7 @@ def test_rereference_car(setup_databatch):
             ref_dat[ecog_ch_idx, :],
             data_batch[ecog_ch_idx, :]
             - data_batch[
-                (nm_channels["type"] == "ecog")
-                & (nm_channels.index != ecog_ch_idx)
+                (nm_channels["type"] == "ecog") & (nm_channels.index != ecog_ch_idx)
             ].mean(axis=0),
         )
 
diff --git a/tests/test_settings_change_after_init.py b/tests/test_settings_change_after_init.py
index 9a70a9b9..d91b53df 100644
--- a/tests/test_settings_change_after_init.py
+++ b/tests/test_settings_change_after_init.py
@@ -1,7 +1,6 @@
 import numpy as np
 
 import py_neuromodulation as nm
-from py_neuromodulation import nm_settings
 
 
 def test_post_init_nm_channels_change():
@@ -34,9 +33,7 @@ def test_post_init_nm_channels_used_channels_change_single_channel():
 
     features = stream.run(data)
 
-    chs_not_used = stream.nm_channels[stream.nm_channels["used"] == 0][
-        "new_name"
-    ]
+    chs_not_used = stream.nm_channels[stream.nm_channels["used"] == 0]["new_name"]
 
     assert (
         np.sum(
@@ -61,9 +58,7 @@ def test_post_init_nm_channels_used_channels_change_multiple_channel():
 
     features = stream.run(data)
 
-    chs_not_used = stream.nm_channels[stream.nm_channels["used"] == 0][
-        "new_name"
-    ]
+    chs_not_used = stream.nm_channels[stream.nm_channels["used"] == 0]["new_name"]
 
     assert (
         np.sum(
diff --git a/tests/test_sharpwave.py b/tests/test_sharpwave.py
index 55a775a4..8e5b25fd 100644
--- a/tests/test_sharpwave.py
+++ b/tests/test_sharpwave.py
@@ -11,94 +11,66 @@ def init_sw_settings() -> dict:
 
 
 def test_sharpwaveinit_wrong_peak_param():
-
     settings = init_sw_settings()
     with pytest.raises(Exception) as e_info:
-
-        settings["sharpwave_analysis_settings"]["sharpwave_features"][
-            "peak_left"
-        ] = 5
-        nm_sharpwaves.SharpwaveAnalyzer.test_settings(
-            settings, ch_names=[], sfreq=1000
-        )
+        settings["sharpwave_analysis_settings"]["sharpwave_features"]["peak_left"] = 5
+        nm_sharpwaves.SharpwaveAnalyzer.test_settings(settings, ch_names=[], sfreq=1000)
 
 
 def test_sharpwaveinit_wrong_filter_range():
     settings = init_sw_settings()
     with pytest.raises(Exception) as e_info:
-        settings["sharpwave_analysis_settings"]["filter_ranges_hz"] = [
-            [5, 1200]
-        ]
-        nm_sharpwaves.SharpwaveAnalyzer.test_settings(
-            settings, ch_names=[], sfreq=1000
-        )
+        settings["sharpwave_analysis_settings"]["filter_ranges_hz"] = [[5, 1200]]
+        nm_sharpwaves.SharpwaveAnalyzer.test_settings(settings, ch_names=[], sfreq=1000)
 
 
 def test_sharpwaveinit_missing_estimator():
     settings = init_sw_settings()
     with pytest.raises(Exception) as e_info:
-
-        settings["sharpwave_analysis_settings"]["sharpwave_features"][
-            "prominence"
-        ] = True
+        settings["sharpwave_analysis_settings"]["sharpwave_features"]["prominence"] = (
+            True
+        )
         settings["sharpwave_analysis_settings"]["estimator"]["mean"] = []
         settings["sharpwave_analysis_settings"]["estimator"]["median"] = []
         settings["sharpwave_analysis_settings"]["estimator"]["max"] = []
         settings["sharpwave_analysis_settings"]["estimator"]["min"] = []
         settings["sharpwave_analysis_settings"]["estimator"]["var"] = []
 
-        nm_sharpwaves.SharpwaveAnalyzer.test_settings(
-            settings, ch_names=[], sfreq=1000
-        )
+        nm_sharpwaves.SharpwaveAnalyzer.test_settings(settings, ch_names=[], sfreq=1000)
 
 
 def test_sharpwaveinit_correct_featurelist():
     settings = init_sw_settings()
-    settings["sharpwave_analysis_settings"]["sharpwave_features"][
-        "prominence"
-    ] = True
-    settings["sharpwave_analysis_settings"]["sharpwave_features"][
-        "interval"
-    ] = True
-    settings["sharpwave_analysis_settings"]["sharpwave_features"][
-        "sharpness"
-    ] = True
+    settings["sharpwave_analysis_settings"]["sharpwave_features"]["prominence"] = True
+    settings["sharpwave_analysis_settings"]["sharpwave_features"]["interval"] = True
+    settings["sharpwave_analysis_settings"]["sharpwave_features"]["sharpness"] = True
     settings["sharpwave_analysis_settings"]["estimator"]["mean"] = [
         "prominence",
         "interval",
     ]
-    settings["sharpwave_analysis_settings"]["estimator"]["median"] = [
-        "sharpness"
-    ]
+    settings["sharpwave_analysis_settings"]["estimator"]["median"] = ["sharpness"]
     settings["sharpwave_analysis_settings"]["estimator"]["max"] = []
     settings["sharpwave_analysis_settings"]["estimator"]["min"] = []
     settings["sharpwave_analysis_settings"]["estimator"]["var"] = []
 
     assert (
-        nm_sharpwaves.SharpwaveAnalyzer.test_settings(
-            settings, ch_names=[], sfreq=1000
-        )
+        nm_sharpwaves.SharpwaveAnalyzer.test_settings(settings, ch_names=[], sfreq=1000)
         == None
     )
 
 
 def test_prominence_features():
-
     settings = init_sw_settings()
     sfreq = 1000
     ch_names = ["ch1", "ch2", "ch3", "ch4"]
 
     # Reset feataures
-    for f in settings["sharpwave_analysis_settings"][
-        "sharpwave_features"
-    ].keys():
+    for f in settings["sharpwave_analysis_settings"]["sharpwave_features"].keys():
         settings["sharpwave_analysis_settings"]["sharpwave_features"][f] = False
     for est in settings["sharpwave_analysis_settings"]["estimator"].keys():
         settings["sharpwave_analysis_settings"]["estimator"][est] = []
 
-    settings["sharpwave_analysis_settings"]["sharpwave_features"][
-        "prominence"
-    ] = True
+    settings["sharpwave_analysis_settings"]["sharpwave_features"]["prominence"] = True
     settings["sharpwave_analysis_settings"]["estimator"]["max"] = ["prominence"]
 
     settings["sharpwave_analysis_settings"]["filter_ranges_hz"] = [[5, 80]]
@@ -122,22 +94,17 @@ def test_prominence_features():
 
 
 def test_interval_feature():
-
     settings = init_sw_settings()
     sfreq = 1000
     ch_names = ["ch1", "ch2", "ch3", "ch4"]
 
     # Reset feataures
-    for f in settings["sharpwave_analysis_settings"][
-        "sharpwave_features"
-    ].keys():
+    for f in settings["sharpwave_analysis_settings"]["sharpwave_features"].keys():
         settings["sharpwave_analysis_settings"]["sharpwave_features"][f] = False
     for est in settings["sharpwave_analysis_settings"]["estimator"].keys():
         settings["sharpwave_analysis_settings"]["estimator"][est] = []
 
-    settings["sharpwave_analysis_settings"]["sharpwave_features"][
-        "interval"
-    ] = True
+    settings["sharpwave_analysis_settings"]["sharpwave_features"]["interval"] = True
     settings["sharpwave_analysis_settings"]["estimator"]["max"] = ["interval"]
 
     # the filter cannot be too high, since adjacent ripples will be detected as peaks
diff --git a/tests/test_target_channel_add.py b/tests/test_target_channel_add.py
index 3f077d44..74c92a8b 100644
--- a/tests/test_target_channel_add.py
+++ b/tests/test_target_channel_add.py
@@ -1,19 +1,15 @@
 import numpy as np
 import pandas as pd
-import pytest
 
 from py_neuromodulation import (
     nm_settings,
     nm_stream_offline,
     nm_define_nmchannels,
-    nm_stream_abc,
 )
 
 
-def get_example_settings(test_arr: np.array) -> nm_stream_abc.PNStream:
-    settings = nm_settings.set_settings_fast_compute(
-        nm_settings.get_default_settings()
-    )
+def get_example_settings(test_arr: np.ndarray) -> tuple[dict, pd.DataFrame]:
+    settings = nm_settings.set_settings_fast_compute(nm_settings.get_default_settings())
 
     nm_channels = nm_define_nmchannels.get_default_channels_from_data(test_arr)
 
@@ -97,4 +93,4 @@ def test_label_add_no_target():
 
     df = stream.run(arr_test)
 
-    assert all([col.startswith("ch") or col.startswith("time") for col in df])
+    assert all([col.startswith("ch") or col.startswith("time") for col in df.columns])