Repository: richardkoehler/pte (language: python)

Issue:
Update bids.py - replace pandas append with concat
`append` is since pandas 2.0 deprecated. I replaced it with concat


Discussion / hints:

Fix patch:
diff --git a/.github/actions/setup-venv/action.yml b/.github/actions/setup-venv/action.yml
new file mode 100644
index 0000000..11b64f3
--- /dev/null
+++ b/.github/actions/setup-venv/action.yml
@@ -0,0 +1,58 @@
+name: Python virtualenv
+description: Set up a Python virtual environment with caching
+inputs:
+  python-version:
+    description: The Python version to use
+    required: true
+  cache-prefix:
+    description: Update this to invalidate the cache
+    required: true
+    default: v0
+runs:
+  using: composite
+  steps:
+    - name: Setup Python
+      uses: actions/setup-python@v5
+      with:
+        python-version: ${{ inputs.python-version }}
+
+    - shell: bash
+      run: |
+        # Install prerequisites.
+        pip install --upgrade pip setuptools wheel virtualenv
+
+    - shell: bash
+      run: |
+        # Get the exact Python version to use in the cache key.
+        echo "PYTHON_VERSION=$(python --version)" >> $GITHUB_ENV
+
+    - uses: actions/cache@v4
+      id: virtualenv-cache
+      with:
+        path: .venv
+        key:
+          ${{ inputs.cache-prefix }}-${{ runner.os }}-${{ env.PYTHON_VERSION
+          }}-${{ hashFiles('pyproject.toml') }}
+
+    - if: steps.virtualenv-cache.outputs.cache-hit != 'true'
+      shell: bash
+      run: |
+        # Set up virtual environment without cache hit.
+        test -d .venv || virtualenv -p $(which python) --copies --reset-app-data .venv
+        . .venv/bin/activate
+        pip install -e .[dev]
+
+    - if: steps.virtualenv-cache.outputs.cache-hit == 'true'
+      shell: bash
+      run: |
+        # Set up virtual environment from cache hit.
+        . .venv/bin/activate
+        pip install --no-deps -e .[dev]
+
+    - shell: bash
+      run: |
+        # Show environment info.
+        . .venv/bin/activate
+        echo "✓ Installed $(python --version) virtual environment to $(which python)"
+        echo "Packages:"
+        pip freeze
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..e74de25
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,146 @@
+name: Main
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+on:
+  pull_request:
+    branches:
+      - main
+  push:
+    branches:
+      - main
+    tags:
+      - "v*.*.*"
+
+env:
+  # Change this to invalidate existing cache.
+  CACHE_PREFIX: v0
+  PYTHONPATH: ./
+
+jobs:
+  tests:
+    name:
+      ${{ matrix.os }} - Python ${{ matrix.python }} - ${{ matrix.task.name }}
+    runs-on: [ubuntu-latest]
+    timeout-minutes: 15
+    strategy:
+      fail-fast: false
+      matrix:
+        python: ["3.12"]
+        task:
+          - name: Test
+            run:
+              ":"
+              # pytest -v --color=yes tests/
+        include:
+          - python: "3.12"
+            task:
+              name: Lint
+              run: ruff check .
+          - python: "3.12"
+            task:
+              name: Type check
+              run: mypy .
+          - python: "3.12"
+            task:
+              name: Build
+              run: hatch build
+          - python: "3.12"
+            task:
+              name: Style
+              run: black --check .
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Setup Python environment
+        uses: ./.github/actions/setup-venv
+        with:
+          python-version: ${{ matrix.python }}
+          cache-prefix: ${{ env.CACHE_PREFIX }}
+
+      - name: Restore mypy cache
+        if: matrix.task.name == 'Type check'
+        uses: actions/cache@v4
+        with:
+          path: .mypy_cache
+          key:
+            mypy-${{ env.CACHE_PREFIX }}-${{ runner.os }}-${{
+            matrix.python}}-${{ hashFiles('*requirements.txt') }}-${{ github.ref
+            }}-${{github.sha }}
+          restore-keys: |
+            mypy-${{ env.CACHE_PREFIX }}-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('*requirements.txt') }}-${{ github.ref }}
+            mypy-${{ env.CACHE_PREFIX }}-${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('*requirements.txt') }}
+
+      - name: ${{ matrix.task.name }}
+        run: |
+          . .venv/bin/activate
+          ${{ matrix.task.run }}
+
+      - name: Upload package distribution files
+        if: matrix.task.name == 'Build'
+        uses: actions/upload-artifact@v4
+        with:
+          name: package
+          path: dist
+
+      - name: Clean up
+        if: always()
+        run: |
+          . .venv/bin/activate
+          pip uninstall -y pte
+
+  release:
+    name: Release
+    if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
+    runs-on: ubuntu-latest
+    needs: [tests]
+    environment:
+      name: pypi
+      url: https://pypi.org/p/pte
+    permissions:
+      id-token: write # IMPORTANT: mandatory for trusted publishing
+      contents: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+
+      - name: Install requirements
+        run: |
+          pip install --upgrade pip
+          pip install -e .[dev]
+
+      - name: Prepare environment
+        run: |
+          echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
+          echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+
+      - name: Download package distribution files
+        uses: actions/download-artifact@v4
+        with:
+          name: package
+          path: dist
+
+      - name: Generate release notes
+        run: |
+          python scripts/release_notes.py > ${{ github.workspace }}-RELEASE_NOTES.md
+
+      - name: Publish distribution 📦 to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+
+      - name: Publish GitHub release
+        uses: softprops/action-gh-release@v1
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          body_path: ${{ github.workspace }}-RELEASE_NOTES.md
+          prerelease: ${{ contains(env.TAG, 'rc') }}
+          files: |
+            dist/*
diff --git a/.gitignore b/.gitignore
index 7f43b0c..c1d2514 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,11 @@
+# Archive
+.archive/
+
+# Ruff linter
+.ruff_cache/
+
 # Byte-compiled / optimized / DLL files
-__pycache__/
+*__pycache__
 *.py[cod]
 *$py.class
 
@@ -7,6 +13,7 @@ __pycache__/
 *.so
 
 # Distribution / packaging
+.pypirc/
 .Python
 build/
 develop-eggs/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..c1e0721
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,91 @@
+ci:
+  autoupdate_commit_msg: "chore: update pre-commit hooks"
+  autofix_commit_msg: "style: pre-commit fixes"
+
+repos:
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 24.2.0
+    hooks:
+      - id: black-jupyter
+
+  - repo: https://github.com/adamchainz/blacken-docs
+    rev: "1.16.0"
+    hooks:
+      - id: blacken-docs
+        additional_dependencies: [black==23.*]
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: "v4.5.0"
+    hooks:
+      - id: check-added-large-files
+      - id: check-case-conflict
+      - id: check-merge-conflict
+      - id: check-symlinks
+      - id: check-yaml
+      - id: debug-statements
+      - id: end-of-file-fixer
+      - id: mixed-line-ending
+      - id: name-tests-test
+        args: ["--pytest-test-first"]
+      - id: requirements-txt-fixer
+      - id: trailing-whitespace
+
+  - repo: https://github.com/pre-commit/pygrep-hooks
+    rev: "v1.10.0"
+    hooks:
+      - id: rst-backticks
+      - id: rst-directive-colons
+      - id: rst-inline-touching-normal
+
+  - repo: https://github.com/pre-commit/mirrors-prettier
+    rev: "v3.0.3"
+    hooks:
+      - id: prettier
+        types_or: [yaml, markdown, html, css, scss, javascript, json]
+        args: [--prose-wrap=always]
+
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: "v0.1.3"
+    hooks:
+      - id: ruff
+        args: ["--fix", "--show-fixes"]
+
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: "v1.6.1"
+    hooks:
+      - id: mypy
+        files: src|tests
+        args: []
+        additional_dependencies:
+          - pytest
+          - pandas-stubs
+
+  - repo: https://github.com/codespell-project/codespell
+    rev: "v2.2.6"
+    hooks:
+      - id: codespell
+
+  # - repo: https://github.com/shellcheck-py/shellcheck-py
+  #   rev: "v0.9.0.6"
+  #   hooks:
+  #     - id: shellcheck
+
+  - repo: local
+    hooks:
+      - id: disallow-caps
+        name: Disallow improper capitalization
+        language: pygrep
+        entry: PyBind|Numpy|Cmake|CCache|Github|PyTest
+        exclude: .pre-commit-config.yaml
+
+  - repo: https://github.com/abravalheri/validate-pyproject
+    rev: v0.15
+    hooks:
+      - id: validate-pyproject
+
+  - repo: https://github.com/python-jsonschema/check-jsonschema
+    rev: 0.28.0
+    hooks:
+      - id: check-dependabot
+      - id: check-github-workflows
+      - id: check-readthedocs
diff --git a/LICENSE b/LICENSE
index d204dbe..9168c1a 100644
--- a/LICENSE
+++ b/LICENSE
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/README.md b/README.md
index 1f86d31..1fccbcb 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,15 @@
-[![Python Versions][python-shield]][python-url]
-[![PyPi][pypi-shield]][pypi-url]
+[![Python Versions][python-shield]][python-url] [![PyPi][pypi-shield]][pypi-url]
 [![License][license-shield]][license-url]
 [![Contributors][contributors-shield]][contributors-url]
 [![Code Style][codestyle-shield]][codestyle-url]
-[![Homepage][homepage-shield]][homepage-url]
-
 
 # PTE - Python tools for electrophysiology
 
-PTE is an open-source software package for working with electrophysiological data
+PTE is an open-source software package for working with electrophysiological
+data
 
-PTE builds upon the packages [MNE](https://mne.tools/stable/index.html) and [MNE-BIDS](https://mne.tools/mne-bids/stable/index.html).
+PTE builds upon the packages [MNE](https://mne.tools/stable/index.html) and
+[MNE-BIDS](https://mne.tools/mne-bids/stable/index.html).
 
 ## Installing PTE
 
@@ -19,24 +18,28 @@ PTE builds upon the packages [MNE](https://mne.tools/stable/index.html) and [MNE
 To install the latest stable release, simply type:
 
 ```bash
-$ pip install pte-stats
+$ pip install pte
 ```
 
 ### Development version
 
-To install the latest delevopment version, first clone this repository using [git](https://git-scm.com/):
+To install the latest development version, first clone this repository using
+[git](https://git-scm.com/):
 
 ```bash
 $ git clone https://github.com/richardkoehler/pte
 ```
 
-Use the package manager [conda](https://docs.conda.io/projects/conda/en/latest/index.html) to set up a new working environment. To do so navigate to the PTE root directory in your terminal and type:
+Use the package manager
+[conda](https://docs.conda.io/projects/conda/en/latest/index.html) to set up a
+new working environment. To do so navigate to the PTE root directory in your
+terminal and type:
 
 ```bash
 $ conda env create -f env.yml
 ```
 
-This will set up a new conda environment called ``pte``.
+This will set up a new conda environment called `pte`.
 
 To activate the environment then type:
 
@@ -44,7 +47,7 @@ To activate the environment then type:
 $ conda activate pte
 ```
 
-If you want to install pte-stats into an existing environment, type:
+If you want to install pte-stats into another existing environment, type:
 
 ```bash
 $ pip install -e .
@@ -54,18 +57,21 @@ $ pip install -e .
 
 ```python
 import pte
-
-# Examples
 ```
 
 ## Contributing
-Please feel free to contribute yourselves or to open an **issue** when you encounter a bug or would like to add a new feature.
 
-For any minor additions or bugfixes, you may simply create a **pull request**. 
+Please feel free to contribute yourselves or to open an **issue** when you
+encounter a bug or would like to add a new feature.
+
+For any minor additions or bugfixes, you may simply create a **pull request**.
 
-For any major changes, make sure to open an **issue** first. When you then create a pull request, be sure to **link the pull request** to the open issue in order to close the issue automatically after merging.
+For any major changes, make sure to open an **issue** first. When you then
+create a pull request, be sure to **link the pull request** to the open issue in
+order to close the issue automatically after merging.
 
-To contribute yourselves, consider installing the full conda development environment to include such tools as black, pylint and isort:
+To contribute yourselves, consider installing the full conda development
+environment to include such tools as black, pylint and isort:
 
 ```bash
 $ conda env create -f env_dev.yml
@@ -75,19 +81,24 @@ $ conda activate pte-dev
 Continuous Integration (CI) including automated testing are set up.
 
 ## License
+
 PTE is licensed under the [MIT license](license-url).
 
 <!-- MARKDOWN LINKS & IMAGES -->
 <!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
-[python-shield]: https://img.shields.io/static/v1?label=Python&message=3.10&logoColor=black&labelColor=grey&color=blue
+
+[python-shield]:
+  https://img.shields.io/static/v1?label=Python&message=3.10&logoColor=black&labelColor=grey&color=blue
 [python-url]: https://pypi.org/project/pte-stats/
-[homepage-shield]: https://img.shields.io/static/v1?label=Homepage&message=ICN&logoColor=black&labelColor=grey&color=9cf
-[homepage-url]: https://www.icneuromodulation.org/
-[contributors-shield]: https://img.shields.io/github/contributors/richardkoehler/pte.svg?
+[contributors-shield]:
+  https://img.shields.io/github/contributors/richardkoehler/pte.svg?
 [contributors-url]: https://github.com/richardkoehler/pte/graphs/contributors
-[license-shield]: https://img.shields.io/static/v1?label=License&message=MIT&logoColor=black&labelColor=grey&color=yellow
+[license-shield]:
+  https://img.shields.io/static/v1?label=License&message=MIT&logoColor=black&labelColor=grey&color=yellow
 [license-url]: https://github.com/richardkoehler/pte/blob/main/LICENSE/
-[codestyle-shield]: https://img.shields.io/static/v1?label=CodeStyle&message=black&logoColor=black&labelColor=grey&color=black
+[codestyle-shield]:
+  https://img.shields.io/static/v1?label=CodeStyle&message=black&logoColor=black&labelColor=grey&color=black
 [codestyle-url]: https://github.com/psf/black
-[pypi-shield]: https://img.shields.io/static/v1?label=PyPi&message=v0.1.0&logoColor=black&labelColor=grey&color=blue
-[pypi-url]: https://pypi.org/project/pte/
\ No newline at end of file
+[pypi-shield]:
+  https://img.shields.io/static/v1?label=PyPi&message=v0.1.0&logoColor=black&labelColor=grey&color=blue
+[pypi-url]: https://pypi.org/project/pte/
diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md
deleted file mode 100644
index b0cbb1c..0000000
--- a/doc/CHANGELOG.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Changelog
-All notable changes to this project will be documented in this file.
-
-The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
-and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-
-## [Unreleased]
-### Added
-### Fixed
-### Changed
-### Deprecated
-### Removed
-
-
-## [0.1.0] - 2022-02-01
-### Added
-- Initial release
-
-[Unreleased]: https://github.com/richardkoehler/pte
\ No newline at end of file
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 100644
index 0000000..b4427da
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to
+[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Unreleased
+
+### Added
+
+### Fixed
+
+### Changed
+
+### Deprecated
+
+### Removed
+
+## [v0.2.0](https://github.com/richardkoehler/pte/releases/tag/v0.2.0) - 2024-02-21
+
+- Minor additions
+
+## [v0.1.0](https://github.com/richardkoehler/pte/releases/tag/v0.1.0) - 2022-02-01
+
+### Added
+
+- Initial release
+
+[Unreleased]: https://github.com/richardkoehler/pte
diff --git a/env.yml b/env.yml
index 2002320..d5df8bf 100644
--- a/env.yml
+++ b/env.yml
@@ -3,6 +3,6 @@ channels:
   - defaults
 dependencies:
   - pip
-  - python=3.10
+  - python>=3.10
   - pip:
-    - pte
\ No newline at end of file
+      - .
diff --git a/env_dev.yml b/env_dev.yml
index edae30c..7e0e6c5 100644
--- a/env_dev.yml
+++ b/env_dev.yml
@@ -3,6 +3,6 @@ channels:
   - defaults
 dependencies:
   - pip
-  - python=3.10
+  - python>=3.10
   - pip:
-    - pte[dev]
\ No newline at end of file
+      - -e .[dev]
diff --git a/pylintrc b/pylintrc
deleted file mode 100644
index 8f30694..0000000
--- a/pylintrc
+++ /dev/null
@@ -1,429 +0,0 @@
-# This Pylint rcfile contains a best-effort configuration to uphold the
-# best-practices and style described in the Google Python style guide:
-#   https://google.github.io/styleguide/pyguide.html
-#
-# Its canonical open-source location is:
-#   https://google.github.io/styleguide/pylintrc
-
-[MASTER]
-
-# Files or directories to be skipped. They should be base names, not paths.
-ignore=third_party
-
-# Files or directories matching the regex patterns are skipped. The regex
-# matches against base names, not paths.
-ignore-patterns=
-
-# Pickle collected data for later comparisons.
-persistent=no
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-# Use multiple processes to speed up Pylint.
-jobs=4
-
-# Allow loading of arbitrary C extensions. Extensions are imported into the
-# active Python interpreter and may run arbitrary code.
-unsafe-load-any-extension=no
-
-
-[MESSAGES CONTROL]
-
-# Only show warnings with the listed confidence levels. Leave empty to show
-# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
-confidence=
-
-# Enable the message, report, category or checker with the given id(s). You can
-# either give multiple identifier separated by comma (,) or put this option
-# multiple time (only on the command line, not in the configuration file where
-# it should appear only once). See also the "--disable" option for examples.
-#enable=
-
-# Disable the message, report, category or checker with the given id(s). You
-# can either give multiple identifiers separated by comma (,) or put this
-# option multiple times (only on the command line, not in the configuration
-# file where it should appear only once).You can also use "--disable=all" to
-# disable everything first and then reenable specific checks. For example, if
-# you want to run only the similarities checker, you can use "--disable=all
-# --enable=similarities". If you want to run only the classes checker, but have
-# no Warning level messages displayed, use"--disable=all --enable=classes
-# --disable=W"
-disable=abstract-method,
-        apply-builtin,
-        arguments-differ,
-        attribute-defined-outside-init,
-        backtick,
-        bad-option-value,
-        basestring-builtin,
-        buffer-builtin,
-        c-extension-no-member,
-        consider-using-enumerate,
-        cmp-builtin,
-        cmp-method,
-        coerce-builtin,
-        coerce-method,
-        delslice-method,
-        div-method,
-        duplicate-code,
-        eq-without-hash,
-        execfile-builtin,
-        file-builtin,
-        filter-builtin-not-iterating,
-        fixme,
-        getslice-method,
-        global-statement,
-        hex-method,
-        idiv-method,
-        implicit-str-concat-in-sequence,
-        import-error,
-        import-self,
-        import-star-module-level,
-        inconsistent-return-statements,
-        input-builtin,
-        intern-builtin,
-        invalid-str-codec,
-        locally-disabled,
-        long-builtin,
-        long-suffix,
-        map-builtin-not-iterating,
-        misplaced-comparison-constant,
-        missing-function-docstring,
-        metaclass-assignment,
-        next-method-called,
-        next-method-defined,
-        no-absolute-import,
-        no-else-break,
-        no-else-continue,
-        no-else-raise,
-        no-else-return,
-        no-init,  # added
-        no-member,
-        no-name-in-module,
-        no-self-use,
-        nonzero-method,
-        oct-method,
-        old-division,
-        old-ne-operator,
-        old-octal-literal,
-        old-raise-syntax,
-        parameter-unpacking,
-        print-statement,
-        raising-string,
-        range-builtin-not-iterating,
-        raw_input-builtin,
-        rdiv-method,
-        reduce-builtin,
-        relative-import,
-        reload-builtin,
-        round-builtin,
-        setslice-method,
-        signature-differs,
-        standarderror-builtin,
-        suppressed-message,
-        sys-max-int,
-        too-few-public-methods,
-        too-many-ancestors,
-        too-many-arguments,
-        too-many-boolean-expressions,
-        too-many-branches,
-        too-many-instance-attributes,
-        too-many-locals,
-        too-many-nested-blocks,
-        too-many-public-methods,
-        too-many-return-statements,
-        too-many-statements,
-        trailing-newlines,
-        unichr-builtin,
-        unicode-builtin,
-        unnecessary-pass,
-        unpacking-in-except,
-        useless-else-on-loop,
-        useless-object-inheritance,
-        useless-suppression,
-        using-cmp-argument,
-        wrong-import-order,
-        xrange-builtin,
-        zip-builtin-not-iterating,
-
-
-[REPORTS]
-
-# Set the output format. Available formats are text, parseable, colorized, msvs
-# (visual studio) and html. You can also give a reporter class, eg
-# mypackage.mymodule.MyReporterClass.
-output-format=text
-
-# Tells whether to display a full report or only the messages
-reports=no
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note). You have access to the variables errors warning, statement which
-# respectively contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (RP0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Template used to display messages. This is a python new-style format string
-# used to format the message information. See doc for all details
-#msg-template=
-
-
-[BASIC]
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=main,_
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=
-
-# Colon-delimited sets of names that determine each other's naming style when
-# the name regexes allow several styles.
-name-group=
-
-# Include a hint for the correct naming format with invalid-name
-include-naming-hint=no
-
-# List of decorators that produce properties, such as abc.abstractproperty. Add
-# to this list to register other decorators that produce valid properties.
-property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl
-
-# Regular expression matching correct function names
-function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$
-
-# Regular expression matching correct variable names
-variable-rgx=^[a-z][a-z0-9_]*$
-
-# Regular expression matching correct constant names
-const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
-
-# Regular expression matching correct attribute names
-attr-rgx=^_{0,2}[a-z][a-z0-9_]*$
-
-# Regular expression matching correct argument names
-argument-rgx=^[a-z][a-z0-9_]*$
-
-# Regular expression matching correct class attribute names
-class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$
-
-# Regular expression matching correct inline iteration names
-inlinevar-rgx=^[a-z][a-z0-9_]*$
-
-# Regular expression matching correct class names
-class-rgx=^_?[A-Z][a-zA-Z0-9]*$
-
-# Regular expression matching correct module names
-module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$
-
-# Regular expression matching correct method names
-method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$
-
-# Regular expression which should only match function or class names that do
-# not require a docstring.
-no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$
-
-# Minimum line length for functions/classes that require docstrings, shorter
-# ones are exempt.
-docstring-min-length=10
-
-
-[TYPECHECK]
-
-# List of decorators that produce context managers, such as
-# contextlib.contextmanager. Add to this list to register other decorators that
-# produce valid context managers.
-contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager
-
-# Tells whether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# List of module names for which member attributes should not be checked
-# (useful for modules/projects where namespaces are manipulated during runtime
-# and thus existing member attributes cannot be deduced by static analysis. It
-# supports qualified module names, as well as Unix pattern matching.
-ignored-modules=
-
-# List of class names for which member attributes should not be checked (useful
-# for classes with dynamically set attributes). This supports the use of
-# qualified names.
-ignored-classes=optparse.Values,thread._local,_thread._local
-
-# List of members which are set dynamically and missed by pylint inference
-# system, and so shouldn't trigger E1101 when accessed. Python regular
-# expressions are accepted.
-generated-members=
-
-
-[FORMAT]
-
-# Maximum number of characters on a single line.
-max-line-length=79
-
-# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt
-# lines made too long by directives to pytype.
-
-# Regexp for a line that is allowed to be longer than the limit.
-ignore-long-lines=(?x)(
-  ^\s*(\#\ )?<?https?://\S+>?$|
-  ^\s*(from\s+\S+\s+)?import\s+.+$)
-
-# Allow the body of an if to be on the same line as the test if there is no
-# else.
-single-line-if-stmt=yes
-
-# Maximum number of lines in a module
-max-module-lines=99999
-
-# String used as indentation unit.  The internal Google style guide mandates 2
-# spaces.  Google's externaly-published style guide says 4, consistent with
-# PEP 8.  Here, we use 2 spaces, for conformity with many open-sourced Google
-# projects (like TensorFlow).
-indent-string='    '
-
-# Number of spaces of indent required inside a hanging  or continued line.
-indent-after-paren=4
-
-# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
-expected-line-ending-format=
-
-
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=TODO
-
-
-[STRING]
-
-# This flag controls whether inconsistent-quotes generates a warning when the
-# character used as a quote delimiter is used inconsistently within a module.
-check-quote-consistency=yes
-
-
-[VARIABLES]
-
-# Tells whether we should check for unused import in __init__ files.
-init-import=no
-
-# A regular expression matching the name of dummy variables (i.e. expectedly
-# not used).
-dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_)
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-# List of strings which can identify a callback function by name. A callback
-# name must start or end with one of those strings.
-callbacks=cb_,_cb
-
-# List of qualified module names which can have objects that can redefine
-# builtins.
-redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools
-
-
-[LOGGING]
-
-# Logging modules to check that the string format arguments are in logging
-# function parameter format
-logging-modules=logging,absl.logging,tensorflow.io.logging
-
-
-[SIMILARITIES]
-
-# Minimum lines number of a similarity.
-min-similarity-lines=4
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-# Ignore imports when computing similarities.
-ignore-imports=no
-
-
-[SPELLING]
-
-# Spelling dictionary name. Available dictionaries: none. To make it working
-# install python-enchant package.
-spelling-dict=
-
-# List of comma separated words that should not be checked.
-spelling-ignore-words=
-
-# A path to a file that contains private dictionary; one word per line.
-spelling-private-dict-file=
-
-# Tells whether to store unknown words to indicated private dictionary in
-# --spelling-private-dict-file option instead of raising a message.
-spelling-store-unknown-words=no
-
-
-[IMPORTS]
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=regsub,
-                   TERMIOS,
-                   Bastion,
-                   rexec,
-                   sets
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report RP0402 must not be disabled)
-import-graph=
-
-# Create a graph of external dependencies in the given file (report RP0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of internal dependencies in the given file (report RP0402 must
-# not be disabled)
-int-import-graph=
-
-# Force import order to recognize a module as part of the standard
-# compatibility libraries.
-known-standard-library=
-
-# Force import order to recognize a module as part of a third party library.
-known-third-party=enchant, absl
-
-# Analyse import fallback blocks. This can be used to support both Python 2 and
-# 3 compatible code, which means that the block might have code that exists
-# only in one or another interpreter, leading to false positives when analysed.
-analyse-fallback-blocks=no
-
-
-[CLASSES]
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,
-                      __new__,
-                      setUp
-
-# List of member names, which should be excluded from the protected access
-# warning.
-exclude-protected=_asdict,
-                  _fields,
-                  _replace,
-                  _source,
-                  _make
-
-# List of valid names for the first argument in a class method.
-valid-classmethod-first-arg=cls,
-                            class_
-
-# List of valid names for the first argument in a metaclass class method.
-valid-metaclass-classmethod-first-arg=mcs
-
-
-[EXCEPTIONS]
-
-# Exceptions that will emit a warning when being caught. Defaults to
-# "Exception"
-overgeneral-exceptions=StandardError,
-                       Exception,
-                       BaseException
diff --git a/pyproject.toml b/pyproject.toml
index a0fa777..d8b8e0e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [build-system]
-build-backend = "flit_core.buildapi"
-requires = ["flit_core >=3.5,<4"]
+build-backend = "hatchling.build"
+requires = ["hatchling"]
 
 [project]
 authors = [
@@ -8,81 +8,115 @@ authors = [
 ]
 classifiers = [
   "Development Status :: 2 - Pre-Alpha",
+  "Intended Audience :: Science/Research",
+  "Intended Audience :: Developers",
   "License :: OSI Approved :: MIT License",
+  "Operating System :: OS Independent",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3 :: Only",
   "Programming Language :: Python :: 3.10",
   "Topic :: Software Development :: Libraries :: Python Modules",
+  "Topic :: Scientific/Engineering",
+  "Typing :: Typed",
 ]
-description = "PTE is an open-source software package for working with electrophysiological data"
+description = "An open-source software package for working with electrophysiological data"
 dynamic = ["version"]
-keywords = ["eeg", "ieeg", "dbs", "ecog", "lfp"]
+keywords = ["eeg", "ieeg", "dbs", "ecog", "lfp", "neurophysiology"]
 license = {file = "LICENSE"}
 maintainers = [
   {name = "Richard M. Köhler"},
 ]
 name = "pte"
-readme = "README.md"
+readme = {file = "README.md", content-type = "text/markdown"}
 requires-python = ">=3.10"
 
 dependencies = [
   "joblib",
   "matplotlib",
-  "mne",
+  "mne[hdf5]>=1.0",
   "mne-bids",
   "numba",
   "numpy",
   "pandas",
   "plotly",
   "pte-stats",
+  "pybv",
   "scikit-image",
   "scipy",
+  "mne-qt-browser",
+  "pyqt5",
 ]
 
 [project.optional-dependencies]
 dev = [
   "black",
-  "isort",
+  "hatch",
   "mypy",
-  "pylint",
+  "packaging",
+  "pandas-stubs",
+  "pre-commit",
   "pytest",
   "pytest-cov",
+  "ruff",
   "tox",
 ]
 
 [project.urls]
 bugtracker = "https://github.com/richardkoehler/pte/issues"
-changelog = "https://github.com/richardkoehler/pte/blob/main/doc/CHANGELOG.md"
+changelog = "https://github.com/richardkoehler/pte/blob/main/docs/CHANGELOG.md"
 repository = "https://github.com/richardkoehler/pte"
 
 [tool.black]
-exclude = '''
-/(
-    __pycache__
-  | plotting/resources
-)/
-
-'''
-include = '''
-(
-    /tests
-  | /src
-)
-'''
 line-length = 79
-target-version = ['py310']
 
-[tool.isort]
-line_length = 79
-profile = "black"
-py_version = 310
-src_paths = ["src"]
-
-[tool.mypy]
-python_version = "3.10"
+[tool.hatch]
+version.path = "src/pte/__init__.py"
 
 [[tool.mypy.overrides]]
 ignore_missing_imports = true
 module = [
+  "joblib",
+  "matplotlib",
+  "matplotlib.figure",
+  "mne",
+  "mne_bids",
+  "mne_bids.copyfiles",
+  "mne_bids.path",
+  "mpl_toolkits.axes_grid1",
   "numba",
+  "numpy",
+  "pandas",
+  "plotly",
+  "pte_stats",
+  "pybv",
+  "scipy",
+  "scipy.io",
+  "scipy.ndimage",
+  "scipy.signal",
+  "scipy.stats",
   "skimage",
   "statsmodels.stats.multitest",
 ]
+
+[tool.ruff]
+line-length = 79
+
+[tool.ruff.lint]
+select = [
+    # pycodestyle
+    "E",
+    # Pyflakes
+    "F",
+    # pyupgrade
+    "UP",
+    # flake8-bugbear
+    "B",
+    # flake8-simplify
+    "SIM",
+    # isort
+    "I",
+]
+
+[tool.ruff.per-file-ignores]
+"__init__.py" = ["F401", "F403"]
diff --git a/requirements/requirements.txt b/requirements/requirements.txt
deleted file mode 100644
index 432b0b2..0000000
--- a/requirements/requirements.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-imageio==2.14.1
-joblib==1.1.0
-llvmlite==0.38.0
-networkx==2.6.3
-numba==0.55.1
-numpy==1.21.5
-packaging==21.3
-pandas==1.4.0
-patsy==0.5.2
-Pillow==9.0.1
-pybv
-pyparsing==3.0.7
-python-dateutil==2.8.2
-pytz==2021.3
-PyWavelets==1.2.0
-scikit-image==0.19.1
-scipy==1.7.3
-six==1.16.0
-statsmodels==0.13.1
-tifffile==2021.11.2
diff --git a/requirements/requirements_deploy.txt b/requirements/requirements_deploy.txt
deleted file mode 100644
index 598d638..0000000
--- a/requirements/requirements_deploy.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-certifi==2021.10.8
-charset-normalizer==2.0.11
-docutils==0.18.1
-flit==3.6.0
-flit_core==3.6.0
-idna==3.3
-imageio==2.14.1
-joblib==1.1.0
-llvmlite==0.38.0
-networkx==2.6.3
-numba==0.55.1
-numpy==1.21.5
-packaging==21.3
-pandas==1.4.0
-patsy==0.5.2
-Pillow==9.0.1
-pybv
-pyparsing==3.0.7
-python-dateutil==2.8.2
-pytz==2021.3
-PyWavelets==1.2.0
-requests==2.27.1
-scikit-image==0.19.1
-scipy==1.7.3
-six==1.16.0
-statsmodels==0.13.1
-tifffile==2021.11.2
-tomli==2.0.0
-tomli_w==1.0.0
-urllib3==1.26.8
diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt
deleted file mode 100644
index e5b5670..0000000
--- a/requirements/requirements_dev.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-joblib
-matplotlib
-mne[hdf5]
-mne-bids
-numba
-numpy
-pandas
-plotly
-pte-stats
-pybv
-scikit-image
-scipy
-black
-isort
-mypy
-pylint
-pytest
-pytest-cov
-tox
\ No newline at end of file
diff --git a/scripts/prepare_changelog.py b/scripts/prepare_changelog.py
new file mode 100644
index 0000000..390397e
--- /dev/null
+++ b/scripts/prepare_changelog.py
@@ -0,0 +1,41 @@
+from datetime import datetime
+from pathlib import Path
+
+from pte_stats import __version__
+
+
+def main():
+    changelog = Path(r"docs/CHANGELOG.md")
+
+    with changelog.open() as f:
+        lines = f.readlines()
+
+    insert_index: int = -1
+    for i in range(len(lines)):
+        line = lines[i]
+        if line.startswith("## Unreleased"):
+            insert_index = i + 1
+        elif line.startswith(f"## [v{__version__}]"):
+            print("CHANGELOG already up-to-date")
+            return
+        elif line.startswith("## [v"):
+            break
+
+    if insert_index < 0:
+        raise RuntimeError("Couldn't find 'Unreleased' section")
+
+    lines.insert(insert_index, "\n")
+    lines.insert(
+        insert_index + 1,
+        f"## [v{__version__}]"
+        "(https://github.com/richardkoehler/pte/releases"
+        f"/tag/v{__version__}) - "
+        f"{datetime.now().strftime('%Y-%m-%d')}\n",
+    )
+
+    with changelog.open("w") as f:
+        f.writelines(lines)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/release.sh b/scripts/release.sh
new file mode 100644
index 0000000..659cab6
--- /dev/null
+++ b/scripts/release.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -e
+
+TAG=$(python -c 'from pte_stats import __version__; print("v" + __version__)')
+
+read -p "Creating new release for $TAG. Do you want to continue? [Y/n] " prompt
+
+if [[ $prompt == "y" || $prompt == "Y" || $prompt == "yes" || $prompt == "Yes" ]]; then
+    python scripts/prepare_changelog.py
+    git add -A
+    git commit -m "Bump version to $TAG for release" || true && git push
+    echo "Creating new git tag $TAG"
+    git tag "$TAG" -m "$TAG"
+    git push --tags
+else
+    echo "Cancelled"
+    exit 1
+fi
diff --git a/scripts/release_notes.py b/scripts/release_notes.py
new file mode 100644
index 0000000..ad806d5
--- /dev/null
+++ b/scripts/release_notes.py
@@ -0,0 +1,83 @@
+"""
+Prepares markdown release notes for GitHub releases.
+"""
+
+import os
+
+import packaging.version
+
+TAG = os.environ["TAG"]
+
+ADDED_HEADER = "### Added 🎉"
+CHANGED_HEADER = "### Changed ⚠️"
+FIXED_HEADER = "### Fixed ✅"
+REMOVED_HEADER = "### Removed 👋"
+
+
+def get_change_log_notes() -> str:
+    in_current_section = False
+    current_section_notes: list[str] = []
+    with open(r"docs/CHANGELOG.md") as changelog:
+        for line in changelog:
+            if line.startswith("## "):
+                if line.startswith("## Unreleased"):
+                    continue
+                if line.startswith(f"## [{TAG}]"):
+                    in_current_section = True
+                    continue
+                break
+            if in_current_section:
+                if line.startswith("### Added"):
+                    line = ADDED_HEADER + "\n"
+                elif line.startswith("### Changed"):
+                    line = CHANGED_HEADER + "\n"
+                elif line.startswith("### Fixed"):
+                    line = FIXED_HEADER + "\n"
+                elif line.startswith("### Removed"):
+                    line = REMOVED_HEADER + "\n"
+                current_section_notes.append(line)
+    assert current_section_notes
+    return "## What's new\n\n" + "".join(current_section_notes).strip() + "\n"
+
+
+def get_commit_history() -> str:
+    new_version = packaging.version.parse(TAG)
+
+    # Pull all tags.
+    os.popen("git fetch --tags")
+
+    # Get all tags sorted by version, latest first.
+    all_tags = (
+        os.popen("git tag -l --sort=-version:refname 'v*'").read().split("\n")
+    )
+
+    # Out of `all_tags`, find the latest previous version so that we can
+    # collect all commits between that version and the new version we're about
+    # to publish. Note that we ignore pre-releases unless the new version is
+    # also a pre-release.
+    last_tag: str | None = None
+    for tag in all_tags:
+        if not tag.strip():  # could be blank line
+            continue
+        version = packaging.version.parse(tag)
+        if new_version.pre is None and version.pre is not None:
+            continue
+        if version < new_version:
+            last_tag = tag
+            break
+    if last_tag is not None:
+        commits = os.popen(
+            f"git log {last_tag}..{TAG} --oneline --first-parent"
+        ).read()
+    else:
+        commits = os.popen("git log --oneline --first-parent").read()
+    return "## Commits\n\n" + commits
+
+
+def main():
+    print(get_change_log_notes())
+    print(get_commit_history())
+
+
+if __name__ == "__main__":
+    main()
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 4cf7f9c..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,20 +0,0 @@
-[metadata]
-name = pte
-version = 0.1.0
-
-[options]
-packages = pte
-python_requires = '>=3.9'
-install_requires =
-    requests
-    joblib
-    matplotlib
-    mne
-    mne-bids
-    numba
-    numpy
-    pandas
-    pip
-    plotly
-    scipy
-    statannotations
\ No newline at end of file
diff --git a/src/pte/__init__.py b/src/pte/__init__.py
index 1e767d1..203acda 100644
--- a/src/pte/__init__.py
+++ b/src/pte/__init__.py
@@ -1,5 +1,5 @@
 """Package for electrophysiological analyses."""
 
-__version__ = "0.1.0"
+__version__ = "0.2.0dev1"
 
 from . import filetools, pipelines, plotting, preprocessing, time_frequency
diff --git a/src/pte/filetools/__init__.py b/src/pte/filetools/__init__.py
index 5fff26d..ef72d42 100644
--- a/src/pte/filetools/__init__.py
+++ b/src/pte/filetools/__init__.py
@@ -7,5 +7,5 @@
     sub_med_stim_from_fname,
 )
 from .events import get_bad_epochs
-from .filefinder import get_filefinder
+from .filefinder import BIDSFinder, DefaultFinder, get_filefinder
 from .matlab import loadmat
diff --git a/src/pte/filetools/bids.py b/src/pte/filetools/bids.py
index 80ce6ae..87dd6db 100644
--- a/src/pte/filetools/bids.py
+++ b/src/pte/filetools/bids.py
@@ -270,13 +270,13 @@ def _rewrite_events(
     in_path = in_path.copy()
     in_path.update(suffix="events", extension=".tsv")
     out_path = out_path.copy()
-    out_path.update(suffix="events", extension="tsv")
+    out_path.update(suffix="events", extension=".tsv")
     shutil.copyfile(in_path.fpath, out_path.fpath)
 
 
 def _get_description(channel_type: str) -> str:
     """Get channel type description."""
-    description = defaultdict(lambda: "Other type of channel")
+    description: defaultdict = defaultdict(lambda: "Other type of channel")
     description.update(
         meggradaxial="Axial Gradiometer",
         megrefgradaxial="Axial Gradiometer Reference",
@@ -301,9 +301,9 @@ def _get_description(channel_type: str) -> str:
 
 def _get_group(channel_name: str) -> str:
     """Get group for corresponding channel name."""
-    sides = defaultdict(lambda: "")
+    sides: defaultdict = defaultdict(lambda: "")
     sides.update(L="left", R="right")
-    groups = defaultdict(lambda: "n/a")
+    groups: defaultdict = defaultdict(lambda: "n/a")
     groups.update(
         MEG="MEG",
         ACC="accelerometer",
@@ -339,33 +339,33 @@ def _rewrite_channels(
 
     channels_to_add = [ch for ch in channels_new if ch not in channels_old]
 
-    if not any((channels_to_add, channels_to_remove)):
-        return
-
-    if channels_to_remove:
-        data_old = data_old.drop(channels_to_remove)
-
-    if channels_to_add:
-        add_list = []
-        ch_types = raw.get_channel_types(picks=channels_to_add)
-        for ch_name, ch_type in zip(channels_to_add, ch_types):
-            add_dict = {
-                data_old.columns[i]: data_old.iloc[0][i]
-                for i in range(len(data_old.columns))
-            }
-            add_dict.update(
-                status="good",
-                description=_get_description(ch_type),
-                type=ch_type.upper(),
-                group=_get_group(ch_name),
-            )
-            add_list.append(add_dict)
-        print(f"Added channels: {channels_to_add}")
-        print(f"Added channel types: {ch_types}")
-
-        index = pd.Index(channels_to_add, name="name")
-        data_to_add = pd.DataFrame(add_list, index=index)
-        data_old = data_old.append(data_to_add, ignore_index=False)
+    if any((channels_to_add, channels_to_remove)):
+        if channels_to_remove:
+            data_old = data_old.drop(channels_to_remove)
+
+        if channels_to_add:
+            add_list = []
+            ch_types = raw.get_channel_types(picks=channels_to_add)
+            for ch_name, ch_type in zip(
+                channels_to_add, ch_types, strict=True
+            ):
+                add_dict = {
+                    data_old.columns[i]: data_old.iloc[0][i]
+                    for i in range(len(data_old.columns))
+                }
+                add_dict.update(
+                    status="good",
+                    description=_get_description(ch_type),
+                    type=ch_type.upper(),
+                    group=_get_group(ch_name),
+                )
+                add_list.append(add_dict)
+            print(f"Added channels: {channels_to_add}")
+            print(f"Added channel types: {ch_types}")
+
+            index = pd.Index(channels_to_add, name="name")
+            data_to_add = pd.DataFrame(add_list, index=index)
+            data_old = pd.concat([data_old, data_to_add], ignore_index=False)
 
     data_old = data_old.reindex(index=channels_new)
     data_old.to_csv(
@@ -385,7 +385,9 @@ def _rewrite_electrodes(file: str | Path, raw: mne.io.BaseRaw) -> None:
     electrodes_old = data_old.index.tolist()
 
     electrodes_to_add = []
-    for ch_name, ch_type in zip(raw.ch_names, raw.get_channel_types()):
+    for ch_name, ch_type in zip(
+        raw.ch_names, raw.get_channel_types(), strict=True
+    ):
         if (ch_name not in electrodes_old) and (
             ch_type in ("ecog", "dbs", "seeg", "eeg")
         ):
@@ -395,7 +397,7 @@ def _rewrite_electrodes(file: str | Path, raw: mne.io.BaseRaw) -> None:
         data=None, index=electrodes_to_add, columns=data_old.columns
     )
 
-    data_old = data_old.append(data_to_add, ignore_index=False)
+    data_old = pd.concat([data_old, data_to_add], ignore_index=False)
     data_old.sort_index(axis=0, inplace=True)
 
     data_old.to_csv(
diff --git a/src/pte/filetools/events.py b/src/pte/filetools/events.py
index 4727df4..6bd1a48 100644
--- a/src/pte/filetools/events.py
+++ b/src/pte/filetools/events.py
@@ -1,4 +1,5 @@
 """Module for handling event annotations."""
+
 from pathlib import Path
 
 import mne_bids
@@ -7,17 +8,15 @@
 
 def get_bad_epochs(
     filename: Path | str | mne_bids.BIDSPath,
-    bad_epochs_dir: Path | str | None,
+    bad_epochs_dir: Path | str,
 ) -> pd.DataFrame:
     """Get DataFrame of bad epochs from *_badepochs file."""
     if not isinstance(filename, mne_bids.BIDSPath):
         filename = mne_bids.get_bids_path_from_fname(Path(filename))
-    basename = (
-        filename.copy().update(suffix=None, datatype=None).basename
-    )
+    basename = filename.copy().update(suffix=None, datatype=None).basename
     full_path = Path(bad_epochs_dir, f"{basename}_badepochs.csv")
     bad_epochs = pd.read_csv(
-        full_path, 
-        dtype={"event_id": int, "event_description": str, "reason": str}
-        )
+        full_path,
+        dtype={"event_id": int, "event_description": str, "reason": str},
+    )
     return bad_epochs
diff --git a/src/pte/filetools/filefinder.py b/src/pte/filetools/filefinder.py
index f71a453..9e606d6 100644
--- a/src/pte/filetools/filefinder.py
+++ b/src/pte/filetools/filefinder.py
@@ -1,45 +1,242 @@
 """Find and filter files. Supports BIDSPath objects from `mne-bids`."""
 
+import os
+import shutil
+from abc import ABC, abstractmethod
 from collections.abc import Sequence
 from dataclasses import dataclass, field
 from pathlib import Path
-from typing import Any
+from typing import Any, Literal
 
 import mne_bids
 
-from pte.filetools.filefinder_abc import DirectoryNotFoundError, FileFinder
 
+@dataclass
+class FileFinder(ABC):
+    """Basic representation of class for finding and filtering files."""
 
-def get_filefinder(
-    datatype: str, hemispheres: dict | None = None, **kwargs
-) -> FileFinder:
-    """Create and return FileFinder of desired type.
+    hemispheres: dict[str, str] | None = field(default_factory=dict[str, str])
+    directory: Path | str = field(init=False)
 
-    Parameters
-    ----------
-    datatype : str
-        Allowed values for `datatype`: ["any", "bids"].
+    def __str__(self) -> str:
+        if not self.files:
+            return "No corresponding files found."
+        headers = ["Index", "Filename"]
+        col_width = max(len(os.path.basename(file)) for file in self.files)
+        format_row = f"{{:>{len(headers[0]) + 2}}}{{:>{col_width + 2}}}"
+        terminal_size = "\u2500" * shutil.get_terminal_size().columns
+        return "\n".join(
+            (
+                "Corresponding files found:",
+                "".join(
+                    f"{{:>{len(header) + 2}}}".format(header)
+                    for header in headers
+                ),
+                terminal_size,
+                *(
+                    format_row.format(idx, os.path.basename(file))
+                    for idx, file in enumerate(self.files)
+                ),
+            )
+        )
 
-    Returns
-    -------
-    FileFinder
-        Instance of FileFinder for reading given `datatype`.
+    def __len__(self) -> int:
+        if not self.files:
+            return 0
+        return len(self.files)
+
+    @abstractmethod
+    def find_files(
+        self,
+        directory: Path | str,
+        extensions: str | Sequence | None = None,
+        keywords: str | Sequence[str] | None = None,
+        hemisphere: str | None = None,
+        stimulation: str | None = None,
+        medication: str | None = None,
+        exclude: str | Sequence[str] | None = None,
+        verbose: bool | str | int | None = False,
+    ) -> None:
+        """Find files in directory with optional
+        keywords and extensions."""
+
+    @abstractmethod
+    def filter_files(
+        self,
+        keywords: str | Sequence[str] | None = None,
+        hemisphere: str | None = None,
+        stimulation: str | None = None,
+        medication: str | None = None,
+        exclude: str | Sequence[str] | None = None,
+        verbose: bool | str | int | None = False,
+    ) -> None:
+        """Filter list of filepaths for given parameters."""
+
+    @staticmethod
+    def _keyword_search(
+        files: list[str], keywords: str | Sequence[str] | None
+    ) -> list:
+        if not keywords:
+            return files
+        if isinstance(keywords, str):
+            keywords = [keywords]
+        filtered_files = [
+            file for file in files if any(key in file for key in keywords)
+        ]
+        return filtered_files
+
+    def _find_files(
+        self,
+        directory: Path | str,
+        extensions: str | Sequence | None = None,
+    ) -> None:
+        """Find files in directory with optional extensions.
+
+        Args:
+            directory (string)
+            keywords (list): e.g. ["SelfpacedRota", "ButtonPress] (optional)
+            extensions (list): e.g. [".json" or "tsv"] (optional)
+            verbose (bool): verbosity level (optional, default=True)
+        """
+
+        files = []
+        for root, _, fnames in os.walk(directory):
+            fnames = [os.path.join(root, file) for file in fnames]
+            fnames = self._keyword_search(fnames, extensions)
+            if fnames:
+                files.extend(fnames)
+        self.files = files
+
+    def _filter_files(
+        self,
+        keywords: str | Sequence[str] | None = None,
+        hemisphere: str | None = None,
+        stimulation: str | None = None,
+        medication: str | None = None,
+        exclude: str | Sequence[str] | None = None,
+    ) -> None:
+        """Filter filepaths for given parameters."""
+        filtered_files = self.files
+        if exclude:
+            if isinstance(exclude, str):
+                exclude = [exclude]
+            filtered_files = [
+                file
+                for file in filtered_files
+                if not any(item in file for item in exclude)
+            ]
+        if keywords:
+            if isinstance(keywords, str):
+                keywords = [keywords]
+            filtered_files = self._keyword_search(filtered_files, keywords)
+        if stimulation:
+            if stimulation.lower() in "stimon":
+                stim = "StimOn"
+            elif stimulation.lower() in "stimoff":
+                stim = "StimOff"
+            else:
+                raise ValueError("Keyword for stimulation not valid.")
+            filtered_files = self._keyword_search(filtered_files, [stim])
+        if medication:
+            if medication.lower() in "medon":
+                med = "MedOn"
+            elif medication.lower() in "medoff":
+                med = "MedOff"
+            else:
+                raise ValueError("Keyword for medication not valid.")
+            filtered_files = self._keyword_search(filtered_files, [med])
+        if hemisphere is not None:
+            if hemisphere.lower() not in ("ipsilateral", "contralateral"):
+                raise ValueError(
+                    "Keyword for hemisphere not valid. Must be one of either"
+                    f"'ipsilateral' or 'contralateral'. Got: {hemisphere}."
+                )
+            hemisphere = hemisphere.lower()
+            matching_files = []
+            for file in filtered_files:
+                entities = mne_bids.get_entities_from_fname(file)
+                subject = entities["subject"]
+                task: str = entities["task"]
+                assert (
+                    self.hemispheres is not None
+                ), "self.hemispheres must be specified."
+                if (
+                    subject not in self.hemispheres
+                    or self.hemispheres[subject] is None
+                ):
+                    raise HemisphereNotSpecifiedError(
+                        subject, self.hemispheres
+                    )
+                hem_sub: str = self.hemispheres[subject]
+                if hemisphere == "ipsilateral" and task.endswith(hem_sub):
+                    matching_files.append(file)
+                if hemisphere == "contralateral" and not task.endswith(
+                    hem_sub
+                ):
+                    matching_files.append(file)
+            filtered_files = matching_files
+        self.files = filtered_files
+
+
+class DirectoryNotFoundError(Exception):
+    """Exception raised when invalid Reader is passed.
+
+    Attributes:
+        directory -- input directory which caused the error
     """
-    finders = {
-        "any": DefaultFinder,
-        "bids": BIDSFinder,
-    }
-    datatype = datatype.lower()
-    if datatype not in finders:
-        raise FinderNotFoundError(datatype, finders)
 
-    return finders[datatype](hemispheres=hemispheres, **kwargs)
+    def __init__(
+        self,
+        directory: Path | str,
+        message: str = "Input directory was not found.",
+    ) -> None:
+        self.directory = directory
+        self.message = message
+        super().__init__(self.message)
+
+    def __str__(self) -> str:
+        return "\n".join((f"{self.message}", f"Got: {self.directory}."))
+
+
+class HemisphereNotSpecifiedError(Exception):
+    """Exception raised when electrode hemisphere is not specified in settings.
+
+    Attributes:
+        subject -- input subject which caused the error
+        hemisphere -- specified hemispheres
+        message -- explanation of the error
+    """
+
+    def __init__(
+        self,
+        subject: str,
+        hemispheres: dict[str, str],
+        message: str = (
+            "Input ECOG hemisphere is not specified in"
+            " `filefinder_settings.py` for given subject."
+        ),
+    ) -> None:
+        self.subject = subject
+        self.hemispheres = hemispheres
+        self.message = message
+        super().__init__(self.message)
+
+    def __str__(self) -> str:
+        return "\n".join(
+            (
+                self.message,
+                f"Unspecified subject: {self.subject}.",
+                f"Specified hemispheres: {self.hemispheres}.",
+            )
+        )
 
 
 @dataclass
 class DefaultFinder(FileFinder):
     """Class for finding and handling any type of file."""
 
+    files: list[str] = field(init=False, default_factory=list)
+
     def __iter__(self):
         self._n = 0
         return self
@@ -54,13 +251,13 @@ def __next__(self) -> Any:
     def find_files(
         self,
         directory: Path | str,
-        extensions: Sequence | str | None = None,
-        keywords: list[str] | str | None = None,
+        extensions: str | Sequence | None = None,
+        keywords: str | Sequence[str] | None = None,
         hemisphere: str | None = None,
         stimulation: str | None = None,
         medication: str | None = None,
-        exclude: str | None = None,
-        verbose: bool = False,
+        exclude: str | Sequence[str] | None = None,
+        verbose: bool | str | int | None = False,
     ) -> None:
         """Find files in directory with optional
         keywords and extensions.
@@ -87,12 +284,12 @@ def find_files(
 
     def filter_files(
         self,
-        keywords: list | None = None,
+        keywords: str | Sequence[str] | None = None,
         hemisphere: str | None = None,
         stimulation: str | None = None,
         medication: str | None = None,
-        exclude: str | None = None,
-        verbose: bool = False,
+        exclude: str | Sequence[str] | None = None,
+        verbose: bool | str | int | None = False,
     ) -> None:
         """Filter filepaths for given parameters and return filtered list."""
         self._filter_files(
@@ -111,17 +308,18 @@ class BIDSFinder(FileFinder):
     """Class for finding and handling data files in BIDS-compliant format."""
 
     bids_root: str = field(init=False)
+    files: list[mne_bids.BIDSPath] = field(init=False, default_factory=list)
 
     def find_files(
         self,
-        directory: str,
-        extensions: Sequence | str | None = (".vhdr", ".edf"),
-        keywords: list | None = None,
+        directory: Path | str,
+        extensions: str | Sequence | None = (".vhdr", ".edf"),
+        keywords: str | Sequence[str] | None = None,
         hemisphere: str | None = None,
         stimulation: str | None = None,
         medication: str | None = None,
-        exclude: str | None = None,
-        verbose: bool = False,
+        exclude: str | Sequence[str] | None = None,
+        verbose: bool | str | int | None = False,
     ) -> None:
         """Find files in directory with optional keywords and extensions.
 
@@ -148,12 +346,12 @@ def find_files(
 
     def filter_files(
         self,
-        keywords: list | None = None,
+        keywords: str | Sequence[str] | None = None,
         hemisphere: str | None = None,
         stimulation: str | None = None,
         medication: str | None = None,
-        exclude: str | None = None,
-        verbose: bool = False,
+        exclude: str | Sequence[str] | None = None,
+        verbose: bool | str | int | None = False,
     ) -> None:
         """Filter list of filepaths for given parameters."""
         self.files = [str(file.fpath.resolve()) for file in self.files]
@@ -171,7 +369,6 @@ def filter_files(
     def _make_bids_paths(
         self, filepaths: list[str]
     ) -> list[mne_bids.BIDSPath]:
-
         """Create list of mne-bids BIDSPath objects from list of filepaths."""
         bids_paths = []
         for filepath in filepaths:
@@ -216,3 +413,28 @@ def __str__(self) -> str:
             f"{self.message} Allowed values: {self.finders}."
             f" Got: {self.datatype}."
         )
+
+
+def get_filefinder(
+    datatype: Literal["any", "bids"], hemispheres: dict | None = None, **kwargs
+) -> FileFinder:
+    """Create and return FileFinder of desired type.
+
+    Parameters
+    ----------
+    datatype : str
+        Allowed values for `datatype`: ["any", "bids"].
+
+    Returns
+    -------
+    FileFinder
+        Instance of FileFinder for reading given `datatype`.
+    """
+    finders = {
+        "any": DefaultFinder,
+        "bids": BIDSFinder,
+    }
+    if datatype not in finders:
+        raise FinderNotFoundError(datatype, finders)
+
+    return finders[datatype](hemispheres=hemispheres, **kwargs)
diff --git a/src/pte/filetools/filefinder_abc.py b/src/pte/filetools/filefinder_abc.py
deleted file mode 100644
index 1ecad7a..0000000
--- a/src/pte/filetools/filefinder_abc.py
+++ /dev/null
@@ -1,218 +0,0 @@
-"""Define abstract base classes to construct FileFinder classes."""
-
-import os
-import shutil
-from abc import ABC, abstractmethod
-from collections.abc import Sequence
-from dataclasses import dataclass, field
-from pathlib import Path
-
-import mne_bids
-
-
-@dataclass
-class FileFinder(ABC):
-    """Basic representation of class for finding and filtering files."""
-
-    hemispheres: dict[str, str] | None = field(default_factory=dict)
-    directory: Path | str = field(init=False)
-    files: list[str] = field(init=False, default_factory=list)
-
-    def __str__(self) -> str:
-        if not self.files:
-            return "No corresponding files found."
-        headers = ["Index", "Filename"]
-        col_width = max(len(os.path.basename(file)) for file in self.files)
-        format_row = f"{{:>{len(headers[0]) + 2}}}{{:>{col_width + 2}}}"
-        terminal_size = "\u2500" * shutil.get_terminal_size().columns
-        return "\n".join(
-            (
-                "Corresponding files found:",
-                "".join(
-                    f"{{:>{len(header) + 2}}}".format(header)
-                    for header in headers
-                ),
-                terminal_size,
-                *(
-                    format_row.format(idx, os.path.basename(file))
-                    for idx, file in enumerate(self.files)
-                ),
-            )
-        )
-
-    def __len__(self) -> int:
-        if not self.files:
-            return 0
-        return len(self.files)
-
-    @abstractmethod
-    def find_files(
-        self,
-        directory: Path | str,
-        extensions: str | Sequence | None = None,
-        keywords: str | Sequence[str] | None = None,
-        hemisphere: str | None = None,
-        stimulation: str | None = None,
-        medication: str | None = None,
-        exclude: str | Sequence[str] | None = None,
-        verbose: bool = False,
-    ) -> None:
-        """Find files in directory with optional
-        keywords and extensions."""
-
-    @abstractmethod
-    def filter_files(
-        self,
-        keywords: str | Sequence[str] | None = None,
-        hemisphere: str | None = None,
-        stimulation: str | None = None,
-        medication: str | None = None,
-        exclude: str | Sequence[str] | None = None,
-        verbose: bool = False,
-    ) -> None:
-        """Filter list of filepaths for given parameters."""
-
-    @staticmethod
-    def _keyword_search(
-        files: list[str], keywords: str | Sequence[str] | None
-    ) -> list:
-        if not keywords:
-            return files
-        if isinstance(keywords, str):
-            keywords = [keywords]
-        filtered_files = [
-            file for file in files if any(key in file for key in keywords)
-        ]
-        return filtered_files
-
-    def _find_files(
-        self,
-        directory: Path | str,
-        extensions: str | Sequence | None = None,
-    ) -> None:
-        """Find files in directory with optional extensions.
-
-        Args:
-            directory (string)
-            keywords (list): e.g. ["SelfpacedRota", "ButtonPress] (optional)
-            extensions (list): e.g. [".json" or "tsv"] (optional)
-            verbose (bool): verbosity level (optional, default=True)
-        """
-
-        files = []
-        for root, _, fnames in os.walk(directory):
-            fnames = [os.path.join(root, file) for file in fnames]
-            fnames = self._keyword_search(fnames, extensions)
-            if fnames:
-                files.extend(fnames)
-        self.files = files
-
-    def _filter_files(
-        self,
-        keywords: str | Sequence[str] | None = None,
-        hemisphere: str | None = None,
-        stimulation: str | None = None,
-        medication: str | None = None,
-        exclude: str | Sequence[str] | None = None,
-    ) -> None:
-        """Filter filepaths for given parameters."""
-        filtered_files = self.files
-        if exclude:
-            if isinstance(exclude, str):
-                exclude = [exclude]
-            filtered_files = [
-                file
-                for file in filtered_files
-                if not any(item in file for item in exclude)
-            ]
-        if keywords:
-            if isinstance(keywords, str):
-                keywords = [keywords]
-            filtered_files = self._keyword_search(filtered_files, keywords)
-        if stimulation:
-            if stimulation.lower() in "stimon":
-                stim = "StimOn"
-            elif stimulation.lower() in "stimoff":
-                stim = "StimOff"
-            else:
-                raise ValueError("Keyword for stimulation not valid.")
-            filtered_files = self._keyword_search(filtered_files, [stim])
-        if medication:
-            if medication.lower() in "medon":
-                med = "MedOn"
-            elif medication.lower() in "medoff":
-                med = "MedOff"
-            else:
-                raise ValueError("Keyword for medication not valid.")
-            filtered_files = self._keyword_search(filtered_files, [med])
-        if hemisphere is not None:
-            matching_files = []
-            for file in filtered_files:
-                subject = mne_bids.get_entities_from_fname(file)["subject"]
-                if (
-                    subject not in self.hemispheres
-                    or self.hemispheres[subject] is None
-                ):
-                    raise HemisphereNotSpecifiedError(
-                        subject, self.hemispheres
-                    )
-                hem = self.hemispheres[subject] + "_"
-                if hemisphere.lower() in "ipsilateral" and hem in file:
-                    matching_files.append(file)
-                if hemisphere.lower() in "contralateral" and hem not in file:
-                    matching_files.append(file)
-            filtered_files = matching_files
-        self.files = filtered_files
-
-
-class DirectoryNotFoundError(Exception):
-    """Exception raised when invalid Reader is passed.
-
-    Attributes:
-        directory -- input directory which caused the error
-    """
-
-    def __init__(
-        self,
-        directory: Path | str,
-        message: str = "Input directory was not found.",
-    ) -> None:
-        self.directory = directory
-        self.message = message
-        super().__init__(self.message)
-
-    def __str__(self) -> str:
-        return "\n".join((f"{self.message}", f"Got: {self.directory}."))
-
-
-class HemisphereNotSpecifiedError(Exception):
-    """Exception raised when electrode hemisphere is not specified in settings.
-
-    Attributes:
-        subject -- input subject which caused the error
-        hemisphere -- specified hemispheres
-        message -- explanation of the error
-    """
-
-    def __init__(
-        self,
-        subject: str,
-        hemispheres: dict[str, str],
-        message: str = (
-            "Input ECOG hemisphere is not specified in"
-            " `filefinder_settings.py` for given subject."
-        ),
-    ) -> None:
-        self.subject = subject
-        self.hemispheres = hemispheres
-        self.message = message
-        super().__init__(self.message)
-
-    def __str__(self) -> str:
-        return "\n".join(
-            (
-                self.message,
-                f"Unspecified subject: {self.subject}.",
-                f"Specified hemispheres: {self.hemispheres}.",
-            )
-        )
diff --git a/src/pte/main.py b/src/pte/main.py
deleted file mode 100644
index 6b0a331..0000000
--- a/src/pte/main.py
+++ /dev/null
@@ -1,11 +0,0 @@
-"""Test package import."""
-import pte
-
-
-def main():
-    """Run this script."""
-    print(dir(pte))
-
-
-if __name__ == "__main__":
-    main()
diff --git a/src/pte/pipelines/pipelines.py b/src/pte/pipelines/pipelines.py
index 7353bf5..4d5f647 100644
--- a/src/pte/pipelines/pipelines.py
+++ b/src/pte/pipelines/pipelines.py
@@ -1,4 +1,5 @@
 """Module for predefined processing pipelines."""
+
 from collections.abc import Sequence
 from pathlib import Path
 
@@ -17,7 +18,7 @@ def process_emg_rms(
     out_path: mne_bids.BIDSPath | None = None,
 ) -> mne.io.BaseRaw:
     """Add EMG root mean square channels to Raw object and save."""
-    if raw.filenames:
+    if raw.filenames and raw.filenames[0] is not None:
         prefix = f"File: {Path(raw.filenames[0]).name}. "
     else:
         prefix = ""
diff --git a/src/pte/plotting/__init__.py b/src/pte/plotting/__init__.py
index f2cd93c..efc5204 100644
--- a/src/pte/plotting/__init__.py
+++ b/src/pte/plotting/__init__.py
@@ -1,6 +1,6 @@
 """Module for plotting functions."""
 
-from .clusterplot import clusterplot_combined
+from .clusterplot import clusterplot_combined, clusterplot_correlation
 from .coordinates import add_coords, find_structure_mni
 from .meshplot import meshplot_2d_compare
 from .raw import plotly_mne, plotly_rawdata
diff --git a/src/pte/plotting/clusterplot.py b/src/pte/plotting/clusterplot.py
index aa09bee..1dcc710 100644
--- a/src/pte/plotting/clusterplot.py
+++ b/src/pte/plotting/clusterplot.py
@@ -1,99 +1,288 @@
 """Module for plotting clusters."""
+
+from collections.abc import Sequence
 from pathlib import Path
+from typing import Literal
 
 import matplotlib.figure
 import numpy as np
 import pte_stats
+import scipy.stats
 from matplotlib import pyplot as plt
 
 
-def clusterplot_combined(
-    power_a: np.ndarray,
-    power_b: np.ndarray | int | float,
+def clusterplot_correlation(
+    data: np.ndarray,
+    corr_data: np.ndarray,
     extent: tuple | list,
     alpha: float = 0.05,
     n_perm: int = 100,
+    fig: matplotlib.figure.Figure | None = None,
     title: str | None = None,
-    borderval_cbar: str | int | float = "auto",
+    x_label: str = "Time [s]",
+    y_label: str = "Frequency [Hz]",
+    cbar_label: str = "Power [AU]",
+    cbar_borderval: Literal["auto"] | int | float = "auto",
+    cbar_borderval_corr: Literal["auto"] | int | float = "auto",
     outpath: Path | str | None = None,
     show: bool = True,
     n_jobs: int = 1,
 ) -> matplotlib.figure.Figure:
     """Plot power, p-values and significant clusters."""
+    data_av = data.mean(axis=0)
 
-    if isinstance(power_b, (int, float)):
-        power_av = power_a.mean(axis=0)
-    else:
-        power_av = power_a.mean(axis=0) - power_b.mean(axis=0)
-
-    if isinstance(borderval_cbar, str):
-        if borderval_cbar != "auto":
+    if isinstance(cbar_borderval, str):
+        if cbar_borderval != "auto":
             raise ValueError(
-                "`border_val` must be either an int, float or"
-                f" 'auto'. Got: {borderval_cbar}."
+                "`cbar_borderval` must be either an int, float or"
+                f" 'auto'. Got: {cbar_borderval}."
             )
-        borderval_cbar = min(power_av.max(), np.abs(power_av.min()))
+        cbar_borderval = min(data_av.max(), np.abs(data_av.min()))
+
+    if not fig:
+        fig, axs = plt.subplots(
+            nrows=1,
+            ncols=3,
+            figsize=(8, 2.4),
+            sharex=True,
+            sharey=True,
+        )
+    else:
+        axs = fig.axes
+    for ax in axs:
+        ax.set_xlabel(x_label)
+    axs[0].set_ylabel(y_label)
+
+    corr_vals = np.empty(data.shape[1:])
+    p_values = np.empty(data.shape[1:])
+    for i in range(data.shape[1]):
+        for j in range(data.shape[2]):
+            corr, pval = scipy.stats.spearmanr(data[:, i, j], corr_data)
+            corr_vals[i, j] = corr
+            p_values[i, j] = pval
 
-    fig, axs = plt.subplots(
-        nrows=1, ncols=3, figsize=(12, 4.8), sharex=True, sharey=True
+    _, cluster_arr = pte_stats.cluster_correct_pvals_2d(
+        p_values=p_values,
+        alpha=alpha,
+        n_perm=n_perm,
+        only_max_cluster=False,
+        n_jobs=n_jobs,
     )
-    # Plot averaged power
+    squared = np.zeros(data.shape[1:], dtype=int)
+    if cluster_arr:
+        for cluster in cluster_arr:
+            if cluster[0].size > 20:
+                squared[cluster] = 1
+        axs[0].contour(
+            squared,
+            levels=[0.99],
+            extent=extent,
+            origin="lower",
+            colors="black",
+        )
+        axs[1].contour(
+            squared,
+            levels=[0.99],
+            extent=extent,
+            origin="lower",
+            colors="black",
+        )
+
     pos_0 = axs[0].imshow(
-        power_av,
+        data_av,
         extent=extent,
         cmap="viridis",
         aspect="auto",
         origin="lower",
-        vmin=borderval_cbar * -1,
-        vmax=borderval_cbar,
-    )
-    fig.colorbar(
-        pos_0,
-        ax=axs[0],
-        label="Power (AU)",
+        vmin=cbar_borderval * -1,
+        vmax=cbar_borderval,
     )
+    fig.colorbar(pos_0, ax=axs[0], label=cbar_label)
 
-    # Plot p-values
-    p_values = pte_stats.permutation_2d(
-        data_a=power_a,
-        data_b=power_b,
-        n_perm=n_perm,
-        two_tailed=True,
-    )
+    if isinstance(cbar_borderval_corr, str):
+        if cbar_borderval_corr != "auto":
+            raise ValueError(
+                "`cbar_borderval_corr` must be either an int, float or"
+                f" 'auto'. Got: {cbar_borderval_corr}."
+            )
+        cbar_borderval_corr = min(corr_vals.max(), np.abs(corr_vals.min()))
+    corr_vals_masked = np.ma.masked_where(np.logical_not(squared), corr_vals)
     pos_1 = axs[1].imshow(
+        corr_vals_masked,
+        extent=extent,
+        cmap="viridis",
+        aspect="auto",
+        origin="lower",
+        vmax=cbar_borderval_corr,
+        vmin=-cbar_borderval_corr,  # type: ignore[operator]
+    )
+    fig.colorbar(pos_1, ax=axs[1], label="Spearman's ρ")
+
+    pos_2 = axs[2].imshow(
         p_values,
         extent=extent,
+        norm="log",  # type: ignore
         cmap="viridis_r",
         aspect="auto",
         origin="lower",
     )
-    fig.colorbar(pos_1, ax=axs[1], label="p-values")
-
-    # Plot significant clusters
-    _, cluster_arr = pte_stats.cluster_analysis_2d(
-        data_a=power_a,
-        data_b=power_b,
-        alpha=alpha,
-        n_perm=n_perm,
-        only_max_cluster=False,
-        n_jobs=n_jobs,
+    axs[2].contour(
+        p_values,
+        levels=[alpha],
+        extent=extent,
+        origin="lower",
+        colors="black",
     )
-    squared = np.zeros(power_a.shape[1:])
-    if cluster_arr:
-        for cluster in cluster_arr:
-            squared[cluster] = 1
-    np.expand_dims(squared, axis=0)
+    fig.colorbar(pos_2, ax=axs[2], label="P [log]")
 
-    pos_2 = axs[2].imshow(
-        squared,
+    if title:
+        fig.suptitle(title)
+    fig.tight_layout()
+    if outpath:
+        fig.savefig(outpath)
+    if show:
+        plt.show()
+    return fig
+
+
+def clusterplot_combined(
+    power_a: np.ndarray,
+    power_b: np.ndarray | int | float,
+    extent: tuple | list,
+    alpha: float = 0.05,
+    n_perm: int = 1000,
+    correction_method: Literal["cluster_pvals", "cluster"] = "cluster_pvals",
+    fig: matplotlib.figure.Figure | None = None,
+    title: str | None = None,
+    x_label: str = "Time [s]",
+    y_label: str = "Frequency [Hz]",
+    cbar_label: str = "Power [AU]",
+    cbar_borderval: str | int | float = "auto",
+    plot_pvals: bool = True,
+    outpath: Path | str | None = None,
+    show: bool = True,
+    n_jobs: int = 1,
+) -> matplotlib.figure.Figure:
+    """Plot power, p-values and significant clusters."""
+
+    if isinstance(power_b, int | float):
+        power_av = power_a.mean(axis=0)
+    else:
+        power_av = power_b.mean(axis=0) - power_a.mean(axis=0)
+
+    if isinstance(cbar_borderval, str):
+        if cbar_borderval != "auto":
+            raise ValueError(
+                "`cbar_borderval` must be either an int, float or"
+                f" 'auto'. Got: {cbar_borderval}."
+            )
+        cbar_borderval = min(power_av.max(), np.abs(power_av.min()))
+
+    if not fig:
+        ncols = 3 if plot_pvals else 1
+        fig, axs = plt.subplots(
+            nrows=1,
+            ncols=ncols,
+            figsize=(8, 2.4),
+            sharex=True,
+            sharey=True,
+        )
+    else:
+        axs = fig.axes
+    if not isinstance(axs, np.ndarray):
+        if not isinstance(axs, Sequence):
+            axs = [axs]
+        axs = np.array(axs)
+    for ax in axs:
+        ax.set_xlabel(x_label)
+        ax.spines["top"].set_visible(False)
+        ax.spines["right"].set_visible(False)
+    axs[0].set_ylabel(y_label)
+    pos_0 = axs[0].imshow(
+        power_av,
         extent=extent,
-        cmap="binary",
+        cmap="viridis",
         aspect="auto",
         origin="lower",
+        vmin=cbar_borderval * -1,
+        vmax=cbar_borderval,
     )
-    fig.colorbar(pos_2, ax=axs[2], label=f"Signif. Clusters (p ≤ {alpha})")
-    fig.suptitle(title)
-    fig.tight_layout()
+    fig.colorbar(pos_0, ax=axs[0], label=cbar_label)
+
+    if correction_method == "cluster_pvals":
+        p_values, _, cluster_arr = pte_stats.cluster_analysis_2d_from_pvals(
+            data_a=power_a,
+            data_b=power_b,
+            alpha=alpha,
+            n_perm=n_perm,
+            only_max_cluster=False,
+            two_tailed=True,
+            n_jobs=n_jobs,
+        )
+    elif correction_method == "cluster":
+        p_values, _, cluster_arr = pte_stats.cluster_analysis_2d(
+            data_a=power_a,
+            data_b=power_b,
+            alpha=alpha,
+            n_perm=n_perm,
+            only_max_cluster=False,
+            two_tailed=True,
+            n_jobs=n_jobs,
+        )
+    else:
+        raise ValueError(
+            f"Unknown correction method. Got: {correction_method}."
+            "Must be one of 'cluster_pvals' or 'cluster'."
+        )
+
+    squared = np.zeros(power_av.shape[:], dtype=int)
+    if cluster_arr:
+        for cluster in cluster_arr:
+            if cluster[0].size > 20:
+                squared[cluster] = 1
+        if squared.any():
+            axs[0].contour(
+                squared,
+                levels=[0.99],
+                extent=extent,
+                origin="lower",
+                colors="black",
+            )
+            if axs.size > 1:
+                axs[1].contour(
+                    squared,
+                    levels=[0.99],
+                    extent=extent,
+                    origin="lower",
+                    colors="black",
+                )
+    if axs.size > 1:
+        pos_1 = axs[1].imshow(
+            p_values,
+            extent=extent,
+            norm="log",
+            cmap="viridis_r",
+            aspect="auto",
+            origin="lower",
+        )
+        fig.colorbar(pos_1, ax=axs[1], label="P [log]")
+
+    if axs.size > 2:
+        pos_2 = axs[2].imshow(
+            squared,
+            extent=extent,
+            cmap="binary",
+            aspect="auto",
+            origin="lower",
+            vmin=0,
+            vmax=1,
+        )
+        cbar = fig.colorbar(
+            pos_2, ax=axs[2], label=f"Signif. Clusters [P≤{alpha}]"
+        )
+        cbar.ax.set_yticks([0, 1])
+    if title is not None:
+        fig.suptitle(title)
     if outpath:
         fig.savefig(outpath)
     if show:
diff --git a/src/pte/plotting/coordinates.py b/src/pte/plotting/coordinates.py
index 50d6da0..60c8cfd 100644
--- a/src/pte/plotting/coordinates.py
+++ b/src/pte/plotting/coordinates.py
@@ -1,6 +1,6 @@
 """Module for handling brain coordinates."""
+
 import pathlib
-from typing import Optional, Union
 
 import numpy as np
 import pandas as pd
@@ -14,7 +14,7 @@
 #  (https://github.com/aaolaveh/anat-from-MNI/blob/master/functions.py)
 def find_structure_mni(
     mni_coords: np.ndarray, database: pathlib.Path | str | None = None
-) -> list[str] | str:
+) -> list[list[str]]:
     """
     Convert MNI coordinate to a description of brain structure in aal
 
@@ -57,19 +57,15 @@ def find_structure_mni(
     rows = np.shape(atlas["DB"])[1]
     descriptions = []
     for ind in ind_coords:
-        single_result = []
+        single_result: list[str] = []
         for j in range(rows):
             # atlas["DB"][0,j][0,0][0] is the j-th 3D-matrix
             graylevel = atlas["DB"][0, j][0, 0][0][ind[0], ind[1], ind[2]]
             if graylevel == 0:
                 label = "undefined"
             else:
-                if j < (rows - 1):
-                    suffix = ""
-                else:
-                    suffix = " (aal)"
-
-                # mat['DB'][0,j][0,0][1]  is the list with regions
+                suffix = "" if j < rows - 1 else " (aal)"
+                # atlas['DB'][0,j][0,0][1]  is the list with regions
                 label = (
                     atlas["DB"][0, j][0, 0][1][0, (graylevel - 1)][0] + suffix
                 )
@@ -145,10 +141,7 @@ def add_coords(data: pd.DataFrame, coords: pd.DataFrame) -> pd.DataFrame:
     """Add x, y and z electrode coordinates to DataFrame."""
     data.loc[:, ["x", "y", "z"]] = None
     for ind in data.index:
-        if "avgref" in ind[1]:
-            ind_elec = (ind[0], ind[1][:-7])
-        else:
-            ind_elec = ind
+        ind_elec = (ind[0], ind[1][:-7]) if "avgref" in ind[1] else ind
         try:
             data.loc[ind, ["x", "y", "z"]] = coords.loc[
                 ind_elec, ["x", "y", "z"]
diff --git a/src/pte/plotting/meshplot.py b/src/pte/plotting/meshplot.py
index 312c79a..2933bfc 100644
--- a/src/pte/plotting/meshplot.py
+++ b/src/pte/plotting/meshplot.py
@@ -1,14 +1,13 @@
 """Module for plotting quantitave results onto brain structures."""
+
 import copy
 from pathlib import Path
-from typing import Optional, Union
 
 import numpy as np
 import pandas as pd
 import scipy.io
-from matplotlib import cm, collections, figure
+from matplotlib import cm, collections, figure, ticker
 from matplotlib import pyplot as plt
-from matplotlib import ticker
 from mpl_toolkits.axes_grid1 import make_axes_locatable
 
 RESOURCES = Path(__file__).parent / "resources"
@@ -31,7 +30,7 @@ def meshplot_2d_compare(
     invert_colors_left: bool = False,
     invert_colors_right: bool = False,
     show: bool = True,
-    verbose: bool = True,
+    verbose: bool | str | int | None = True,
 ) -> figure.Figure:
     """Plot data on both hemispheres in a 2D view."""
     vertices = scipy.io.loadmat(str(RESOURCES / "vertices.mat"))
@@ -61,11 +60,9 @@ def meshplot_2d_compare(
 
     ecog_list, stn_list, cbar_list = [], [], []
 
-    for i, (data, side, lims, invert_colors) in enumerate(
-        (
-            (data_left, "left", lims_left, invert_colors_left),
-            (data_right, "right", lims_right, invert_colors_right),
-        )
+    for data, side, lims, invert_colors in (
+        (data_left, "left", lims_left, invert_colors_left),
+        (data_right, "right", lims_right, invert_colors_right),
     ):
         plot_cort, plot_stn = _plot_single_hem(
             data=data,
@@ -81,14 +78,14 @@ def meshplot_2d_compare(
         stn_list.append(plot_stn)
         cbar_list.append(side)
 
-    for i, ax in enumerate(axes):
+    for ax in axes:
         ax.axes.xaxis.set_ticks([])
         ax.axes.yaxis.set_ticks([])
         ax.axis("off")
 
     label_cbars = [label_left, label_right]
     for idx, (plot_cort, plot_stn, location) in enumerate(
-        zip(ecog_list, stn_list, cbar_list)
+        zip(ecog_list, stn_list, cbar_list, strict=True)
     ):
         for i, (pos, ratio) in enumerate(
             ((plot_cort, ratio_cortex_subcortex), (plot_stn, 1))
@@ -111,12 +108,11 @@ def meshplot_2d_compare(
             ticks_loc = cbar.ax.get_yticks().tolist()
             cbar.ax.yaxis.set_major_locator(ticker.FixedLocator(ticks_loc))
             cbar.ax.set_yticklabels(
-                labels=np.round(cbar.get_ticks(), 2),
-                color=color
+                labels=np.round(cbar.get_ticks(), 2), color=color
             )
-            cbar.outline.set_edgecolor(color)
-
-    fig.suptitle(title)
+            cbar.outline.set_edgecolor(color)  # type: ignore[operator]
+    if title is not None:
+        fig.suptitle(title)
     if outpath is not None:
         fig.savefig(outpath, bbox_inches="tight")
     if show:
@@ -124,9 +120,7 @@ def meshplot_2d_compare(
     return fig
 
 
-def _get_lims(
-    data: np.ndarray, num_devs: int | float
-) -> tuple[float, float]:
+def _get_lims(data: np.ndarray, num_devs: int | float) -> tuple[float, float]:
     """Get lower and upper limit in standard deviations."""
     mean = np.nanmean(data)
     std = np.nanstd(data)
@@ -143,7 +137,7 @@ def _plot_single_hem(
     dot_size: int,
     key: str,
     reverse_cmap: bool,
-    verbose: bool,
+    verbose: bool | str | int | None,
 ) -> tuple[collections.PathCollection, collections.PathCollection]:
     """Plot data for a single hemisphere."""
     data = data.dropna(axis=0, subset=["x", "y", "z"])
diff --git a/src/pte/plotting/raw.py b/src/pte/plotting/raw.py
index 3faeb6f..2a3c350 100644
--- a/src/pte/plotting/raw.py
+++ b/src/pte/plotting/raw.py
@@ -1,7 +1,6 @@
 """Plotting functions based on plotly."""
 
 from pathlib import Path
-from typing import Optional, Union
 
 import mne
 import numpy as np
diff --git a/src/pte/preprocessing/annotation.py b/src/pte/preprocessing/annotation.py
index 8936ecc..c8c6e58 100644
--- a/src/pte/preprocessing/annotation.py
+++ b/src/pte/preprocessing/annotation.py
@@ -1,4 +1,5 @@
 """Module for annotating Raw objects and other data."""
+
 import mne
 
 
diff --git a/src/pte/preprocessing/channels.py b/src/pte/preprocessing/channels.py
index f561948..e54ee4f 100644
--- a/src/pte/preprocessing/channels.py
+++ b/src/pte/preprocessing/channels.py
@@ -42,16 +42,18 @@ def add_emg_rms(
         raw=raw.copy(),
         emg_ch=ch_name,
         window_duration=[window_duration],
-        analog_ch=analog_channel,
+        analog_channel=analog_channel,
         rereference=False,
     )
 
     rms_channel = f"EMG_RMS_{window_duration}"
 
-    raw_rms.plot(scalings="auto", block=True, title="EMG Root Mean Square (RMS)")
+    raw_rms.plot(
+        scalings="auto", block=True, title="EMG Root Mean Square (RMS)"
+    )
 
     raw = raw.add_channels(  # type: ignore
-        [raw_rms.pick_channels([rms_channel])],
+        [raw_rms.pick([rms_channel])],
         force_update_info=True,
     )
     if new_ch_name == "auto":
@@ -92,7 +94,7 @@ def add_squared_channel(
     events_ids = events[:, 0]
     data_squared = np.zeros((1, raw.n_times))
     for i in np.arange(0, len(events_ids), 2):
-        data_squared[0, events_ids[i] : events_ids[i + 1]] = 1.0 # * 1e-6
+        data_squared[0, events_ids[i] : events_ids[i + 1]] = 1.0  # * 1e-6
 
     info = mne.create_info(
         ch_names=[ch_name], ch_types=["misc"], sfreq=raw.info["sfreq"]
@@ -111,7 +113,7 @@ def add_squared_channel(
 
 def summation_channel_name(summation_channels: Sequence[str]) -> str:
     """Create channel name for summation montage from given channels."""
-    base_items = None
+    base_items: list[str] = []
     channel_numbers = []
     for ch_name in summation_channels:
         items = ch_name.split("_")
@@ -130,10 +132,10 @@ def bipolar_channel_name(channels: Sequence[str]) -> str:
         raise ValueError(
             "Length of `channels` must be 2. Got:" f"{len(channels)}."
         )
-    base_items = None
+    base_items: list[str] = []
     channel_numbers = []
     for ch_name in channels:
-        items = ch_name.split("_")
+        items: list[str] = ch_name.split("_")
         channel_numbers.append(items.pop(2))
         if not base_items:
             base_items = items
diff --git a/src/pte/preprocessing/emg.py b/src/pte/preprocessing/emg.py
index f3caa49..305fcd9 100644
--- a/src/pte/preprocessing/emg.py
+++ b/src/pte/preprocessing/emg.py
@@ -1,7 +1,6 @@
 """Module for processing of EMG channels."""
 
 from collections.abc import Sequence
-from typing import Optional, Union
 
 import mne
 import numpy as np
@@ -12,7 +11,7 @@ def get_emg_rms(
     raw: mne.io.BaseRaw,
     emg_ch: str | list[str] | np.ndarray,
     window_duration: float | int | Sequence,
-    analog_ch: list[str] | str | None = None,
+    analog_channel: str | Sequence[str] | None = None,
     rereference: bool = False,
     notch_filter: float | int = 50,
 ) -> mne.io.BaseRaw:
@@ -38,7 +37,7 @@ def get_emg_rms(
         Raw object containing root mean square of windowed signal and target
         channel.
     """
-    raw_emg = raw.copy()
+    raw_emg: mne.io.BaseRaw = raw.copy()
     raw_emg.pick(picks=emg_ch)
 
     if not raw_emg.preload:
@@ -64,13 +63,13 @@ def get_emg_rms(
 
     data_bip = raw_emg.get_data(verbose=False)[0]
     if notch_filter:
-        assert isinstance(notch_filter, (int, float))
+        assert isinstance(notch_filter, int | float)
         freqs = np.arange(notch_filter, raw.info["sfreq"] / 2, notch_filter)
         raw_emg = raw_emg.notch_filter(freqs=freqs, picks="emg", verbose=False)
     raw_filtered = raw_emg.filter(
         l_freq=15, h_freq=500, picks="all", verbose=False
     )
-    if isinstance(window_duration, (int, float)):
+    if isinstance(window_duration, int | float):
         window_duration = [window_duration]
 
     data = raw_filtered.get_data()[0]
@@ -78,20 +77,22 @@ def get_emg_rms(
 
     for idx, window in enumerate(window_duration):
         rms_raw = _rms_window_nb(data, window, raw.info["sfreq"])
+        # now scale to match other MISC channels
         rms_zscore = (rms_raw - np.mean(rms_raw)) / np.std(rms_raw)
-        # now scale for compatibility with other MISC channels
         data_rms[idx, :] = rms_zscore
 
     emg_ch_names = [f"EMG_RMS_{window}" for window in window_duration]
 
-    if analog_ch:
-        if isinstance(analog_ch, str):
-            analog_ch = [analog_ch]
-        data_analog = raw.copy().pick(picks=analog_ch).get_data()[0]
+    if analog_channel:
+        if isinstance(analog_channel, str):
+            analog_channel = [analog_channel]
+        elif not isinstance(analog_channel, list):
+            analog_channel = list(analog_channel)
+        data_analog = raw.copy().pick(picks=analog_channel).get_data()[0]
         if np.abs(min(data_analog)) > max(data_analog):
             data_analog = data_analog * -1
         data_all = np.vstack((data_analog, data_bip, data_rms))
-        ch_names = analog_ch + ["EMG_BIP"] + emg_ch_names
+        ch_names = analog_channel + ["EMG_BIP"] + emg_ch_names
     else:
         data_all = np.vstack((data_bip, data_rms))
         ch_names = ["EMG_BIP"] + emg_ch_names
diff --git a/src/pte/preprocessing/preprocessing.py b/src/pte/preprocessing/preprocessing.py
index ccd9f38..a788a90 100644
--- a/src/pte/preprocessing/preprocessing.py
+++ b/src/pte/preprocessing/preprocessing.py
@@ -1,4 +1,5 @@
 """Module for preprocessing functions (resampling, referencing, etc.)"""
+
 from collections.abc import Sequence
 from pathlib import Path
 from typing import Literal
@@ -10,174 +11,216 @@
 
 
 def pick_by_nm_channels(
-    raw: mne.io.BaseRaw,
-    nm_channels_dir: Path | str,
-    fname: mne_bids.BIDSPath,
+    raw: mne.io.BaseRaw, nm_channels: pd.DataFrame
 ) -> mne.io.BaseRaw:
-    """Pick channels (``used`` and ``good``) according to *nm_channels.csv."""
-    raw = raw.copy()
-    basename = str(Path(fname).stem)
-    fpath = Path(nm_channels_dir) / Path(basename + "_nm_channels.csv")
-    nm_channels: pd.DataFrame = pd.read_csv(fpath, header=0)
+    """Pick channels (``used`` == 1) according to *nm_channels.csv."""
     channel_picks = nm_channels[(nm_channels["used"] == 1)]
     if len(channel_picks) == 0:
         raise ValueError(
-            "No valid channels found in given nm_channels.csv file:"
-            f" {fpath.name}"
+            "No valid channels found in given nm_channels DataFrame."
+            "Please check the `used` column."
         )
-    raw.pick_channels(ch_names=channel_picks["new_name"].to_list())
+    raw.pick(channel_picks["new_name"].to_list())
     return raw
 
 
-def bipolar_refs_from_nm_channels(
+def load_nm_channels(
     nm_channels_dir: Path | str,
     filename: Path | str | mne_bids.BIDSPath,
-    types: str | Sequence = ("ecog", "dbs", "eeg"),
-) -> tuple[list[str], list[str], list[str]]:
-    """Get referencing montage from *nm_channels.csv file."""
-    if not isinstance(types, Sequence):
-        types = (types,)
-    if isinstance(filename, mne_bids.BIDSPath):
-        # Implement this later
-        # basename = filename.copy().update(
-        # extension=None, suffix=None, datatype=None
-        # ).basename
+) -> pd.DataFrame:
+    """Load *nm_channels.csv file."""
+    if isinstance(filename, Path):
+        basename = filename.stem
+    elif isinstance(filename, mne_bids.BIDSPath):
         basename = filename.copy().update(extension=None).basename
     else:
-        basename = str(Path(filename).stem)
-    fpath = Path(nm_channels_dir) / Path(basename + "_nm_channels.csv")
+        basename = Path(filename).stem
+    fpath = Path(nm_channels_dir, f"{basename}_nm_channels.csv")
     nm_channels: pd.DataFrame = pd.read_csv(fpath, header=0)
-    anodes, cathodes, ch_names = [], [], []
-    for ch_type in types:
-        df_picks = nm_channels.loc[
-            (nm_channels.type == ch_type)
-            & (nm_channels.used == 1)
-            & nm_channels.rereference.notna()
-            & (nm_channels.rereference != "None")
-            & (nm_channels.rereference != "average")
-        ]
-        anodes.extend(df_picks.name)
-        cathodes.extend(df_picks.rereference)
-        ch_names.extend(df_picks.new_name)
-    return anodes, cathodes, ch_names
+    return nm_channels
 
 
-def bandstop_filter(
+def stim_filter(
     raw: mne.io.BaseRaw,
-    bandstop_freq: str |int | float | np.ndarray | None = "auto",
+    stim_freq: str | int | float | np.ndarray | None = "auto",
     fname: str | None = None,
+    verbose: bool | str | int | None = False,
 ) -> mne.io.BaseRaw:
     """Bandstop filter Raw data"""
-    if bandstop_freq is None:
+    if stim_freq is None:
         return raw
 
-    if isinstance(bandstop_freq, str):
-        if bandstop_freq != "auto":
+    if isinstance(stim_freq, str):
+        if stim_freq != "auto":
             raise ValueError(
-                "`bandstop_freq` must be one of either `string`"
-                f"`float`, `'auto'` or `None`. Got: {bandstop_freq}."
+                "`stim_freq` must be one of either `int`"
+                f"`float`, `'auto'` or `None`. Got: {stim_freq}."
             )
         if not isinstance(fname, str):
             try:
                 fname = raw.filenames[0]
             except ValueError as error:
                 raise ValueError(
-                    "If `bandstop_freq` is `'auto'`, `fname` must be provided."
+                    "If `stim_freq` is `'auto'`, `fname` must be provided."
                 ) from error
         if "StimOn" not in fname:
             return raw
-        bandstop_freq = 130
+        stim_freq = 130
 
-    if isinstance(bandstop_freq, (int, float)):
-        bandstop_freq = np.arange(
-            bandstop_freq, raw.info["sfreq"] / 2, bandstop_freq
-        )
+    if isinstance(stim_freq, int | float):
+        stim_freq = np.arange(stim_freq, raw.info["sfreq"] // 2 - 1, stim_freq)
+
+    notch_widths = (stim_freq * 0.2).clip(max=26)
+    raw = raw.notch_filter(
+        stim_freq, notch_widths=notch_widths, verbose=verbose
+    )
+    return raw
+
+
+def bipolar_refs_from_nm_channels(
+    nm_channels,
+) -> tuple[list[str], list[str], list[str]]:
+    """Get referencing montage from nm_channels DataFrame."""
+    anodes, cathodes, ch_names = [], [], []
+    df_picks = nm_channels.loc[
+        (nm_channels.used == 1)
+        & nm_channels.rereference.notna()
+        & (nm_channels.rereference != "None")
+        & (nm_channels.rereference != "average")
+    ]
+    anodes.extend(df_picks.name)
+    cathodes.extend(df_picks.rereference)
+    ch_names.extend(df_picks.new_name)
+    return anodes, cathodes, ch_names
 
-    if bandstop_freq:
-        print("FREQUENCIES:", bandstop_freq)
-        raw = raw.notch_filter(
-            bandstop_freq, notch_widths=bandstop_freq * 0.2, verbose=True
-        )
 
+def ref_by_nm_channels(
+    raw: mne.io.BaseRaw, nm_channels: pd.DataFrame
+) -> mne.io.BaseRaw:
+    anodes, cathodes, new_names = bipolar_refs_from_nm_channels(nm_channels)
+    if not new_names:
+        print("No channels given for bipolar re-referencing.")
+        return raw
+    # Renaming necessary to account for possible name duplications
+    curr_names = raw.ch_names
+    rename = {}
+    for i, ch in enumerate(new_names):
+        if ch in curr_names:
+            new_name = f"{ch}_new"
+            new_names[i] = new_name
+            rename[new_name] = ch
+    raw = mne.set_bipolar_reference(  # type: ignore
+        raw,
+        anode=anodes,
+        cathode=cathodes,
+        ch_name=new_names,
+        drop_refs=False,
+    )
+
+    if rename:
+        raw.drop_channels(rename.values())
+        raw.rename_channels(rename)
+
+    drop = list(set(anodes + cathodes) & set(raw.ch_names))
+    keep = nm_channels.query("used == 1 and name not in @anodes")
+    if not keep.empty:
+        drop = [ch for ch in drop if ch not in keep["name"].tolist()]
+    if drop:
+        raw.drop_channels(drop)
+
+    bads = raw.info["bads"]
+    for ch in new_names:
+        if ch in bads:
+            bads.remove(ch)
     return raw
 
 
 def preprocess(
     raw: mne.io.BaseRaw,
-    nm_channels_dir: Path,
+    nm_channels_dir: Path | None = None,
     filename: Path | str | mne_bids.BIDSPath | None = None,
-    average_ref_types: Sequence[str] | None = None,
+    average_ref_types: Sequence[str] | str | None = None,
+    ref_nm_channels: bool = True,
     notch_filter: int | Literal["auto"] | None = "auto",
-    resample_freq: int |float |None = 500,
-    high_pass: int |float |None = None,
-    low_pass: int |float |None = None,
-    bandstop_freq: str |int | float | np.ndarray | None = "auto",
+    resample_freq: int | float | None = 500,
+    high_pass: int | float | None = None,
+    low_pass: int | float | None = None,
+    stim_freq: str | int | float | np.ndarray | None = "auto",
     pick_used_channels: bool = False,
+    sort_channels: bool = True,
+    verbose: bool | str | int | None = True,
 ) -> mne.io.BaseRaw:
     """Preprocess raw data."""
+    if pick_used_channels or ref_nm_channels and nm_channels_dir is None:
+        raise ValueError("`nm_channels_dir` must be provided.")
+    if nm_channels_dir:
+        if filename is None:
+            if not raw.filenames:
+                raise ValueError(
+                    "If `filename` is not provided, `raw` must have a"
+                    " `filenames` attribute."
+                )
+            filename = raw.filenames[0]
+        nm_channels = load_nm_channels(nm_channels_dir, filename)
+
     if notch_filter == "auto":
         notch_filter = raw.info["line_freq"]
-    if filename is None:
-        filename = raw.filenames[0]
-    if isinstance(filename, Path):
-        filename = str(filename)
 
-    # raw.pick(picks=["ecog", "dbs"], verbose=False)
     if not raw.preload:
         raw.load_data(verbose=True)
 
     if average_ref_types:
+        if isinstance(average_ref_types, str):
+            average_ref_types = [average_ref_types]
         for pick_type in average_ref_types:
             raw.set_eeg_reference(
-                ref_channels="average", ch_type=pick_type, verbose=True
+                ref_channels="average", ch_type=pick_type, verbose=verbose
             )
             raw.rename_channels(
                 {
                     ch: f"{ch}-avgref"
                     for ch, ch_type in zip(
-                        raw.ch_names, raw.get_channel_types()
+                        raw.ch_names, raw.get_channel_types(), strict=True
                     )
                     if ch_type == pick_type
                 }
             )
 
-    anodes, cathodes, ch_names = bipolar_refs_from_nm_channels(
-        nm_channels_dir=nm_channels_dir, filename=filename
-    )
-    if not ch_names:
-        print("No channels given for bipolar re-referencing.")
-    else:
-        raw = mne.set_bipolar_reference( # type: ignore
-            raw,
-            anode=anodes,
-            cathode=cathodes,
-            ch_name=ch_names,
-            drop_refs=True,
-        )
-        bads = raw.info['bads']
-        for ch in ch_names:
-            if ch in bads:
-                bads.remove(ch) 
+    if ref_nm_channels and nm_channels_dir:
+        raw = ref_by_nm_channels(raw=raw, nm_channels=nm_channels)
+
+    if nm_channels_dir:
+        names = nm_channels.query("used == 1 or target == 1")
+        names = names[["name", "new_name"]].dropna()
+        old_names = names["name"].to_list()
+        new_names = names["new_name"].to_list()
+        curr_names = raw.ch_names
+        rename_map = {
+            old: new
+            for old, new in zip(old_names, new_names, strict=True)
+            if old in curr_names
+        }
+        raw.rename_channels(rename_map)
 
-    raw.resample(sfreq=resample_freq, verbose=True)
+    if pick_used_channels:
+        raw = pick_by_nm_channels(raw=raw, nm_channels=nm_channels)
 
-    raw.filter(l_freq=high_pass, h_freq=low_pass, verbose=True)
+    if resample_freq is not None:
+        raw.resample(sfreq=resample_freq, verbose=verbose)
+
+    if high_pass is not None or low_pass is not None:
+        raw.filter(l_freq=high_pass, h_freq=low_pass, verbose=verbose)
 
     if notch_filter is not None:
-        notch_freqs = np.arange(
-            notch_filter, 
-            raw.info["sfreq"] / 2,
-            notch_filter
-            )
+        notch_freqs: np.ndarray = np.arange(
+            notch_filter, raw.info["sfreq"] / 2, notch_filter
+        )
         if notch_freqs.size > 0:
-            raw.notch_filter(notch_freqs, verbose=True)
-
-    raw = bandstop_filter(raw=raw, bandstop_freq=bandstop_freq)
+            raw.notch_filter(notch_freqs, verbose=verbose)
 
-    if pick_used_channels:
-        raw = pick_by_nm_channels(
-            raw=raw, nm_channels_dir=nm_channels_dir, fname=filename
-        )
+    if stim_freq is not None:
+        raw = stim_filter(raw=raw, stim_freq=stim_freq, verbose=verbose)
 
-    raw.reorder_channels(sorted(raw.ch_names))
+    if sort_channels:
+        raw.reorder_channels(sorted(raw.ch_names))
     return raw
diff --git a/src/pte/time_frequency/bursts.py b/src/pte/time_frequency/bursts.py
index 9898681..36fd3f3 100644
--- a/src/pte/time_frequency/bursts.py
+++ b/src/pte/time_frequency/bursts.py
@@ -1,34 +1,19 @@
 """Module for burst analysis."""
-from typing import Union
 
 import numpy as np
 
 
 def burst_length_and_amplitude(
-    power: np.ndarray,
-    threshold: int | float,
-    sfreq: int | float,
-    return_burst_amplitude: bool = True,
-    return_burst_indexes: bool = True,
-) -> (
-    np.ndarray |
-    tuple[np.ndarray, np.ndarray] |
-    tuple[np.ndarray, np.ndarray, np.ndarray]
-):
+    power: np.ndarray, threshold: int | float, sfreq: int | float
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
     """Calculates the duration, amplitude and indexes of bursts.
 
     Args:
         power (np.ndarray): Power as array of shape (n_samples, )
         threshold (int | float): Threshold of power for identifying bursts
         sfreq (int | float): Sampling frequency
-        return_burst_amplitude (bool, optional): If True, return burst
-        amplitude. Defaults to True.
-        return_burst_indexes (bool, optional): If True, return start and stop
-        indexes of individual bursts. Defaults to True.
 
     Returns:
-        np.ndarray |
-        tuple[np.ndarray, np.ndarray] |
         tuple[np.ndarray, np.ndarray, np.ndarray]:
         Burst length (in seconds), mean burst amplitude and start and end
         indexes of individual bursts
@@ -36,29 +21,27 @@ def burst_length_and_amplitude(
     bursts = np.zeros(power.shape[0] + 1)
     bursts[1:] = power >= threshold
     bursts_onoff = np.diff(bursts)
-    isburst = False
-    burst_length = []
+    is_burst = False
     burst_start = 0
-    burst_amplitude = []
-    burst_indexes = []
+    burst_lengths = []
+    burst_amplitudes = []
+    burst_indices = []
     for index, threshold_crossed in enumerate(bursts_onoff):
         if threshold_crossed:
-            if isburst:
-                burst_length.append(index - burst_start)
-                burst_amplitude.append(power[burst_start:index].mean())
-                burst_indexes.append([burst_start, index])
-                isburst = False
+            if is_burst:
+                burst_lengths.append(index - burst_start)
+                burst_amplitudes.append(power[burst_start:index].mean())
+                burst_indices.append([burst_start, index])
+                is_burst = False
             else:
                 burst_start = index
-                isburst = True
-    if isburst:
-        burst_length.append(index + 1 - burst_start)
-        burst_amplitude.append(power[burst_start : index + 1].mean())
-        burst_indexes.append([burst_start, index + 1])
-    burst_length = np.array(burst_length) / sfreq
-    returns = [burst_length]
-    if return_burst_amplitude:
-        returns.append(np.array(burst_amplitude))
-    if return_burst_indexes:
-        returns.append(np.array(burst_indexes))
-    return tuple(returns)
+                is_burst = True
+    if is_burst:
+        burst_lengths.append(index + 1 - burst_start)
+        burst_amplitudes.append(power[burst_start : index + 1].mean())
+        burst_indices.append([burst_start, index + 1])
+    return (
+        np.array(burst_lengths) / sfreq,
+        np.array(burst_amplitudes),
+        np.array(burst_indices),
+    )
diff --git a/src/pte/time_frequency/power.py b/src/pte/time_frequency/power.py
index 68707c9..d553c21 100644
--- a/src/pte/time_frequency/power.py
+++ b/src/pte/time_frequency/power.py
@@ -1,5 +1,7 @@
 """Functions for calculating and handling band-power."""
+
 from pathlib import Path
+from typing import Literal
 
 import matplotlib.figure
 import mne
@@ -47,7 +49,7 @@ def plot_power(
 
 def smooth_power(
     power: mne.time_frequency.AverageTFR,
-    smoothing_type: str = "gaussian",
+    smoothing_type: Literal["gaussian", "median"] = "gaussian",
     **kwargs,
 ) -> mne.time_frequency.AverageTFR:
     """Smooth data in AverageTFR object using scipy smoothing filters."""
@@ -63,19 +65,19 @@ def smooth_power(
 
 def smooth_2d_array(
     data: np.ndarray,
-    smoothing_type: str = "gaussian",
+    smoothing_type: Literal["gaussian", "median"] = "gaussian",
     **kwargs,
 ) -> np.ndarray:
     """Smooth 2D data using scipy smoothing filters."""
     if smoothing_type == "gaussian":
         if "sigma" not in kwargs:
-            kwargs["sigma"] = 5
+            kwargs["sigma"] = 1
         data_out = scipy.ndimage.gaussian_filter(
             input=data, mode="reflect", **kwargs
         )
     elif smoothing_type == "median":
         if "size" not in kwargs:
-            kwargs["size"] = 5
+            kwargs["size"] = 1
         data_out = scipy.ndimage.median_filter(
             input=data,
             mode="reflect",
@@ -90,11 +92,11 @@ def smooth_2d_array(
 
 def apply_baseline(
     power: mne.time_frequency.AverageTFR,
-    baseline: tuple[int | float | None, int | float | None]
-    | np.ndarray
-    | None = (None, None),
+    baseline: (
+        tuple[int | float | None, int | float | None] | np.ndarray | None
+    ) = (None, None),
     mode: str = "percent",
-):
+) -> mne.time_frequency.AverageTFR:
     """Apply baseline correction given interval or custom values."""
     if baseline is None:
         return power
@@ -120,6 +122,7 @@ def apply_baseline(
 #       contributors may be used to endorse or promote products derived from
 #       this software without specific prior written permission.
 
+
 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
@@ -194,23 +197,25 @@ def _get_baseline_indices(
     if bmin is None:
         imin = 0
     else:
-        imin = np.where(times >= bmin)[0]
-        if len(imin) == 0:
+        baseline_indices = np.where(times >= bmin)[0]
+        if len(baseline_indices) == 0:
             raise ValueError(
-                f"bmin is too large {bmin}, it exceeds the largest"
-                " time value."
+                f"The baseline values are not within the limits of the time"
+                f" range. Baseline values: {baseline}. Time range: {times[0]}"
+                f" to {times[-1]}."
             )
-        imin = int(imin[0])
+        imin = int(baseline_indices[0])
     if bmax is None:
         imax = len(times)
     else:
-        imax = np.where(times <= bmax)[0]
-        if len(imax) == 0:
+        baseline_indices = np.where(times <= bmax)[0]
+        if len(baseline_indices) == 0:
             raise ValueError(
-                f"bmax is too small {bmax}, it is smaller than the"
-                " smallest time value."
+                f"The baseline values are not within the limits of the time"
+                f" range. Baseline values: {baseline}. Time range: {times[0]}"
+                f" to {times[-1]}."
             )
-        imax = int(imax[-1]) + 1
+        imax = int(baseline_indices[-1]) + 1
     if imin >= imax:
         raise ValueError(
             f"Bad rescaling slice ({imin}:{imax}) from time values"
@@ -241,12 +246,16 @@ def get_baseline(
 
 
 def average_power(
-    powers: list[mne.time_frequency.AverageTFR]
-    | mne.time_frequency.AverageTFR,
+    powers: (
+        list[mne.time_frequency.AverageTFR] | mne.time_frequency.AverageTFR
+    ),
     picks: str | list[str] | slice,
-    baseline: tuple[int | float | None, int | float | None]
-    | list[np.ndarray]
-    | None = (None, None),
+    baseline: (
+        tuple[int | float | None, int | float | None]
+        | list[np.ndarray]
+        | np.ndarray
+        | None
+    ) = (None, None),
     baseline_mode: str | None = "zscore",
     clip: int | float | None = None,
 ) -> mne.time_frequency.AverageTFR:
@@ -255,14 +264,13 @@ def average_power(
         powers = [powers]
     if isinstance(baseline, np.ndarray):
         baseline = [baseline]
-    if isinstance(baseline, list):
-        if not len(baseline) == len(powers):
-            raise ValueError(
-                "If numpy arrays are provided for"
-                "custom baseline correction, the same number of numpy arrays"
-                " and `power` (TFR) objects must be provided. Got:"
-                f" {len(baseline)} baseline arrays and {len(powers)} powers."
-            )
+    if isinstance(baseline, list) and not len(baseline) == len(powers):
+        raise ValueError(
+            "If numpy arrays are provided for"
+            "custom baseline correction, the same number of numpy arrays"
+            " and `power` (TFR) objects must be provided. Got:"
+            f" {len(baseline)} baseline arrays and {len(powers)} powers."
+        )
 
     power_all = None
     power_all_files = []
@@ -319,7 +327,7 @@ def average_power(
 
 
 def load_power(
-    files: list[Path | str], verbose: bool = False
+    files: list[Path | str], verbose: bool | str | int | None = False
 ) -> list[mne.time_frequency.AverageTFR] | list[mne.time_frequency.EpochsTFR]:
     """Load power from *-tfr.h5 files."""
     powers = []
@@ -352,15 +360,17 @@ def morlet_from_epochs(
         upper_freq = min(epochs.info["sfreq"] / 2, 200.0)
         freqs = np.arange(1.0, upper_freq)
 
-    if isinstance(decim_power, str):
-        if decim_power != "auto":
-            raise ValueError(
-                "If decim_power is a string, it must be `auto`. Got:"
-                f" {decim_power}"
-            )
-        decim = int(epochs.info["sfreq"] / 100)
-    else:
-        decim = decim_power
+    if "decim" not in kwargs:
+        if isinstance(decim_power, str):
+            if decim_power != "auto":
+                raise ValueError(
+                    "If decim_power is a string, it must be `auto`. Got:"
+                    f" {decim_power}"
+                )
+            decim = int(epochs.info["sfreq"] / 100)
+        else:
+            decim = int(decim_power)
+        kwargs["decim"] = decim
 
     power = mne.time_frequency.tfr_morlet(
         inst=epochs,
@@ -371,7 +381,6 @@ def morlet_from_epochs(
         average=average,
         return_itc=False,
         verbose=True,
-        decim=decim,
         **kwargs,
     )
     return power
@@ -379,12 +388,13 @@ def morlet_from_epochs(
 
 def epochs_from_raw(
     raw: mne.io.BaseRaw,
+    events_trial_onset: str | list[str] | list[tuple[str, str]],
+    events_trial_end: str | list[str] | list[tuple[str, str]] | None = None,
     tmin: int | float = -6,
     tmax: int | float = 6,
     baseline: tuple | None = None,
-    events_trial_onset: str | list[str | tuple[str, str]] | None = None,
-    events_trial_end: str | list[str | tuple[str, str]] | None = None,
     min_distance_trials: int | float = 0,
+    verbose: bool | str | int | None = True,
     **kwargs,
 ) -> mne.Epochs:
     """Return epochs from given events."""
@@ -395,7 +405,7 @@ def epochs_from_raw(
         tmin=tmin,
         tmax=tmax,
         baseline=baseline,
-        verbose=True,
+        verbose=verbose,
         **kwargs,
     )
     if min_distance_trials:
@@ -413,7 +423,7 @@ def epochs_from_raw(
 
 def get_events(
     raw: mne.io.BaseRaw,
-    event_picks: str | list[str | tuple[str, str]],
+    event_picks: str | list[str] | list[tuple[str, str]],
 ) -> tuple[np.ndarray, dict]:
     """Get events from given Raw instance and event id."""
     if isinstance(event_picks, str):
@@ -448,6 +458,7 @@ def discard_epochs(
     events_begin: np.ndarray,
     min_distance_events: int | float,
     events_end: np.ndarray | None = None,
+    inplace: bool = True,
 ) -> mne.Epochs:
     """Discard epochs based on minimal distance between event onset and end."""
     if events_end is not None:
@@ -456,29 +467,31 @@ def discard_epochs(
     else:
         events = events_begin[:, 0]
         event_diffs = np.diff(events)
-    drop_indices = np.where(
+    dist_small = np.where(
         event_diffs <= min_distance_events * epochs.info["sfreq"]
     )[0]
-    epochs = epochs.drop(indices=drop_indices)
-    return epochs
+    drop_indices = np.unique(np.concatenate((dist_small, dist_small + 1)))
+    if not inplace:
+        epochs = epochs.copy()
+    return epochs.drop(indices=drop_indices, reason="min_distance_events")
 
 
 def power_from_bids(
     bids_path: mne_bids.BIDSPath,
     nm_channels_dir: Path,
-    events_trial_onset: list[str] | None = None,
-    events_trial_end: list[str] | None = None,
+    events_trial_onset: str | list[str] | list[tuple[str, str]],
+    events_trial_end: str | list[str] | list[tuple[str, str]] | None = None,
     min_distance_trials: int | float = 0,
     bad_epochs_dir: Path | str | None = None,
     out_dir: Path | str | None = None,
     kwargs_preprocess: dict | None = None,
     kwargs_epochs: dict | None = None,
     kwargs_power: dict | None = None,
+    verbose: bool | str | int | None = False,
 ) -> mne.time_frequency.AverageTFR | mne.time_frequency.EpochsTFR | None:
     """Calculate power from single file."""
     print(f"File: {bids_path.basename}")
-    raw = mne_bids.read_raw_bids(bids_path, verbose=False)
-
+    raw = mne_bids.read_raw_bids(bids_path, verbose=verbose)
     try:
         if kwargs_preprocess is None:
             kwargs_preprocess = {}
@@ -487,6 +500,7 @@ def power_from_bids(
             nm_channels_dir=nm_channels_dir,
             filename=bids_path,
             pick_used_channels=True,
+            verbose=verbose,
             **kwargs_preprocess,
         )
     except ValueError as error:
@@ -538,5 +552,5 @@ def power_from_bids(
 
     if out_dir:
         fname = Path(out_dir) / (str(bids_path.fpath.stem) + "_tfr.h5")
-        power.save(fname=fname, verbose=True, overwrite=True)
+        power.save(fname=fname, verbose=verbose, overwrite=True)
     return power
diff --git a/tox.ini b/tox.ini
index ede0964..210b426 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,18 +1,14 @@
 [tox]
 envlist =
-    py3{10}
-    style
-    lint
-    typing
+    py3{12,11,10}
+    pre-commit
 skip_missing_interpreters = true
 isolated_build = true
 
 [testenv]
-deps = -r requirements/requirements_dev.txt
 commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs:tests}
 
-[testenv:{lint,style,typing}]
+[testenv:{pre-commit}]
 commands =
-    lint: pylint src/ tests/ 
-    style: black src/ tests/ --check --diff
-    typing: mypy src/
+    install_pre-commit: pip install pre-commit
+    run_pre-commit: pre-commit run --all-files


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 15c9738..0000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-name: Tests
-on:
-  push:
-    branches:
-      - main
-      - '*.x'
-    paths-ignore:
-      - 'docs/**'
-      - '*.md'
-      - '*.rst'
-      - '*.txt'
-  pull_request:
-    branches:
-      - main
-      - '*.x'
-    paths-ignore:
-      - 'docs/**'
-      - '*.md'
-      - '*.rst'
-      - '*.txt'
-jobs:
-  tests:
-    name: ${{ matrix.name }}
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - {name: 3.10 Linux, python: '3.10', os: ubuntu-latest, tox: py310}
-          - {name: 3.10 Windows, python: '3.10', os: windows-latest, tox: py310}
-          - {name: 3.10 Mac, python: '3.10', os: macos-latest, tox: py310}
-          - {name: Lint, python: '3.10', os: ubuntu-latest, tox: lint}
-          - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
-          - {name: Style, python: '3.10', os: ubuntu-latest, tox: style}
-    steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-        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@v2
-        with:
-          path: ${{ steps.pip-cache.outputs.dir }}
-          key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}|${{ hashFiles('requirements/*.txt') }}
-      - run: pip install tox
-      - run: tox -e ${{ matrix.tox }}
diff --git a/tests/test_import.py b/tests/test_import.py
index 14b1f6d..bd5128c 100644
--- a/tests/test_import.py
+++ b/tests/test_import.py
@@ -1,4 +1,5 @@
 """Test import of pte"""
+
 import pte
 
 
diff --git a/tests/time_frequency/__init__.py b/tests/time_frequency/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/time_frequency/test_bursts.py b/tests/time_frequency/test_bursts.py
deleted file mode 100644
index 375ea36..0000000
--- a/tests/time_frequency/test_bursts.py
+++ /dev/null
@@ -1,63 +0,0 @@
-from typing import Union
-
-import numpy as np
-
-
-def burst_length_and_amplitude(
-    power: np.ndarray,
-    threshold: int | float,
-    sfreq: int | float,
-    return_burst_amplitude: bool = True,
-    return_burst_indexes: bool = True,
-) -> (
-    np.ndarray |
-    tuple[np.ndarray, np.ndarray] |
-    tuple[np.ndarray, np.ndarray, np.ndarray]
-):
-    """Calculates the duration, amplitude and indexes of bursts.
-
-    Args:
-        power (np.ndarray): Power as array of shape (n_samples, )
-        threshold (int | float): Threshold of power for identifying bursts
-        sfreq (int | float): Sampling frequency
-        return_burst_amplitude (bool, optional): If True, return burst
-        amplitude. Defaults to True.
-        return_burst_indexes (bool, optional): If True, return start and stop
-        indexes of individual bursts. Defaults to True.
-
-    Returns:
-        np.ndarray |
-        tuple[np.ndarray, np.ndarray] |
-        tuple[np.ndarray, np.ndarray, np.ndarray]:
-        Burst length (in seconds), mean burst amplitude and start and end
-        indexes of individual bursts
-    """
-    bursts = np.zeros(power.shape[0] + 1)
-    bursts[1:] = power >= threshold
-    bursts_onoff = np.diff(bursts)
-    isburst = False
-    burst_length = []
-    burst_start = 0
-    burst_amplitude = []
-    burst_indexes = []
-    for index, threshold_crossed in enumerate(bursts_onoff):
-        if threshold_crossed:
-            if isburst:
-                burst_length.append(index - burst_start)
-                burst_amplitude.append(power[burst_start:index].mean())
-                burst_indexes.append([burst_start, index])
-                isburst = False
-            else:
-                burst_start = index
-                isburst = True
-    if isburst:
-        burst_length.append(index + 1 - burst_start)
-        burst_amplitude.append(power[burst_start : index + 1].mean())
-        burst_indexes.append([burst_start, index + 1])
-    burst_length = np.array(burst_length) / sfreq
-    returns = [burst_length]
-    if return_burst_amplitude:
-        returns.append(np.array(burst_amplitude))
-    if return_burst_indexes:
-        returns.append(np.array(burst_indexes))
-    return tuple(returns)