Repository: neuromodulation/py_neuromodulation (language: python)

Issue:
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)

Fix type hints, clean imports,  and other errors reported by static analysis
So in regards to #303 I passed 3 different static analyzers over the codebase (Ruff, PyRight and Mypy) and fixed a bunch of stuff of varying grades of relevance. The goals were:
- To overall improve the consistency of the code and adherence to latest standards
- To make sure default values for arguments are handled correctly (e.g. in the case of None defaults)
- To clean up error reports of trivial warnings and flags so that potential serious errors are easier to spot in future static analyses
- To perhaps improve startup time by cleaning unused imports

In terms of results, they were:
- For Mypy, going from 272 errors to 24 errors. These 24 errors are now things that actually require fixing, see report file below generated by this command:
```mypy --follow-imports skip --exclude nm_cohortwrapper\.py --exclude nm_across_patient_decoding\.py --exclude nm_eval_timing\.py --exclude nm_nolds\.py  --exclude FieldTrip\.py --exclude nm_RMAP\.py --exclude nm_decode\.py  py_neuromodulation\```

[mypy_report_23_04_24.txt](https://github.com/neuromodulation/py_neuromodulation/files/15074254/mypy_report_23_04_24.txt)

- For PyRight, from [287 errors, 2 warnings] to [168 errors, 0 warnings]
[pyright_types.txt](https://github.com/neuromodulation/py_neuromodulation/files/15075289/pyright_types.txt)

A non-exhaustive but quite complete list of the changes done are:
- Remove unused imports
- Fix incorrect type typehints (e.g. bool = None, or np.array instead of np.ndarray)
- Change Optional and Union typehints to modern style i.e. list[str|None] or int | None
- Add comments to potential bugs, check commit (e.g. duplicated "s" argument to axis.scatter)
- Fix empty error raises with corresponding errors
- Commented out variables that were created but never used, feel free to remove
- Commented out class-level variables that were intended to be instance-level variables (i.e. variables defined at top-indentation level within a class and without self. prefix)
- And a bit of perhaps a controversial one: Favor empty lists and empty strings as default values for arguments as opposed to None values. Reasons are several: 
    - static analysis complains a lot about None obviously not having any methods or being passable to functions. 
    - it generates a lot of type analysis cascades of errors (e.g. when the input to a function can be str | None but that input is passed to a function that only accepts str and doesn't even handle the None case)
    - If i'm not mistaken, passing empty lists, arrays or strings is never an expected input for the functions where I changed this, so handling the empty case instead of the None case should not generate any unexpected behaviours

I would understand if the last change is not accepted, for me it seems more intuitive to check for empty inputs or sentinel values rather than Python NoneType, but it's a matter of preference at the end of the day.



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 33bff4b8..627119fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,6 @@ __pycache__/
 *.py[cod]
 *$py.class
 
-# C extensions
-*.so
-
 # Distribution / packaging
 .pypirc/
 .Python
@@ -160,9 +157,12 @@ sub/
 
 py_neuromodulation/data/derivatives/*
 
+.python-version
+
+py_neuromodulation/ConnectivityDecoding/connectome_folder/*
+
 plot_0_first_demo.py
+plot_1_example_BIDS.py
 
 requirements.lock
-requirements-dev.lock
-
-py_neuromodulation/ConnectivityDecoding/connectome_struct.mat
+requirements-dev.lock
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 427403c9..fbe38084 100644
--- a/README.rst
+++ b/README.rst
@@ -40,22 +40,10 @@ py_neuromodulation requires at least python 3.10. For installation you can use p
 
     pip install py-neuromodulation
 
-We recommend however installing the package using `rye <https://rye-up.com/guide/installation/>`_:
-
-.. code-block::
-
-    git clone https://github.com/neuromodulation/py_neuromodulation.git
-    rye pin 3.11
-    rye sync
-
-And then activating the virtual environment e.g. in Windows using:
+Alternatively you can also install the package in a conda environment:
 
 .. code-block::
 
-    .\.venv\Scripts\activate
-
-Alternatively you can also install the package in a conda environment:
-
     conda create -n pynm-test python=3.11
     conda activate pynm-test
 
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..103694ed 100644
--- a/examples/plot_2_example_add_feature.py
+++ b/examples/plot_2_example_add_feature.py
@@ -6,7 +6,7 @@
 """
 
 import py_neuromodulation as nm
-from py_neuromodulation import nm_features_abc
+from py_neuromodulation.nm_features import NMFeature
 import numpy as np
 from typing import Iterable
 
@@ -25,14 +25,14 @@
 )
 
 
-class NewFeature(nm_features_abc.Feature):
+class NewFeature(NMFeature):
     def __init__(
         self, settings: dict, ch_names: Iterable[str], sfreq: float
     ) -> None:
         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/liblsl/liblsl.1.16.0.dylib b/liblsl/liblsl.1.16.0.dylib
new file mode 100644
index 00000000..26a46314
Binary files /dev/null and b/liblsl/liblsl.1.16.0.dylib differ
diff --git a/liblsl/liblsl.so.1.16.2 b/liblsl/liblsl.so.1.16.2
new file mode 100644
index 00000000..93b4f1d1
Binary files /dev/null and b/liblsl/liblsl.so.1.16.2 differ
diff --git a/liblsl/libpugixml.so.1.12 b/liblsl/libpugixml.so.1.12
new file mode 100644
index 00000000..b1f124d6
Binary files /dev/null and b/liblsl/libpugixml.so.1.12 differ
diff --git a/liblsl/lsl.dll b/liblsl/lsl.dll
new file mode 100644
index 00000000..cef9a597
Binary files /dev/null and b/liblsl/lsl.dll differ
diff --git a/py_neuromodulation/ConnectivityDecoding/Automated Anatomical Labeling 3 (Rolls 2020).nii b/py_neuromodulation/ConnectivityDecoding/Automated Anatomical Labeling 3 (Rolls 2020).nii
new file mode 100644
index 00000000..edfda51b
Binary files /dev/null and b/py_neuromodulation/ConnectivityDecoding/Automated Anatomical Labeling 3 (Rolls 2020).nii differ
diff --git a/py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m b/py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m
new file mode 100644
index 00000000..8dcc16b6
--- /dev/null
+++ b/py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m
@@ -0,0 +1,34 @@
+addpath('C:\code\wjn_toolbox');
+addpath(genpath('C:\code\leaddbs'));
+addpath(genpath('C:\code\spm12'));
+
+
+%%
+ctx = wjn_mni_cortex();
+downsample_ctx=ctx.vertices(1:20:end,:); %downsample by 10
+
+save("downsampled_cortex.mat", "downsample_ctx")
+
+figure;
+scatter3(downsample_ctx(:,1), downsample_ctx(:,2), downsample_ctx(:,3), 'filled');
+title('3D Scatter Plot Example');
+xlabel('X-axis');
+ylabel('Y-axis');
+zlabel('Z-axis');
+grid on;
+
+
+
+PATH_OUT = "D:\Connectome_RMAP_OUT\ROIs";
+
+for a =1:size(downsample_ctx,1)
+    disp(a)
+    roiname  = fullfile(PATH_OUT, strcat('ROI-', string(a), '.nii'));
+    mni = [downsample_ctx(a, 1) downsample_ctx(a, 2) downsample_ctx(a, 3)];
+    wjn_spherical_roi(roiname,mni,4);
+end
+
+
+
+
+
diff --git a/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py b/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py
new file mode 100644
index 00000000..fa97e424
--- /dev/null
+++ b/py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py
@@ -0,0 +1,95 @@
+import nibabel as nib
+import numpy as np
+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.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)
+
+        x_dim, y_dim, z_dim = self.data.shape
+
+        # Create arrays of voxel coordinates
+        x_coords, y_coords, z_coords = np.meshgrid(
+            range(x_dim), range(y_dim), range(z_dim), indexing="ij"
+        )
+
+        # Downsample here the voxels --> check lateron if the voxels have non-zero values
+        x_c_flatten = x_coords.flatten()[::resampling_factor]
+        y_c_flatten = y_coords.flatten()[::resampling_factor]
+        z_c_flatten = z_coords.flatten()[::resampling_factor]
+
+        # Combine coordinates into a single array
+        voxel_coordinates = np.column_stack(
+            (
+                x_c_flatten,
+                y_c_flatten,
+                z_c_flatten,
+                np.ones(x_c_flatten.shape[0]),
+            )
+        )
+
+        aff_m = self.img.affine
+        aff_m[0, 0] = 2
+        aff_m[0, 3] = -90
+
+        mni_coordinates = np.dot(aff_m, voxel_coordinates.T).T[:, :3]
+
+        return mni_coordinates
+
+    def select_non_zero_voxels(
+        self,
+        mni_coordinates: np.ndarray,
+    ):
+        coords = np.hstack((mni_coordinates, np.ones((mni_coordinates.shape[0], 1))))
+
+        # and transform back to get the voxel values
+        voxels_downsampled = np.array(
+            np.linalg.solve(self.img.affine, coords.T).T
+        ).astype(int)[:, :3]
+
+        ival = []
+        coord_ = []
+        for i in range(voxels_downsampled.shape[0]):
+            ival.append(self.data[tuple(voxels_downsampled[i, :])])
+            coord_.append(mni_coordinates[i, :])
+
+        # get only voxel values non-zero
+        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.ndarray):
+        fig = plt.figure()
+        ax = fig.add_subplot(111, projection="3d")
+        ax.scatter(
+            coord_non_zero[:, 0],
+            coord_non_zero[:, 1],
+            coord_non_zero[:, 2],
+            s=50,
+            alpha=0.2,
+        )
+        plt.show()
+
+
+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)
diff --git a/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py b/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py
new file mode 100644
index 00000000..2238ff6b
--- /dev/null
+++ b/py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py
@@ -0,0 +1,107 @@
+import nibabel as nib
+import numpy as np
+import scipy.io as sio
+import os
+
+
+def write_connectome_mat(
+    PATH_Fingerprints: str = r"D:\Connectome_RMAP_OUT\ROIs\HCP1000 6K",
+    PATH_CONNECTOME: str = os.path.join(
+        "py_neuromodulation",
+        "ConnectivityDecoding",
+        "connectome_struct.mat",
+    ),
+    func_: bool = False,
+):
+    # connectome = sio.loadmat(PATH_CONNECTOME)  # check if read was successful
+
+    # load all fingerprints and put them in .npy
+    dict_connectome = {}
+    if func_ is False:
+        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
+        ]
+
+    # I except 1025 files, check which ones are missing
+    missing_files = []
+
+    for i in range(1, 1026):
+        MISSING = False
+
+        if func_ is False:
+            if f"ROI-{i}_struc_seed.nii" not in files_fps:
+                missing_files.append(f"ROI-{i}_struc_seed.nii")
+                MISSING = True
+        else:
+            if f"ROI-{i}_func_seed_AvgR_Fz.nii" not in files_fps:
+                missing_files.append(f"ROI-{i}_func_seed_AvgR_Fz.nii")
+                MISSING = True
+
+        if MISSING:
+            ROI_file = os.path.join(
+                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"
+            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)
+        if "struc" in f:
+            dict_connectome[f[f.find("ROI-") + 4 : f.find("_struc")]] = fp
+        else:
+            dict_connectome[
+                f[f.find("ROI-") + 4 : f.find("_func_seed_AvgR_Fz.nii")]
+            ] = fp
+
+        print(idx)
+    # save the dictionary
+    sio.savemat(
+        PATH_CONNECTOME,
+        dict_connectome,
+    )
+
+
+if __name__ == "__main__":
+    write_connectome_mat(
+        PATH_Fingerprints=r"D:\Connectome_RMAP_OUT\whole_brain\struc\HCP1000 6K",
+        PATH_CONNECTOME=os.path.join(
+            "py_neuromodulation",
+            "ConnectivityDecoding",
+            "connectome_whole_brain_struc.mat",
+        ),
+    )  # 58 files are missing
+
+    write_connectome_mat(
+        PATH_Fingerprints=r"D:\Connectome_RMAP_OUT\whole_brain\func",
+        PATH_CONNECTOME=os.path.join(
+            "py_neuromodulation",
+            "ConnectivityDecoding",
+            "connectome_whole_brain_func.mat",
+        ),
+        func_=True,
+    )
+
+    write_connectome_mat(
+        PATH_Fingerprints=r"D:\Connectome_RMAP_OUT\hull\func\GSP 1000 (Yeo 2011)_Full Set (Yeo 2011)",
+        PATH_CONNECTOME=os.path.join(
+            "py_neuromodulation",
+            "ConnectivityDecoding",
+            "connectome_hull_func.mat",
+        ),
+        func_=True,
+    )  # all there
+
+    write_connectome_mat(
+        PATH_Fingerprints=r"D:\Connectome_RMAP_OUT\hull\struc\HCP1000 6K",
+        PATH_CONNECTOME=os.path.join(
+            "py_neuromodulation",
+            "ConnectivityDecoding",
+            "connectome_hull_struc.mat",
+        ),
+    )  # 5 missing
diff --git a/py_neuromodulation/ConnectivityDecoding/mni_coords_cortical_surface.mat b/py_neuromodulation/ConnectivityDecoding/mni_coords_cortical_surface.mat
new file mode 100644
index 00000000..14f020a1
Binary files /dev/null and b/py_neuromodulation/ConnectivityDecoding/mni_coords_cortical_surface.mat differ
diff --git a/py_neuromodulation/ConnectivityDecoding/mni_coords_whole_brain.mat b/py_neuromodulation/ConnectivityDecoding/mni_coords_whole_brain.mat
new file mode 100644
index 00000000..0bdd1943
Binary files /dev/null and b/py_neuromodulation/ConnectivityDecoding/mni_coords_whole_brain.mat differ
diff --git a/py_neuromodulation/ConnectivityDecoding/rmap_func_all.nii b/py_neuromodulation/ConnectivityDecoding/rmap_func_all.nii
new file mode 100644
index 00000000..070d6af0
Binary files /dev/null and b/py_neuromodulation/ConnectivityDecoding/rmap_func_all.nii differ
diff --git a/py_neuromodulation/ConnectivityDecoding/rmap_struc.nii b/py_neuromodulation/ConnectivityDecoding/rmap_struc.nii
new file mode 100644
index 00000000..539cce69
Binary files /dev/null and b/py_neuromodulation/ConnectivityDecoding/rmap_struc.nii differ
diff --git a/py_neuromodulation/__init__.py b/py_neuromodulation/__init__.py
index baa54f0b..f34e8b04 100644
--- a/py_neuromodulation/__init__.py
+++ b/py_neuromodulation/__init__.py
@@ -1,13 +1,26 @@
-from . import (
-    nm_analysis,
-    nm_stream_abc,
-    nm_cohortwrapper,
-    nm_across_patient_decoding,
-    nm_stream_offline,
-    nm_settings,
-    nm_define_nmchannels,
-)
+import os
+import platform
+from pathlib import PurePath
+from importlib.metadata import version
+from .nm_logger import NMLogger
+import matplotlib
+
+matplotlib.use("tkagg")  # Set matplotlib backend to TkAgg (Qt backend crashes)
+
+__version__ = version(__package__)  # get version from pyproject.toml
+
+PYNM_DIR = PurePath(__file__).parent  # Define constant for py_nm directory
+
+logger = NMLogger(__name__)  # logger initialization first to prevent circular import
+
+# Set  environment variable MNE_LSL_LIB (required to import Stream below)
+LSL_DICT = {
+    "Windows": "lsl.dll",
+    "Linux": "liblsl.so.1.16.2",
+    "Darwin": "liblsl.1.16.0.dylib",
+}
+os.environ["MNE_LSL_LIB"] = f"{PYNM_DIR.parent}/liblsl/{LSL_DICT[platform.system()]}"
+
+# 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 821c17c2..758f394b 100644
--- a/py_neuromodulation/nm_RMAP.py
+++ b/py_neuromodulation/nm_RMAP.py
@@ -1,17 +1,285 @@
-import enum
-import nibabel as nib
 import numpy as np
-import os
+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
+import nibabel as nib
+from matplotlib import pyplot as plt
+
+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 = [
+    1,
+    8,
+    16,
+    33,
+    34,
+    35,
+    36,
+    37,
+    51,
+    75,
+    77,
+    78,
+    99,
+    109,
+    115,
+    136,
+    155,
+    170,
+    210,
+    215,
+    243,
+    352,
+    359,
+    361,
+    415,
+    416,
+    422,
+    529,
+    567,
+    569,
+    622,
+    623,
+    625,
+    627,
+    632,
+    633,
+    634,
+    635,
+    639,
+    640,
+    641,
+    643,
+    644,
+    650,
+    661,
+    663,
+    667,
+    683,
+    684,
+    685,
+    704,
+    708,
+    722,
+    839,
+    840,
+    905,
+    993,
+    1011,
+]
+
+
+class ConnectivityChannelSelector:
+    def __init__(
+        self,
+        whole_brain_connectome: bool = True,
+        func_connectivity: bool = True,
+    ) -> None:
+        """ConnectivityChannelSelector
+
+        Parameters
+        ----------
+        whole_brain_connectome : bool, optional
+            if True a 1236 whole-brain point grid is chosen,
+            if False, a 1025 point grid of the cortical hull is loaded,
+            by default True
+        func_connectivity : bool, optional
+            if true, functional connectivity fMRI is loaded,
+            if false structural dMRIby, default True
+        """
+
+        self.connectome_name = self._get_connectome_name(
+            whole_brain_connectome, func_connectivity
+        )
+
+        self.whole_brain_connectome = whole_brain_connectome
+        self.func_connectivity = func_connectivity
+
+        self.PATH_CONN_DECODING = PYNM_DIR / "ConnectivityDecoding"
+
+        if whole_brain_connectome:
+            self.PATH_GRID = PurePath(
+                self.PATH_CONN_DECODING,
+                "mni_coords_whole_brain.mat",
+            )
+            self.grid = sio.loadmat(self.PATH_GRID)["downsample_ctx"]
+            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,
+                    LIST_STRUC_UNCONNECTED_GRIDPOINTS_WHOLEBRAIN,
+                    axis=0,
+                )
+        else:
+            self.PATH_GRID = PurePath(
+                self.PATH_CONN_DECODING,
+                "mni_coords_cortical_surface.mat",
+            )
+            self.grid = sio.loadmat(self.PATH_GRID)["downsample_ctx"]
+            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
+                )
+
+        if func_connectivity:
+            self.RMAP_arr = nib.load(
+                PurePath(self.PATH_CONN_DECODING, "RMAP_func_all.nii")
+            ).get_fdata()
+        else:
+            self.RMAP_arr = nib.load(
+                PurePath(self.PATH_CONN_DECODING, "RMAP_struc.nii")
+            ).get_fdata()
+
+    def _get_connectome_name(self, whole_brain_connectome: str, func_connectivity: str):
+        connectome_name = "connectome_"
+        if whole_brain_connectome:
+            connectome_name += "whole_brain_"
+        else:
+            connectome_name += "hull_"
+        if func_connectivity:
+            connectome_name += "func"
+        else:
+            connectome_name += "struc"
+        return connectome_name
+
+    def get_available_connectomes(self) -> list:
+        """Return list of saved connectomes in the
+        package folder/ConnectivityDecoding/connectome_folder/ folder.
+
+        Returns
+        -------
+        list_connectomes: list
+        """
+        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)
+        plt.show()
+
+    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.ndarray
+            MNI coordinates with shape (num_channels, 3)
+
+        Returns
+        -------
+        Tuple[list, list]
+            Grid coordinates, grid indices
+        """
+
+        idx_ = []
+        for c in coord:
+            dist = np.linalg.norm(self.grid - c, axis=1)
+            idx_.append(np.argmin(dist))
+
+        return [self.grid[idx] for idx in idx_], idx_
+
+    def get_rmap_correlations(
+        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.ndarray, optional
+            Passed RMAP, by default None
+
+        Returns
+        -------
+        List
+            correlation values
+        """
+
+        RMAP_ = self.RMAP_arr if RMAP_use is None else RMAP_use
+        RMAP_ = RMAP_.flatten()
+        corrs = []
+        for fp in fps:
+            corrs.append(np.corrcoef(RMAP_, fp.flatten())[0][1])
+        return corrs
+
+    def load_connectome(
+        self,
+        whole_brain_connectome: bool | None = None,
+        func_connectivity: bool | None = None,
+    ) -> None:
+        """Load connectome, if not available download connectome from
+        Zenodo.
+
+        Parameters
+        ----------
+        whole_brain_connectome : bool, optional
+            if true whole brain connectome
+            if false cortical hull grid connectome, by default None
+        func_connectivity : bool, optional
+            if true fMRI if false dMRI, by default None
+        """
+
+        if whole_brain_connectome is not None:
+            self.whole_brain_connectome = whole_brain_connectome
+        if func_connectivity is not None:
+            self.func_connectivity = func_connectivity
+
+        self.connectome_name = self._get_connectome_name(
+            self.whole_brain_connectome, self.func_connectivity
+        )
+
+        PATH_CONNECTOME = Path(
+            self.PATH_CONN_DECODING,
+            "connectome_folder",
+            self.connectome_name + ".mat",
+        )
+
+        if not PATH_CONNECTOME.exists():
+            user_input = input(
+                "Do you want to download the connectome? (yes/no): "
+            ).lower()
+            if user_input == "yes":
+                self._download_connectome()
+            elif user_input == "no":
+                print("Connectome missing, has to be downloaded")
+
+        self.connectome = sio.loadmat(PATH_CONNECTOME)
+
+    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(
+        self,
+    ):
+        # download the connectome from the Zenodo API
+        print("Downloading the connectome...")
 
-from py_neuromodulation import nm_plots
+        record_id = "10804702"
+        file_name = self.connectome_name
 
+        filepath = Path(self.PATH_CONN_DECODING, "connectome_folder")
+        filepath.mkdir(parents=True, exist_ok=True)
 
-class RMAPChannelSelector:
+        urlretrieve(
+            f"https://zenodo.org/api/records/{record_id}/files/{file_name}/content",
+            filepath / f"{self.connectome_name}.mat",
+        )
 
+
+class RMAPCross_Val_ChannelSelector:
     def __init__(self) -> None:
         pass
 
@@ -22,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")
 
@@ -76,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)
@@ -119,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 = []
@@ -144,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])
@@ -163,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])
@@ -197,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}",
@@ -217,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])
 
@@ -227,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
@@ -235,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..2bf8c7ad 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 import NMFeature
 
 
-class Bispectra(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: int | float
-    ) -> None:
+class Bispectra(NMFeature):
+    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 4b6e7096..61779191 100644
--- a/py_neuromodulation/nm_bursts.py
+++ b/py_neuromodulation/nm_bursts.py
@@ -1,44 +1,42 @@
-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 import NMFeature
+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(NMFeature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
         self.s = settings
         self.sfreq = sfreq
         self.ch_names = ch_names
         self.threshold = self.s["burst_settings"]["threshold"]
         self.time_duration_s = self.s["burst_settings"]["time_duration_s"]
+        self.samples_overlap = int(
+            self.sfreq
+            * (self.s["segment_length_features_ms"] / 1000)
+            / self.s["sampling_rate_features_hz"]
+        )
 
         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,
@@ -48,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)
@@ -78,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,
@@ -93,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):
@@ -105,11 +101,15 @@ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
                     self.data_buffer[ch_name][fband_name] = new_dat
                 else:
                     self.data_buffer[ch_name][fband_name] = np.concatenate(
-                        (self.data_buffer[ch_name][fband_name], new_dat), axis=0
+                        (
+                            self.data_buffer[ch_name][fband_name],
+                            new_dat[-self.samples_overlap :],
+                        ),
+                        axis=0,
                     )[-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
                 )
 
@@ -117,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
@@ -152,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
         """
@@ -170,15 +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):
+        for i in range(burst_time_points.size // 2):
             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]])
+            burst_amplitude.append(
+                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..8d0b4505 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 import NMFeature
+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(NMFeature):
+    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..06420f04 100644
--- a/py_neuromodulation/nm_features.py
+++ b/py_neuromodulation/nm_features.py
@@ -1,28 +1,70 @@
-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 typing import Protocol, TYPE_CHECKING
+from collections.abc import Iterable
 
+if TYPE_CHECKING:
+    import numpy as np
 
-class Features:
-    """Class for calculating features."""
+from py_neuromodulation.nm_types import ImportDetails, get_class
 
-    features: list[nm_features_abc.Feature] = []
 
+class NMFeature(Protocol):
     def __init__(
-        self, s: dict, ch_names: list[str], sfreq: int | float
+        self, settings: dict, ch_names: Iterable[str], sfreq: int | float
     ) -> None:
+        """Method to check passed settings"""
+
+    @staticmethod
+    def test_settings(
+        settings: dict,
+        ch_names: Iterable[str],
+        sfreq: int | float,
+    ):
+        """Method to check passed settings"""
+
+    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.ndarray'
+            (channels, time)
+        features_compute : dict
+        ch_names : Iterable[str]
+
+        Returns
+        -------
+        dict
+        """
+        ...
+
+
+FEATURE_DICT: dict[str, ImportDetails] = {
+    "raw_hjorth": ImportDetails("py_neuromodulation.nm_hjorth_raw", "Hjorth"),
+    "return_raw": ImportDetails("py_neuromodulation.nm_hjorth_raw", "Raw"),
+    "bandpass_filter": ImportDetails("py_neuromodulation.nm_oscillatory", "BandPower"),
+    "stft": ImportDetails("py_neuromodulation.nm_oscillatory", "STFT"),
+    "fft": ImportDetails("py_neuromodulation.nm_oscillatory", "FFT"),
+    "welch": ImportDetails("py_neuromodulation.nm_oscillatory", "Welch"),
+    "sharpwave_analysis": ImportDetails(
+        "py_neuromodulation.nm_sharpwaves", "SharpwaveAnalyzer"
+    ),
+    "fooof": ImportDetails("py_neuromodulation.nm_fooof", "FooofAnalyzer"),
+    "nolds": ImportDetails("py_neuromodulation.nm_nolds", "Nolds"),
+    "coherence": ImportDetails("py_neuromodulation.nm_coherence", "NM_Coherence"),
+    "bursts": ImportDetails("py_neuromodulation.nm_bursts", "Burst"),
+    "linelength": ImportDetails("py_neuromodulation.nm_linelength", "LineLength"),
+    "mne_connectivity": ImportDetails(
+        "py_neuromodulation.nm_mne_connectivity", "MNEConnectivity"
+    ),
+    "bispectrum": ImportDetails("py_neuromodulation.nm_bispectra", "Bispectra"),
+}
+
+
+class Features:
+    """Class for calculating features.p"""
+
+    def __init__(self, settings: dict, ch_names: list[str], sfreq: float) -> None:
         """_summary_
 
         Parameters
@@ -31,7 +73,7 @@ def __init__(
             _description_
         ch_names : list[str]
             _description_
-        sfreq : int | float
+        sfreq : float
             _description_
 
         Raises
@@ -40,48 +82,20 @@ def __init__(
             _description_
         """
 
-        self.features = []
-
-        for feature in s["features"]:
-            if s["features"][feature] is False:
-                continue
-            match feature:
-                case "raw_hjorth":
-                    FeatureClass = nm_hjorth_raw.Hjorth
-                case "return_raw":
-                    FeatureClass = nm_hjorth_raw.Raw
-                case "bandpass_filter":
-                    FeatureClass = nm_oscillatory.BandPower
-                case "stft":
-                    FeatureClass = nm_oscillatory.STFT
-                case "fft":
-                    FeatureClass = nm_oscillatory.FFT
-                case "welch":
-                    FeatureClass = nm_oscillatory.Welch
-                case "sharpwave_analysis":
-                    FeatureClass = nm_sharpwaves.SharpwaveAnalyzer
-                case "fooof":
-                    FeatureClass = nm_fooof.FooofAnalyzer
-                case "nolds":
-                    FeatureClass = nm_nolds.Nolds
-                case "coherence":
-                    FeatureClass = nm_coherence.NM_Coherence
-                case "bursts":
-                    FeatureClass = nm_bursts.Burst
-                case "linelength":
-                    FeatureClass = nm_linelength.LineLength
-                case "mne_connectivity":
-                    FeatureClass = nm_mne_connectivity.MNEConnectivity
-                case "bispectrum":
-                    FeatureClass = nm_bispectra.Bispectra
-                case _:
-                    raise ValueError(f"Unknown feature found. Got: {feature}.")
-
-            FeatureClass.test_settings(s, ch_names, sfreq)
-            f_obj = FeatureClass(s, ch_names, sfreq)
-            self.features.append(f_obj)
-
-    def register_new_feature(self, feature: nm_features_abc.Feature) -> None:
+        for feature_name in settings["features"]:
+            if feature_name not in FEATURE_DICT:
+                raise ValueError(f"Unknown feature found. Got: {feature_name}.")
+
+        self.features: list[NMFeature] = [
+            get_class(FEATURE_DICT[feature_name])(settings, ch_names, sfreq)
+            for feature_name, feature_enabled in settings["features"].items()
+            if feature_enabled
+        ]
+        
+        for feature in self.features:
+            feature.test_settings(settings, ch_names, sfreq)
+
+    def register_new_feature(self, feature: NMFeature) -> None:
         """Register new feature.
 
         Parameters
@@ -91,7 +105,7 @@ def register_new_feature(self, feature: nm_features_abc.Feature) -> None:
         """
         self.features.append(feature)
 
-    def estimate_features(self, data: np.ndarray) -> dict:
+    def estimate_features(self, data: "np.ndarray") -> dict:
         """Calculate features, as defined in settings.json
         Features are based on bandpower, raw Hjorth parameters and sharp wave
         characteristics.
@@ -105,7 +119,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
deleted file mode 100644
index 3d5143a6..00000000
--- a/py_neuromodulation/nm_features_abc.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from abc import ABC, abstractmethod
-import numpy as np
-from typing import Iterable
-
-
-class Feature(ABC):
-    @abstractmethod
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: int | float
-    ) -> None:
-        pass
-
-    @staticmethod
-    @abstractmethod
-    def test_settings(
-        settings: dict,
-        ch_names: Iterable[str],
-        sfreq: int | float,
-    ):
-        """Method to check passed settings"""
-        pass
-
-    @abstractmethod
-    def calc_feature(self, data: np.array, features_compute: dict) -> dict:
-        """
-        Feature calculation method. Each method needs to loop through all channels
-
-        Parameters
-        ----------
-        data : np.array
-            (channels, time)
-        features_compute : dict
-        ch_names : Iterable[str]
-
-        Returns
-        -------
-        dict
-        """
-        pass
diff --git a/py_neuromodulation/nm_filter.py b/py_neuromodulation/nm_filter.py
index cb4651ba..655a96b2 100644
--- a/py_neuromodulation/nm_filter.py
+++ b/py_neuromodulation/nm_filter.py
@@ -1,12 +1,11 @@
 """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.nm_preprocessing import NMPreprocessor
+from py_neuromodulation import logger
 
 
 class MNEFilter:
@@ -23,13 +22,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 +41,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 +58,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 +69,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 +108,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,28 +120,23 @@ 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
 
 
-class NotchFilter:
+class NotchFilter(NMPreprocessor):
     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 +166,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 +204,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..a9b61a77 100644
--- a/py_neuromodulation/nm_filter_preprocessing.py
+++ b/py_neuromodulation/nm_filter_preprocessing.py
@@ -1,25 +1,26 @@
 import numpy as np
 
-from py_neuromodulation import nm_filter
+from py_neuromodulation.nm_filter import MNEFilter
 
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
 
-class PreprocessingFilter:
 
-    def __init__(self, settings: dict, sfreq: int | float) -> None:
+class PreprocessingFilter(NMPreprocessor):
+    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 +28,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 +86,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..3651fc88 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 import NMFeature
+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(NMFeature):
+    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 48aad08c..27c9078c 100644
--- a/py_neuromodulation/nm_generator.py
+++ b/py_neuromodulation/nm_generator.py
@@ -1,16 +1,14 @@
-import os
+from collections.abc import Iterator
+from pathlib import Path
 import time
-import logging
-
-logger = logging.getLogger("PynmLogger")
-
-from typing import Iterator, Union
 from pynput import keyboard
 import numpy as np
-import mne
+
 from mne_lsl.player import PlayerLSL
 from mne_lsl.stream import StreamLSL
+import mne
 
+from py_neuromodulation import logger
 
 class LSLOfflinePlayer:
 
@@ -18,9 +16,9 @@ def __init__(
         self,
         settings: dict,
         stream_name: str = "example_stream",
-        f_name: str = None,
-        sfreq: Union[int, float] = None,
-        data: np.array = None,
+        f_name: str | None = None,
+        sfreq: int | float | None = None,
+        data: np.ndarray | None = None,
     ) -> None:
 
         self.settings = settings
@@ -40,11 +38,11 @@ def __init__(
 
             info = mne.create_info(
                 ch_names=[f"ch{i}" for i in range(data.shape[0])],
-                ch_types=[f"dbs" for _ in range(data.shape[0])],
+                ch_types=["dbs" for _ in range(data.shape[0])],
                 sfreq=sfreq,
             )
             raw = mne.io.RawArray(data, info)
-            self._path_raw = os.path.join(os.getcwd() + "temp_raw.fif")
+            self._path_raw = Path.cwd() / "temp_raw.fif"
             raw.save(self._path_raw, overwrite=True)
 
         self.player = PlayerLSL(
@@ -80,7 +78,7 @@ def on_press(self, key):
             self.key_pressed = True
             return False
 
-    def get_next_batch(self) -> np.array:
+    def get_next_batch(self) -> Iterator[tuple[np.ndarray, np.ndarray]]:
         self.last_time = time.time()
         check_data = None
         data = None
@@ -117,8 +115,8 @@ def get_next_batch(self) -> np.array:
 def raw_data_generator(
     data: np.ndarray,
     settings: dict,
-    sfreq: int,
-) -> Iterator[np.ndarray]:
+    sfreq: float,
+) -> Iterator[tuple[None, np.ndarray]]:
     """
     This generator function mimics online data acquisition.
     The data are iteratively sampled with sfreq_new.
@@ -127,7 +125,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: 1D array of time stamps
@@ -143,12 +141,7 @@ def raw_data_generator(
     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 (
-                np.arange(cnt - offset_start, cnt) / sfreq,  # what is this exactly? Not in main. need? 
-                data[:, np.floor(cnt - offset_start).astype(int) : cnt]
-            )
+            yield None, 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..775be560 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 import NMFeature
 
 
-class Hjorth(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class Hjorth(NMFeature):
+    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(NMFeature):
+    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..aaa51397 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 import NMFeature
 
 
-class LineLength(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
+class LineLength(NMFeature):
+    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_logger.py b/py_neuromodulation/nm_logger.py
new file mode 100644
index 00000000..17d2b4a3
--- /dev/null
+++ b/py_neuromodulation/nm_logger.py
@@ -0,0 +1,66 @@
+from pathlib import Path
+from py_neuromodulation.nm_types import _PathLike
+import logging
+
+INFOFORMAT = "%(name)s:\t%(message)s"
+DEBUGFORMAT = "%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(funcName)s:%(lineno)d:\t%(message)s"
+
+LOG_LEVELS = {
+    "DEBUG": (logging.DEBUG, DEBUGFORMAT),
+    "INFO": (logging.INFO, INFOFORMAT),
+    "WARNING": (logging.WARN, DEBUGFORMAT),
+    "ERROR": (logging.ERROR, DEBUGFORMAT),
+}
+
+
+class NMLogger(logging.Logger):
+    """
+    Subclass of logging.Logger with some extra functionality
+    """
+
+    def __init__(self, name: str, level: str = "INFO") -> None:
+        super().__init__(name, LOG_LEVELS[level][0])
+
+        self.setLevel(level)
+
+        self._console_handler = logging.StreamHandler()
+        self._console_handler.setLevel(level)
+        self._console_handler.setFormatter(logging.Formatter(LOG_LEVELS[level][1]))
+
+        self.addHandler(self._console_handler)
+
+    def set_level(self, level: str):
+        """
+        Set console logging level
+        """
+        self.setLevel(level)
+        self._console_handler.setLevel(level)
+        self._console_handler.setFormatter(logging.Formatter(LOG_LEVELS[level][1]))
+
+    def log_to_file(self, path: _PathLike, mode: str = "w"):
+        """
+        Add file handlers to the logger
+
+        Parameters
+        ----------
+        path: directory where to save logfiles
+        mode : str, ('w', 'a')
+            w: overwrite files
+            a: append to files
+        """
+
+        path = Path(path)
+        path.mkdir(parents=True, exist_ok=True)
+
+        self.debug_file_handler = logging.FileHandler(path / "logfile_pynm_debug.log")
+        self.debug_file_handler.setLevel(logging.DEBUG)
+        self.debug_file_handler.setFormatter(logging.Formatter(DEBUGFORMAT))
+
+        self.info_file_handler = logging.FileHandler(
+            path / "logfile_pynm_info.log", mode=mode
+        )
+        self.info_file_handler.setLevel(logging.INFO)
+        self.info_file_handler.setFormatter(logging.Formatter(INFOFORMAT))
+
+        self.addHandler(self.info_file_handler)
+        self.addHandler(self.debug_file_handler)
diff --git a/py_neuromodulation/nm_mne_connectivity.py b/py_neuromodulation/nm_mne_connectivity.py
index a13e4fdc..6cccb226 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 import NMFeature
 
 
-class MNEConnectivity(nm_features_abc.Feature):
+class MNEConnectivity(NMFeature):
     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..111f618e 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 import NMFeature
+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(NMFeature):
+    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..66a338bc 100644
--- a/py_neuromodulation/nm_normalization.py
+++ b/py_neuromodulation/nm_normalization.py
@@ -1,8 +1,13 @@
 """Module for real-time data normalization."""
+
 from enum import Enum
 
 from sklearn import preprocessing
 import numpy as np
+
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
+
+
 class NORM_METHODS(Enum):
     MEAN = "mean"
     MEDIAN = "median"
@@ -15,7 +20,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,
@@ -34,14 +39,14 @@ def test_normalization_settings(
     assert isinstance(clip, (float, int, bool))
 
 
-class RawNormalizer:
+class RawNormalizer(NMPreprocessor):
     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 +58,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 +68,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 +90,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 +112,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 +143,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 +194,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 +238,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..afd3bfc5 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_features import NMFeature
+from py_neuromodulation.nm_filter import MNEFilter
+from py_neuromodulation.nm_kalmanfilter import define_KF
 
 
-class OscillatoryFeature(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
-        self.s = settings
+class OscillatoryFeature(NMFeature):
+    def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
+        self.settings = 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())
@@ -21,74 +21,72 @@ def __init__(
 
     @staticmethod
     def test_settings_osc(
-        s: dict,
+        settings: dict,
         ch_names: Iterable[str],
-        sfreq: int | float,
+        sfreq: float,
         osc_feature_name: str,
     ):
         assert (
             fb[0] < sfreq / 2 and fb[1] < sfreq / 2
-            for fb in s["frequency_ranges_hz"].values()
+            for fb in settings["frequency_ranges_hz"].values()
         ), (
             "the frequency band ranges need to be smaller than the nyquist frequency"
-            f"got sfreq = {sfreq} and fband ranges {s['frequency_ranges_hz']}"
+            f"got sfreq = {sfreq} and fband ranges {settings['frequency_ranges_hz']}"
         )
 
         if osc_feature_name != "bandpass_filter_settings":
             assert isinstance(
-                s[osc_feature_name]["windowlength_ms"], int
-            ), f"windowlength_ms needs to be type int, got {s[osc_feature_name]['windowlength_ms']}"
+                settings[osc_feature_name]["windowlength_ms"], int
+            ), f"windowlength_ms needs to be type int, got {settings[osc_feature_name]['windowlength_ms']}"
 
             assert (
-                s[osc_feature_name]["windowlength_ms"]
-                <= s["segment_length_features_ms"]
+                settings[osc_feature_name]["windowlength_ms"]
+                <= settings["segment_length_features_ms"]
             ), (
-                f"oscillatory feature windowlength_ms = ({s[osc_feature_name]['windowlength_ms']})"
+                f"oscillatory feature windowlength_ms = ({settings[osc_feature_name]['windowlength_ms']})"
                 f"needs to be smaller than"
-                f"s['segment_length_features_ms'] = {s['segment_length_features_ms']}",
+                f"settings['segment_length_features_ms'] = {settings['segment_length_features_ms']}",
             )
+
         else:
-            for seg_length in s[osc_feature_name][
-                "segment_lengths_ms"
-            ].values():
+            for seg_length in settings[osc_feature_name]["segment_lengths_ms"].values():
                 assert isinstance(
                     seg_length, int
                 ), f"segment length has to be type int, got {seg_length}"
+
         assert isinstance(
-            s[osc_feature_name]["log_transform"], bool
-        ), f"log_transform needs to be type bool, got {s[osc_feature_name]['log_transform']}"
+            settings[osc_feature_name]["log_transform"], bool
+        ), f"log_transform needs to be type bool, got {settings[osc_feature_name]['log_transform']}"
 
-        assert isinstance(s["frequency_ranges_hz"], dict)
+        assert isinstance(settings["frequency_ranges_hz"], dict)
 
         assert (
             isinstance(value, list)
-            for value in s["frequency_ranges_hz"].values()
+            for value in settings["frequency_ranges_hz"].values()
         )
-        assert (len(value) == 2 for value in s["frequency_ranges_hz"].values())
+        assert (len(value) == 2 for value in settings["frequency_ranges_hz"].values())
 
         assert (
             isinstance(value[0], list)
-            for value in s["frequency_ranges_hz"].values()
+            for value in settings["frequency_ranges_hz"].values()
         )
 
         assert (
-            len(value[0]) == 2 for value in s["frequency_ranges_hz"].values()
+            len(value[0]) == 2 for value in settings["frequency_ranges_hz"].values()
         )
 
         assert (
             isinstance(value[1], (float, int))
-            for value in s["frequency_ranges_hz"].values()
+            for value in settings["frequency_ranges_hz"].values()
         )
 
     def init_KF(self, feature: str) -> None:
-        for f_band in self.s["kalman_filter_settings"]["frequency_bands"]:
+        for f_band in self.settings["kalman_filter_settings"]["frequency_bands"]:
             for channel in self.ch_names:
-                self.KF_dict[
-                    "_".join([channel, feature, f_band])
-                ] = nm_kalmanfilter.define_KF(
-                    self.s["kalman_filter_settings"]["Tp"],
-                    self.s["kalman_filter_settings"]["sigma_w"],
-                    self.s["kalman_filter_settings"]["sigma_v"],
+                self.KF_dict["_".join([channel, feature, f_band])] = define_KF(
+                    self.settings["kalman_filter_settings"]["Tp"],
+                    self.settings["kalman_filter_settings"]["sigma_w"],
+                    self.settings["kalman_filter_settings"]["sigma_v"],
                 )
 
     def update_KF(self, feature_calc: float, KF_name: str) -> float:
@@ -102,29 +100,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:
+        for feature_est_name in list(self.settings[est_name]["features"].keys()):
+            if self.settings[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
 
@@ -138,16 +136,14 @@ def __init__(
     ) -> None:
         super().__init__(settings, ch_names, sfreq)
 
-        if self.s["fft_settings"]["log_transform"]:
+        if self.settings["fft_settings"]["log_transform"]:
             self.log_transform = True
         else:
             self.log_transform = False
 
-        window_ms = self.s["fft_settings"]["windowlength_ms"]
+        window_ms = self.settings["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 +155,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):
-        OscillatoryFeature.test_settings_osc(s, ch_names, sfreq, "fft_settings")
+    def test_settings(settings: dict, ch_names: Iterable[str], sfreq: float):
+        OscillatoryFeature.test_settings_osc(settings, 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)
@@ -177,7 +173,7 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
             )
 
         for ch_idx, ch_name in enumerate(self.ch_names):
-            if self.s["fft_settings"]["return_spectrum"]:
+            if self.settings["fft_settings"]["return_spectrum"]:
                 features_compute.update(
                     {
                         f"{ch_name}_fft_psd_{str(f)}": Z[ch_idx][idx]
@@ -197,7 +193,7 @@ def __init__(
     ) -> None:
         super().__init__(settings, ch_names, sfreq)
 
-        self.log_transform = self.s["welch_settings"]["log_transform"]
+        self.log_transform = self.settings["welch_settings"]["log_transform"]
 
         self.feature_params = []
         for ch_idx, ch_name in enumerate(self.ch_names):
@@ -206,13 +202,13 @@ 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):
+    def test_settings(settings: dict, ch_names: Iterable[str], sfreq: float):
         OscillatoryFeature.test_settings_osc(
-            s, ch_names, sfreq, "welch_settings"
+            settings, 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 +222,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,
@@ -238,7 +232,7 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
             )
 
         for ch_idx, ch_name in enumerate(self.ch_names):
-            if self.s["welch_settings"]["return_spectrum"]:
+            if self.settings["welch_settings"]["return_spectrum"]:
                 features_compute.update(
                     {
                         f"{ch_name}_welch_psd_{str(f)}": Z[ch_idx][idx]
@@ -258,8 +252,8 @@ def __init__(
     ) -> None:
         super().__init__(settings, ch_names, sfreq)
 
-        self.nperseg = int(self.s["stft_settings"]["windowlength_ms"])
-        self.log_transform = self.s["stft_settings"]["log_transform"]
+        self.nperseg = int(self.settings["stft_settings"]["windowlength_ms"])
+        self.log_transform = self.settings["stft_settings"]["log_transform"]
 
         self.feature_params = []
         for ch_idx, ch_name in enumerate(self.ch_names):
@@ -268,13 +262,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(settings: dict, ch_names: Iterable[str], sfreq: float):
+        OscillatoryFeature.test_settings_osc(settings, 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 +278,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,
@@ -298,7 +288,7 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
             )
 
         for ch_idx, ch_name in enumerate(self.ch_names):
-            if self.s["stft_settings"]["return_spectrum"]:
+            if self.settings["stft_settings"]["return_spectrum"]:
                 Z_ch_mean = Z[ch_idx].mean(axis=1)
                 features_compute.update(
                     {
@@ -316,12 +306,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"]
+        bp_settings = self.settings["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 +320,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 +332,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,26 +351,26 @@ def __init__(
                         )
 
     @staticmethod
-    def test_settings(s: dict, ch_names: Iterable[str], sfreq: int | float):
+    def test_settings(settings: dict, ch_names: Iterable[str], sfreq: float):
         OscillatoryFeature.test_settings_osc(
-            s, ch_names, sfreq, "bandpass_filter_settings"
+            settings, ch_names, sfreq, "bandpass_filter_settings"
         )
 
         assert (
             isinstance(value, bool)
-            for value in s["bandpass_filter_settings"][
+            for value in settings["bandpass_filter_settings"][
                 "bandpower_features"
             ].values()
         )
 
         assert any(
             value is True
-            for value in s["bandpass_filter_settings"][
+            for value in settings["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"][
+        for fband_name, seg_length_fband in settings["bandpass_filter_settings"][
             "segment_lengths_ms"
         ].items():
             assert isinstance(seg_length_fband, int), (
@@ -390,17 +378,17 @@ def test_settings(s: dict, ch_names: Iterable[str], sfreq: int | float):
                 f"needs to be of type int, got {seg_length_fband}"
             )
 
-            assert seg_length_fband <= s["segment_length_features_ms"], (
+            assert seg_length_fband <= settings["segment_length_features_ms"], (
                 f"segment length {seg_length_fband} needs to be smaller than "
-                f" s['segment_length_features_ms'] = {s['segment_length_features_ms']}"
+                f" settings['segment_length_features_ms'] = {settings['segment_length_features_ms']}"
             )
 
-        for fband_name in list(s["frequency_ranges_hz"].keys()):
+        for fband_name in list(settings["frequency_ranges_hz"].keys()):
             assert fband_name in list(
-                s["bandpass_filter_settings"]["segment_lengths_ms"].keys()
+                settings["bandpass_filter_settings"]["segment_lengths_ms"].keys()
             ), (
                 f"frequency range {fband_name} "
-                "needs to be defined in s['bandpass_filter_settings']['segment_lengths_ms']"
+                "needs to be defined in settings['bandpass_filter_settings']['segment_lengths_ms']"
             )
 
     def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
@@ -417,15 +405,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_preprocessing.py b/py_neuromodulation/nm_preprocessing.py
new file mode 100644
index 00000000..f71bdeee
--- /dev/null
+++ b/py_neuromodulation/nm_preprocessing.py
@@ -0,0 +1,86 @@
+from typing import Protocol, TYPE_CHECKING
+from inspect import getfullargspec
+from typing import Type
+from py_neuromodulation.nm_types import ImportDetails, get_class
+
+if TYPE_CHECKING:
+    import numpy as np
+    import pandas as pd
+
+
+class NMPreprocessor(Protocol):
+    def __init__(self, settings: dict, sfreq: float) -> None: ...
+
+    def process(self, data: "np.ndarray") -> "np.ndarray": ...
+
+
+PREPROCESSOR_DICT: dict[str, ImportDetails] = {
+    "preprocessing_filter": ImportDetails(
+        "py_neuromodulation.nm_filter_preprocessing", "PreprocessingFilter"
+    ),
+    "notch_filter": ImportDetails("py_neuromodulation.nm_filter", "NotchFilter"),
+    "raw_resampling": ImportDetails("py_neuromodulation.nm_resample", "Resampler"),
+    "re_referencing": ImportDetails(
+        "py_neuromodulation.nm_rereference", "ReReferencer"
+    ),
+    "raw_normalization": ImportDetails(
+        "py_neuromodulation.nm_normalization", "RawNormalizer"
+    ),
+}
+
+
+class NMPreprocessors:
+    "Class for initializing and holding data preprocessing classes"
+
+    def __init__(
+        self,
+        settings: dict,
+        nm_channels: "pd.DataFrame",
+        sfreq: float,
+        line_noise: float | None = None,
+    ) -> None:
+        possible_arguments = {
+            "sfreq": sfreq,
+            "settings": settings,
+            "nm_channels": nm_channels,
+            "line_noise": line_noise,
+        }
+
+        for preprocessing_method in settings["preprocessing"]:
+            if preprocessing_method not in PREPROCESSOR_DICT.keys():
+                raise ValueError(
+                    f"Invalid preprocessing method '{preprocessing_method}'. Must be one of {PREPROCESSOR_DICT.keys()}"
+                )
+
+        # Get needed preprocessor classes from settings
+        preprocessor_classes: dict[str, Type[NMPreprocessor]] = {
+            preprocessor_name: get_class(import_details)
+            for preprocessor_name, import_details in PREPROCESSOR_DICT.items()
+            if preprocessor_name in settings["preprocessing"]
+        }
+
+        # Function to instantiate preprocessor with settings
+        def instantiate_preprocessor(
+            preprocessor_class: Type[NMPreprocessor], preprocessor_name: str
+        ) -> NMPreprocessor:
+            settings_str = f"{preprocessor_name}_settings"
+            # Filter out arguments that are not in the preprocessor's __init__ method
+            args = {
+                arg: possible_arguments[arg]
+                for arg in getfullargspec(preprocessor_class).args
+                if arg in possible_arguments
+            }
+            # Retrieve more possible arguments from settings
+            args |= settings.get(settings_str, {})
+            # Pass arguments to preprocessor class and return instance
+            return preprocessor_class(**args)
+
+        self.preprocessors: list[NMPreprocessor] = [
+            instantiate_preprocessor(preprocessor_class, preprocessor_name)
+            for preprocessor_name, preprocessor_class in preprocessor_classes.items()
+        ]
+
+    def process_data(self, data: "np.ndarray") -> "np.ndarray":
+        for preprocessor in self.preprocessors:
+            data = preprocessor.process(data)
+        return data
diff --git a/py_neuromodulation/nm_projection.py b/py_neuromodulation/nm_projection.py
index 236ec3db..05f510a8 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(),
@@ -96,26 +85,20 @@ def __init__(
             nmplotter.plot_cortex()
 
     @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:
+    def test_settings(settings: dict):
+        if settings["postprocessing"]["project_cortex"]:
+            assert isinstance(settings["project_cortex_settings"]["max_dist_mm"], (float, int))
+        if settings["postprocessing"]["project_subcortex"]:
             assert isinstance(
-                s["project_subcortex_settings"]["max_dist_mm"], (float, int)
+                settings["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..9973691c 100644
--- a/py_neuromodulation/nm_rereference.py
+++ b/py_neuromodulation/nm_rereference.py
@@ -1,21 +1,22 @@
 """Re-referencing Module."""
+
 import numpy as np
 import pandas as pd
 
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
 
-class ReReferencer:
-    ref_matrix: np.ndarray
 
+class ReReferencer(NMPreprocessor):
     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 +27,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..af06e5cc 100644
--- a/py_neuromodulation/nm_resample.py
+++ b/py_neuromodulation/nm_resample.py
@@ -1,14 +1,18 @@
 """Module for resampling."""
-import mne
+
 import numpy as np
 
+from mne.filter import resample as mne_resample
+
+from py_neuromodulation.nm_preprocessing import NMPreprocessor
 
-class Resampler:
+
+class Resampler(NMPreprocessor):
     """Resample data.
 
     Parameters
     ----------
-    sfreq : int | float
+    sfreq : float
         Original sampling frequency.
 
     Attributes
@@ -19,14 +23,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 +49,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 37207977..38269db3 100644
--- a/py_neuromodulation/nm_run_analysis.py
+++ b/py_neuromodulation/nm_run_analysis.py
@@ -1,62 +1,43 @@
 """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,
-)
-
-_PathLike = str | os.PathLike
+from py_neuromodulation import nm_IO, logger
+from py_neuromodulation.nm_types import _PathLike
+from py_neuromodulation.nm_features import Features
+from py_neuromodulation.nm_preprocessing import NMPreprocessors
+from py_neuromodulation.nm_normalization import FeatureNormalizer
+from py_neuromodulation.nm_projection import Projection
 
 
 class Preprocessor(Protocol):
-    def process(self, data: np.ndarray) -> np.ndarray:
-        pass
+    def process(self, data: np.ndarray) -> np.ndarray: ...
 
     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,69 +63,33 @@ 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
 
         (self.ch_names_used, _, self.feature_idx, _) = self._get_ch_info()
 
-        self.preprocessors: list[Preprocessor] = []
-        for preprocessing_method in self.settings["preprocessing"]:
-            settings_str = f"{preprocessing_method}_settings"
-            match preprocessing_method:
-                case "raw_resampling":
-                    preprocessor = nm_resample.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(
-                        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(
-                        sfreq=self.sfreq_raw,
-                        nm_channels=self.nm_channels,
-                    )
-                    self.preprocessors.append(preprocessor)
-                case "raw_normalization":
-                    preprocessor = nm_normalization.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(
-                        settings=self.settings,
-                        sfreq=self.sfreq_raw,
-                    )
-                    self.preprocessors.append(preprocessor)
-                case _:
-                    raise ValueError(
-                        "Invalid preprocessing method. Must be one of"
-                        f" {_PREPROCESSING_CONSTRUCTORS}. Got"
-                        f" {preprocessing_method}"
-                    )
+        self.preprocessors = NMPreprocessors(
+            settings=self.settings,
+            nm_channels=self.nm_channels,
+            sfreq=self.sfreq_raw,
+            line_noise=self.line_noise,
+        )
 
         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(
-            s=self.settings,
+        self.features = Features(
+            settings=self.settings,
             ch_names=self.ch_names_used,
             sfreq=self.sfreq_raw,
         )
@@ -174,12 +119,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 +133,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 +157,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 +171,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 +179,6 @@ def _get_grids(
         ----------
         settings : dict
         path_grids : str
-        grid_type : GRIDS
 
         Returns
         -------
@@ -251,21 +186,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 +209,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 +252,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:
@@ -347,8 +279,7 @@ def process(self, data: np.ndarray) -> pd.Series:
 
         data = np.nan_to_num(data)[self.feature_idx, :]
 
-        for processor in self.preprocessors:
-            data = processor.process(data)
+        data = self.preprocessors.process_data(data)
 
         # calculate features
         features_dict = self.features.estimate_features(data)
@@ -371,23 +302,15 @@ 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
-
-        if self.verbose is True:
-            logger.info(
-                "Last batch took: "
-                + str(np.round(time() - start_time, 2))
-                + " seconds"
-            )
+                features_current.loc[features_current.index.str.contains(ch)] = np.nan
+
+        if self.verbose:
+            logger.info("Last batch took: %.2f seconds", time() - start_time)
 
         return features_current
 
@@ -400,7 +323,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,
@@ -409,14 +332,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
 
@@ -425,9 +344,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.json b/py_neuromodulation/nm_settings.json
index 8e722220..31980706 100644
--- a/py_neuromodulation/nm_settings.json
+++ b/py_neuromodulation/nm_settings.json
@@ -57,9 +57,6 @@
             "frequency_high_hz": 200
         }
     },
-    "re_referencing_settings": {
-        "reference": "common"
-    },
     "documentation_normalization_options": [
         "mean",
         "median",
diff --git a/py_neuromodulation/nm_settings.py b/py_neuromodulation/nm_settings.py
index be0f2ee8..3f46ffee 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 = 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..4facdb50 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 import NMFeature
 
 
 class NoValidTroughException(Exception):
     pass
 
 
-class SharpwaveAnalyzer(nm_features_abc.Feature):
-    def __init__(
-        self, settings: dict, ch_names: Iterable[str], sfreq: float
-    ) -> None:
-
+class SharpwaveAnalyzer(NMFeature):
+    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 071a9cbb..fd3a0cab 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,
         stream_name: str | None = "example_stream",
@@ -58,15 +31,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
@@ -80,13 +53,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
@@ -97,7 +68,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,
@@ -109,12 +80,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,
@@ -126,9 +97,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
         """
@@ -162,30 +131,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:
@@ -203,9 +172,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:
@@ -215,6 +182,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 927c3f2e..4622bd15 100644
--- a/py_neuromodulation/nm_stream_offline.py
+++ b/py_neuromodulation/nm_stream_offline.py
@@ -1,39 +1,30 @@
 """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 mne_lsl import stream_viewer
-
-from py_neuromodulation import (
-    nm_generator,
-    nm_IO,
-    nm_stream_abc,
-    nm_define_nmchannels,
-)
+from pathlib import Path
+from joblib import Parallel, delayed
 
-_PathLike = str | os.PathLike
+from py_neuromodulation.nm_generator import raw_data_generator, LSLStream
+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
 
+from mne_lsl.stream_viewer import StreamViewer
 
-class _GenericStream(nm_stream_abc.PNStream):
+class _GenericStream(NMStream):
     """_GenericStream base class.
     This class can be inhereted for different types of offline streams
 
     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
@@ -58,15 +49,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
@@ -75,10 +62,7 @@ def _add_timestamp(
         feature_series["time"] = cnt_samples * 1000 / self.sfreq
 
         if self.verbose:
-            logging.info(
-                str(np.round(feature_series["time"] / 1000, 2))
-                + " seconds of data processed"
-            )
+            logger.info("%.2f seconds of data processed", feature_series['time'] / 1000)
 
         return feature_series
 
@@ -89,9 +73,9 @@ def _handle_data(self, data: np.ndarray | pd.DataFrame) -> np.ndarray:
             if not len(names_expected) == data.shape[0]:
                 raise ValueError(
                     "If data is passed as an array, the first dimension must"
-                    " match the number of channel names in `nm_channels`. Got:"
-                    f" Data columns: {data.shape[0]}, nm_channels.name:"
-                    f" {len(names_expected)}."
+                    " 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)}.'
                 )
             return data
         names_data = data.columns.to_list()
@@ -101,10 +85,11 @@ def _handle_data(self, data: np.ndarray | pd.DataFrame) -> np.ndarray:
         ):
             raise ValueError(
                 "If data is passed as a DataFrame, the"
-                "columns must match the channel names in `nm_channels`. Got:"
-                f"Data columns: {names_data}, nm_channels.name: {names_data}."
+                "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}.'
             )
-        return data.to_numpy()
+        return data.to_numpy().transpose()
 
     def _check_settings_for_parallel(self):
         """Check specified settings and raise error if parallel processing is not possible.
@@ -117,11 +102,11 @@ 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."
             )
@@ -142,7 +127,7 @@ def _process_batch(self, data_batch, cnt_samples):
     def _run(
         self,
         data: np.ndarray,
-        out_path_root: _PathLike | None = None,
+        out_path_root: _PathLike = "",
         folder_name: str = "sub",
         stream_lsl: bool = True,
         stream_lsl_name: str = "example_stream",
@@ -152,18 +137,18 @@ def _run(
     ) -> pd.DataFrame:
 
         if stream_lsl is False:
-            generator = nm_generator.raw_data_generator(
+            generator = raw_data_generator(
                 data=data,
                 settings=self.settings,
                 sfreq=self.sfreq,
             )
         else:
-            self.lsl_stream = nm_generator.LSLStream(
+            self.lsl_stream = LSLStream(
                 settings=self.settings, stream_name=stream_lsl_name
             )
 
             if plot_lsl:
-                viewer = stream_viewer.StreamViewer(stream_name=stream_lsl_name)
+                viewer = StreamViewer(stream_name=stream_lsl_name)
                 viewer.start()
             
             if self.sfreq != self.lsl_stream.stream.sinfo.sfreq:
@@ -199,7 +184,7 @@ def _run(
             )
 
         else:
-            l_features = []
+            l_features : list[pd.Series] = []
             cnt_samples = offset_start
             start_time = None
             while True:
@@ -216,7 +201,7 @@ def _run(
                     data_batch.astype(np.float64)
                 )
 
-                start_time = time_[0] if start_time is None else start_time
+                # start_time = time_[0] if start_time is None else start_time
 
                 # feature_series["time"] = (
                 #     time_[-1] - start_time
@@ -240,11 +225,11 @@ def _run(
 
     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:
@@ -254,7 +239,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
@@ -283,9 +268,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:
@@ -300,12 +283,12 @@ def plot_raw_signal(
 class Stream(_GenericStream):
     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,
@@ -315,7 +298,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
@@ -323,9 +306,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
@@ -338,9 +321,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(
@@ -361,12 +342,12 @@ 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,
-        out_path_root: _PathLike | None = None,
+        data: np.ndarray | pd.DataFrame | None = None,
+        out_path_root: _PathLike = Path.cwd(),
         folder_name: str = "sub",
         parallel: bool = False,
         n_jobs: int = -2,
@@ -404,9 +385,13 @@ def run(
         elif self.data is None and data is None and self.stream_lsl is False:
             raise ValueError("No data passed to run function.")
 
-        if parallel is True:
+        if parallel:
             self._check_settings_for_parallel()
 
+        out_path = Path(out_path_root, folder_name)
+        out_path.mkdir(parents=True, exist_ok=True)
+        logger.log_to_file(out_path)
+
         return self._run(
             data,
             out_path_root,
diff --git a/py_neuromodulation/nm_types.py b/py_neuromodulation/nm_types.py
new file mode 100644
index 00000000..b7bb6eb7
--- /dev/null
+++ b/py_neuromodulation/nm_types.py
@@ -0,0 +1,14 @@
+from os import PathLike
+from typing import NamedTuple, Type, Any
+from importlib import import_module
+
+_PathLike = str | PathLike
+
+
+class ImportDetails(NamedTuple):
+    module_name: str
+    class_name: str
+
+
+def get_class(module_details: ImportDetails) -> Type[Any]:
+    return getattr(import_module(module_details.module_name), module_details.class_name)
diff --git a/py_neuromodulation/utils/_logging.py b/py_neuromodulation/utils/_logging.py
deleted file mode 100644
index 99674688..00000000
--- a/py_neuromodulation/utils/_logging.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import logging
-
-# include the filename in the log output
-# Configure the logger
-logger = logging.getLogger("PynmLogger")
-logger.setLevel(logging.INFO)
-
-# Create a file handler and set its level to DEBUG
-# change mode to "a" to append to the file
-file_handler = logging.FileHandler("logfile_pynm.log", mode="w")
-file_handler.setLevel(logging.INFO)
-
-# console_handler = logging.StreamHandler()
-# console_handler.setLevel(logging.DEBUG)
-
-# Create a formatter and add it to the handler
-formatter = logging.Formatter(
-    "%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(message)s"
-)
-file_handler.setFormatter(formatter)
-# console_handler.setFormatter(formatter)
-
-# Add the file handler to the logger
-logger.addHandler(file_handler)
-# logger.addHandler(console_handler)
diff --git a/pyproject.toml b/pyproject.toml
index 8c5581dc..583841b4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,14 +1,14 @@
 [build-system]
-build-backend = "flit_core.buildapi"
-requires = ["flit_core >=3.2,<4"]
+build-backend = "hatchling.build"
+requires = ["hatchling"]
 
 [project]
 name = "py_neuromodulation"
-version = "0.0.1"
-authors = [{name = "Timon Merk", email="timon.merk@charite.de"}]
+version = "0.0.4"
+authors = [{ name = "Timon Merk", email = "timon.merk@charite.de" }]
 classifiers = [
   "Development Status :: 2 - Pre-Alpha",
-  "License :: OSI Approved :: MIT License ",
+  "License :: OSI Approved :: MIT License",
   "Programming Language :: Python",
   "Topic :: Software Development :: Libraries :: Python Modules",
 ]
@@ -23,63 +23,76 @@ keywords = [
   "deep-brain-stimulation",
   "machine-learning",
 ]
-license = {file = "LICENSE"}
-maintainers = [{name = "Timon Merk"}]
+license = { file = "LICENSE" }
+maintainers = [{ name = "Timon Merk" }]
 readme = "README.rst"
 requires-python = ">=3.10"
 
 dependencies = [
-    "mne",
-    "filterpy >= 1.4.5",
-    "fooof",
-    "imbalanced-learn",
-    "matplotlib >= 3.3.4",
-    "mne-bids >= 0.8",
-    "mne-connectivity",
-    "mrmr-selection",
-    "nolds",
-    "numpy >= 1.21.2",
-    "pandas >= 1.2.2",
-    "pip",
-    "pynput",
-    "pybids",
-    "scikit-image",
-    "scikit-learn >= 0.24.2",
-    "scikit-optimize",
-    "scipy >= 1.7.1",
-    "seaborn >= 0.11",
-    "notebook",
-    "ipython",
-    "pybispectra>=1.1.0",
-    "pyparrm",
-    "pyarrow>=14.0.2",
-    "joblib>=1.3.2",
-    "pytest>=8.0.2",
-    "mne-lsl>=1.2.0",
-    "pyside6>=6.6.3.1"
+  "mne",
+  "filterpy >= 1.4.5",
+  "fooof",
+  "imbalanced-learn",
+  "matplotlib >= 3.3.4",
+  "mne-bids >= 0.8",
+  "mne-connectivity",
+  "mrmr_selection",
+  "nolds",
+  "numpy >= 1.21.2",
+  "pandas >= 1.2.2",
+  "scikit-image",
+  "scikit-learn >= 0.24.2",
+  "scikit-optimize",
+  "scipy >= 1.7.1",
+  "seaborn >= 0.11",
+  "pybispectra>=1.0.0",
+  "pyparrm",
+  "pyarrow>=14.0.2",
+  "joblib>=1.3.2",
+  "hatch>=1.9.4",
+  "statsmodels",
+  "mne-lsl>=1.2.0",
+  "pynput",
+  "pyqt5",
 ]
 
 [project.optional-dependencies]
-dev = ["black", "pytest", "pytest-cov"]
+test = ["pytest>=8.0.2", "pytest-xdist"]
+dev = ["ruff", "pytest>=8.0.2", "pytest-cov", "pytest-sugar", "notebook"]
+docs = [
+  "py-neuromodulation[dev]",
+  "nbsphinx",
+  "sphinx",
+  "pandoc",
+  "recommonmark",
+  "sphinx",
+  "numpydoc",
+  "sphinx-gallery",
+  "nbsphinx_link",
+  "pydata-sphinx-theme",
+  "pypandoc",
+  "sphinx_togglebutton",
+]
+
+[tool.rye]
+dev-dependencies = ["py-neuromodulation[dev]"]
 
 [project.urls]
 bugtracker = "https://github.com/neuromodulation/py_neuromodulation/issues"
 repository = "https://github.com/neuromodulation/py_neuromodulation"
 
-[tool.black]
-exclude = '''
-/(
-  __pycache__
-)/
-'''
-include = '''
-(
-  /src/
-  # | /tests
-)
-'''
-line-length = 80
-target-version = ['py310']
+[tool.ruff]
+target-version = "py310"
 
-[tool.flit.module]
-name = "py_neuromodulation"
+[tool.hatch.build.targets.sdist]
+exclude = ["/.github", "/docs"]
+
+[tool.hatch.build.targets.wheel]
+packages = ["py_neuromodulation"]
+exclude = [
+  "py_neuromodulation/data/derivatives",
+  "py_neuromodulation/ConnectivityDecoding/connectome_folder",
+]
+
+[tool.mypy]
+ignore_missing_imports = true
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index da41dfa6..00000000
--- a/tox.ini
+++ /dev/null
@@ -1,16 +0,0 @@
-[tox]
-env_list =
-   py3{10, 11}
- 
-skip_missing_interpreters = true
-isolated_build = true
-
-
-[testenv]
-description = install pytest in a virtual environment and invoke it on the tests folder
-deps =
-    pytest>=7
-    pytest-sugar
-commands = pytest tests {posargs}
-
-# check https://stackoverflow.com/a/40831237/5060208 for adding posargs to pytest


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index de6ae1c9..115c642e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -20,48 +20,34 @@ on:
       - '*.txt'
 jobs:
   tests:
-    name: ${{ matrix.name }}
-    runs-on: ${{ matrix.os }}
-    env:
-      APPDATA: DefaultNonExistingPath 
-      MNE_LSL_LIB: ${{ github.workspace }}/liblsl.1.16.0.dylib
+    name: ${{ matrix.platform.name }} Python ${{ matrix.python }}
+    runs-on: ${{ matrix.platform.os }}
     strategy:
       fail-fast: false
       matrix:
-        include:
-          - {name: 3.10 Mac, python: '3.10', os: macos-latest, tox: py310}
-          - {name: 3.10 Linux, python: '3.10', os: ubuntu-latest, tox: py310}
+        platform:
+          - os: ubuntu-latest
+            name: Linux
+          - os: macos-latest
+            name: MacOS
+          - os: windows-latest
+            name: Windows
+        python:
+          - '3.10'
     steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-python@v4
+      - uses: actions/checkout@v4
+      - name: Install and cache Linux packages
+        if: ${{ runner.os == 'Linux' }}
+        uses: tecolicom/actions-use-apt-tools@v1
         with:
-          python-version: ${{ matrix.python }}
-      - name: update pip
-        run: |
-          python -m pip install -U pip
-      - name: get pip cache dir
-        id: pip-cache
-        run: echo "::set-output name=dir::$(pip cache dir)"
-      - name: cache pip
-        uses: actions/cache@v3
+          tools: binutils qtbase5-dev qt5-qmake libpugixml1v5 xvfb
+      - name: Set up Python with uv
+        uses: drivendataorg/setup-python-uv-action@v1
         with:
-          path: ${{ steps.pip-cache.outputs.dir }}
-          key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('requirements/*.txt') }}
-          restore-keys: pip|${{ runner.os }}|${{ matrix.python }}
-      - name: Install liblsl (linux)
-        if: ${{ matrix.os == 'ubuntu' }}
-        run: |
-          curl -L https://github.com/sccn/liblsl/releases/download/v1.16.2/liblsl-1.16.2-jammy_amd64.deb -o liblsl-1.16.2-jammy_amd64.deb
-          sudo apt update
-          sudo apt install -y binutils libpugixml-dev qtbase5-dev qt5-qmake
-          sudo apt install -y ./liblsl-1.16.2-jammy_amd64.deb
-          rm liblsl-1.16.2-jammy_amd64.deb
-      - name: Install dependencies
-        run: |
-          python -m pip install --progress-bar off --upgrade pip setuptools
-          python -m pip install --progress-bar off .[test]
-      - run: pip install tox
-        env:
-          APPDATA: DefaultNonExistingPath 
-          MNE_LSL_LIB: ${{ github.workspace }}/liblsl.1.16.0.dylib
-      - run: tox -e ${{ matrix.tox }}
+          python-version: ${{ matrix.python }}
+          cache: packages
+          cache-dependency-path: pyproject.toml
+      - name: Install test dependencies
+        run: uv pip install -q .[test]
+      - name: Run tests
+        run: ${{ runner.os == 'Linux' && 'xvfb-run' || '' }} pytest -n auto tests/
diff --git a/tests/conftest.py b/tests/conftest.py
index 0cf03360..68d3a614 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[1]]
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..93c8f1ae 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)
 
@@ -93,7 +86,6 @@ def test_different_sampling_rate_0DOT1Hz():
 
 
 def test_wrong_initalization_of_segment_length_features_ms_and_osc_window_length():
-    segment_length_features_ms = 800
 
     arr_test = np.random.random([2, 1200])
     settings, nm_channels = get_example_settings(arr_test)
diff --git a/tests/test_fooof.py b/tests/test_fooof.py
index 1ed10636..14753ac4 100644
--- a/tests/test_fooof.py
+++ b/tests/test_fooof.py
@@ -2,13 +2,10 @@
 
 
 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
-    )
-    data_batch = next(generator, None)
+    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
     # there can be None's in the feature_series
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_lsl_stream.py b/tests/test_lsl_stream.py
index dec0fe67..35c7facc 100644
--- a/tests/test_lsl_stream.py
+++ b/tests/test_lsl_stream.py
@@ -109,7 +109,7 @@ def call_every_100ms():
 
     player = nm_generator.LSLOfflinePlayer(f_name = f_name, settings = settings)
 
-    def get_example_stream(test_arr: np.array) -> nm_stream_abc.PNStream:
+    def get_example_stream(test_arr: np.ndarray) -> nm_stream_abc.NMStream:
 
         settings["features"]["welch"] = True
         settings["features"]["fft"] = True
@@ -164,7 +164,7 @@ def get_example_stream(test_arr: np.array) -> nm_stream_abc.PNStream:
     settings = nm_settings.get_default_settings()
     settings = nm_settings.set_settings_fast_compute(settings)
 
-    def get_example_stream(test_arr: np.array) -> nm_stream_abc.PNStream:
+    def get_example_stream(test_arr: np.ndarray) -> nm_stream_abc.NMStream:
 
         settings["features"]["welch"] = False
         settings["features"]["fft"] = True
diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py
index b3eae0e5..8ffb885a 100644
--- a/tests/test_multiprocessing.py
+++ b/tests/test_multiprocessing.py
@@ -1,6 +1,5 @@
-import py_neuromodulation as pn
+import py_neuromodulation as nm
 import numpy as np
-from py_neuromodulation import nm_settings
 import pytest
 
 
@@ -13,7 +12,7 @@ def get_stream():
 
     data = np.random.random([NUM_CHANNELS, NUM_DATA])
 
-    stream = pn.Stream(
+    stream = nm.Stream(
         sfreq=sfreq,
         data=data,
         sampling_rate_features_hz=sampling_rate_features_hz,
@@ -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])